1use std::fmt;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum PanicKind {
10 Overflow,
12 DivideByZero,
14 IndexOutOfBounds,
17 DecodeError,
19 TypeError,
22 DerivationError,
25 CapiMisuse,
28 Internal,
30}
31
32impl PanicKind {
33 pub fn name(self) -> &'static str {
34 match self {
35 PanicKind::Overflow => "overflow",
36 PanicKind::DivideByZero => "divide-by-zero",
37 PanicKind::IndexOutOfBounds => "index-out-of-bounds",
38 PanicKind::DecodeError => "decode-error",
39 PanicKind::TypeError => "type-error",
40 PanicKind::DerivationError => "derivation-error",
41 PanicKind::CapiMisuse => "capi-misuse",
42 PanicKind::Internal => "internal",
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct TraceFrame {
53 pub definition: String,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct SoilError {
62 pub kind: PanicKind,
63 pub message: String,
64 pub trace: Vec<TraceFrame>,
65 pub inputs: Option<String>,
68}
69
70impl SoilError {
71 pub fn new(kind: PanicKind, message: impl Into<String>) -> Self {
72 SoilError {
73 kind,
74 message: message.into(),
75 trace: Vec::new(),
76 inputs: None,
77 }
78 }
79
80 pub fn with_inputs(mut self, inputs: impl Into<String>) -> Self {
81 self.inputs = Some(inputs.into());
82 self
83 }
84}
85
86impl fmt::Display for SoilError {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 write!(f, "{}: {}", self.kind.name(), self.message)?;
89 if let Some(inputs) = &self.inputs {
90 write!(f, " (inputs: {inputs})")?;
91 }
92 for frame in &self.trace {
93 write!(f, "\n in {}", frame.definition)?;
94 }
95 Ok(())
96 }
97}
98
99impl std::error::Error for SoilError {}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn display_includes_kind_message_and_trace() {
107 let mut err =
108 SoilError::new(PanicKind::DivideByZero, "divisor is zero").with_inputs("[10,0]");
109 err.trace.push(TraceFrame {
110 definition: "csvstats::mean".to_string(),
111 });
112 let rendered = err.to_string();
113 assert_eq!(
114 rendered,
115 "divide-by-zero: divisor is zero (inputs: [10,0])\n in csvstats::mean"
116 );
117 }
118
119 #[test]
120 fn kind_names_are_stable() {
121 assert_eq!(PanicKind::Overflow.name(), "overflow");
122 assert_eq!(PanicKind::Internal.name(), "internal");
123 }
124}