1use std::fmt;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub enum Code {
13 DslParseError,
15 DslTypeError,
17 StreamReused,
19 StreamabilityUnknown,
21 InputOrderViolation,
24 CaptureOverlapUnsupported,
26 MissingValue,
28 DuplicateMember,
30 InvalidNumber,
32 ProtocolOrderError,
35 TargetValueUnrepresentable,
38 ResourceLimitExceeded,
40 InputInvalid,
42 OutputFailed,
44 Aborted,
46}
47
48impl Code {
49 pub fn as_str(self) -> &'static str {
51 match self {
52 Code::DslParseError => "DSL_PARSE_ERROR",
53 Code::DslTypeError => "DSL_TYPE_ERROR",
54 Code::StreamReused => "STREAM_REUSED",
55 Code::StreamabilityUnknown => "STREAMABILITY_UNKNOWN",
56 Code::InputOrderViolation => "INPUT_ORDER_VIOLATION",
57 Code::CaptureOverlapUnsupported => "CAPTURE_OVERLAP_UNSUPPORTED",
58 Code::MissingValue => "MISSING_VALUE",
59 Code::DuplicateMember => "DUPLICATE_MEMBER",
60 Code::InvalidNumber => "INVALID_NUMBER",
61 Code::ProtocolOrderError => "PROTOCOL_ORDER_ERROR",
62 Code::TargetValueUnrepresentable => "TARGET_VALUE_UNREPRESENTABLE",
63 Code::ResourceLimitExceeded => "RESOURCE_LIMIT_EXCEEDED",
64 Code::InputInvalid => "INPUT_INVALID",
65 Code::OutputFailed => "OUTPUT_FAILED",
66 Code::Aborted => "ABORTED",
67 }
68 }
69
70 pub const ALL: [Code; 15] = [
72 Code::DslParseError,
73 Code::DslTypeError,
74 Code::StreamReused,
75 Code::StreamabilityUnknown,
76 Code::InputOrderViolation,
77 Code::CaptureOverlapUnsupported,
78 Code::MissingValue,
79 Code::DuplicateMember,
80 Code::InvalidNumber,
81 Code::ProtocolOrderError,
82 Code::TargetValueUnrepresentable,
83 Code::ResourceLimitExceeded,
84 Code::InputInvalid,
85 Code::OutputFailed,
86 Code::Aborted,
87 ];
88
89 pub fn parse(text: &str) -> Option<Code> {
91 Code::ALL.iter().copied().find(|c| c.as_str() == text)
92 }
93}
94
95impl fmt::Display for Code {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 f.write_str(self.as_str())
98 }
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct Limit {
104 pub name: &'static str,
106 pub value: u64,
107}
108
109#[derive(Clone, Debug, PartialEq)]
115pub struct Fail {
116 pub code: Code,
117 pub message: String,
118 pub path: Option<String>,
120 pub limit: Option<Box<Limit>>,
123 pub row: Option<u64>,
125 pub column: Option<u64>,
126 pub file: Option<Box<str>>,
130 pub committed_output: bool,
131}
132
133impl Fail {
134 pub fn new(code: Code, message: impl Into<String>) -> Fail {
135 Fail {
136 code,
137 message: message.into(),
138 path: None,
139 limit: None,
140 row: None,
141 column: None,
142 file: None,
143 committed_output: false,
144 }
145 }
146
147 pub fn in_file(mut self, file: impl Into<Box<str>>) -> Fail {
149 self.file = Some(file.into());
150 self
151 }
152
153 pub fn at_path(mut self, path: impl Into<String>) -> Fail {
154 self.path = Some(path.into());
155 self
156 }
157
158 pub fn at(mut self, row: u64, column: u64) -> Fail {
159 self.row = Some(row);
160 self.column = Some(column);
161 self
162 }
163
164 pub fn committed(mut self) -> Fail {
165 self.committed_output = true;
166 self
167 }
168
169 pub fn limit(name: &'static str, value: u64, message: impl Into<String>) -> Fail {
171 Fail {
172 limit: Some(Box::new(Limit { name, value })),
173 ..Fail::new(Code::ResourceLimitExceeded, message)
174 }
175 }
176
177 pub fn protocol(message: impl Into<String>) -> Fail {
178 Fail::new(Code::ProtocolOrderError, message)
179 }
180
181 pub fn input(message: impl Into<String>) -> Fail {
182 Fail::new(Code::InputInvalid, message)
183 }
184
185 pub fn output(message: impl Into<String>) -> Fail {
186 Fail::new(Code::OutputFailed, message)
187 }
188
189 pub fn aborted() -> Fail {
190 Fail::new(Code::Aborted, "the run was cancelled")
191 }
192
193 #[cfg(feature = "tabnas")]
197 pub fn from_tabnas(e: &tabnas::TabnasError) -> Fail {
198 let mut f = Fail::new(
199 Code::InputInvalid,
200 format!("{}: {}", e.code, e.detail.trim_end()),
201 );
202 if e.row > 0 {
203 f.row = Some(e.row as u64);
204 f.column = Some(e.col as u64);
205 }
206 f
207 }
208
209 pub fn to_json(&self) -> serde_json::Value {
213 let mut m = serde_json::Map::new();
214 m.insert("code".into(), self.code.as_str().into());
215 m.insert("message".into(), self.message.clone().into());
216 if let Some(p) = &self.path {
217 m.insert("path".into(), p.clone().into());
218 }
219 if let Some(l) = &self.limit {
220 m.insert(
221 "limit".into(),
222 serde_json::json!({ "name": l.name, "value": l.value }),
223 );
224 }
225 if let Some(r) = self.row {
226 m.insert("row".into(), r.into());
227 }
228 if let Some(c) = self.column {
229 m.insert("col".into(), c.into());
230 }
231 if let Some(file) = &self.file {
232 m.insert("file".into(), file.to_string().into());
233 }
234 m.insert(
235 "output".into(),
236 if self.committed_output {
237 "partial"
238 } else {
239 "none"
240 }
241 .into(),
242 );
243 serde_json::Value::Object(m)
244 }
245}
246
247impl fmt::Display for Fail {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 write!(f, "{}: {}", self.code, self.message)?;
250 if let Some(p) = &self.path {
251 write!(f, " at {p}")?;
252 }
253 match (&self.file, self.row, self.column) {
254 (Some(file), Some(r), Some(c)) => write!(f, " ({file}:{r}:{c})")?,
255 (None, Some(r), Some(c)) => write!(f, " ({r}:{c})")?,
256 (Some(file), _, _) => write!(f, " (in {file})")?,
257 (None, _, _) => {}
258 }
259 if let Some(l) = &self.limit {
260 write!(f, " [{} = {}]", l.name, l.value)?;
261 }
262 Ok(())
263 }
264}
265
266impl std::error::Error for Fail {}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
275 fn a_position_names_its_file_when_it_has_one() {
276 let plain = Fail::new(Code::DslTypeError, "arity: one argument").at(2, 3);
277 assert_eq!(
278 plain.to_string(),
279 "DSL_TYPE_ERROR: arity: one argument (2:3)"
280 );
281 let filed = Fail::new(Code::DslTypeError, "arity: one argument")
282 .at(2, 3)
283 .in_file("render.alc");
284 assert_eq!(
285 filed.to_string(),
286 "DSL_TYPE_ERROR: arity: one argument (render.alc:2:3)"
287 );
288 let no_position = Fail::new(Code::DslParseError, "bad_def: a name").in_file("lift.alc");
289 assert_eq!(
290 no_position.to_string(),
291 "DSL_PARSE_ERROR: bad_def: a name (in lift.alc)"
292 );
293 }
294
295 #[test]
296 fn codes_are_stable_names() {
297 for code in Code::ALL {
298 assert_eq!(Code::parse(code.as_str()), Some(code));
299 assert!(code
300 .as_str()
301 .bytes()
302 .all(|b| b.is_ascii_uppercase() || b == b'_'));
303 }
304 assert_eq!(Code::parse("nope"), None);
305 }
306
307 #[test]
308 fn json_shape() {
309 let f = Fail::limit("max_record_bytes", 64, "a row of 65 bytes")
310 .at_path(".rows[3]")
311 .committed();
312 let j = f.to_json();
313 assert_eq!(j["code"], "RESOURCE_LIMIT_EXCEEDED");
314 assert_eq!(j["limit"]["name"], "max_record_bytes");
315 assert_eq!(j["limit"]["value"], 64);
316 assert_eq!(j["path"], ".rows[3]");
317 assert_eq!(j["output"], "partial");
318 assert_eq!(
319 f.to_string(),
320 "RESOURCE_LIMIT_EXCEEDED: a row of 65 bytes at .rows[3] [max_record_bytes = 64]"
321 );
322 assert!(j.get("file").is_none(), "{j}");
325 let filed = Fail::new(Code::DslTypeError, "arity: one argument")
326 .at(2, 3)
327 .in_file("render.alc")
328 .to_json();
329 assert_eq!(filed["file"], "render.alc");
330 assert_eq!(
331 (filed["row"].clone(), filed["col"].clone()),
332 (2.into(), 3.into())
333 );
334 }
335}