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 pub fn from_tabnas(e: &tabnas::TabnasError) -> Fail {
196 let mut f = Fail::new(
197 Code::InputInvalid,
198 format!("{}: {}", e.code, e.detail.trim_end()),
199 );
200 if e.row > 0 {
201 f.row = Some(e.row as u64);
202 f.column = Some(e.col as u64);
203 }
204 f
205 }
206
207 pub fn to_json(&self) -> serde_json::Value {
211 let mut m = serde_json::Map::new();
212 m.insert("code".into(), self.code.as_str().into());
213 m.insert("message".into(), self.message.clone().into());
214 if let Some(p) = &self.path {
215 m.insert("path".into(), p.clone().into());
216 }
217 if let Some(l) = &self.limit {
218 m.insert(
219 "limit".into(),
220 serde_json::json!({ "name": l.name, "value": l.value }),
221 );
222 }
223 if let Some(r) = self.row {
224 m.insert("row".into(), r.into());
225 }
226 if let Some(c) = self.column {
227 m.insert("col".into(), c.into());
228 }
229 if let Some(file) = &self.file {
230 m.insert("file".into(), file.to_string().into());
231 }
232 m.insert(
233 "output".into(),
234 if self.committed_output {
235 "partial"
236 } else {
237 "none"
238 }
239 .into(),
240 );
241 serde_json::Value::Object(m)
242 }
243}
244
245impl fmt::Display for Fail {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 write!(f, "{}: {}", self.code, self.message)?;
248 if let Some(p) = &self.path {
249 write!(f, " at {p}")?;
250 }
251 match (&self.file, self.row, self.column) {
252 (Some(file), Some(r), Some(c)) => write!(f, " ({file}:{r}:{c})")?,
253 (None, Some(r), Some(c)) => write!(f, " ({r}:{c})")?,
254 (Some(file), _, _) => write!(f, " (in {file})")?,
255 (None, _, _) => {}
256 }
257 if let Some(l) = &self.limit {
258 write!(f, " [{} = {}]", l.name, l.value)?;
259 }
260 Ok(())
261 }
262}
263
264impl std::error::Error for Fail {}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
273 fn a_position_names_its_file_when_it_has_one() {
274 let plain = Fail::new(Code::DslTypeError, "arity: one argument").at(2, 3);
275 assert_eq!(
276 plain.to_string(),
277 "DSL_TYPE_ERROR: arity: one argument (2:3)"
278 );
279 let filed = Fail::new(Code::DslTypeError, "arity: one argument")
280 .at(2, 3)
281 .in_file("render.alc");
282 assert_eq!(
283 filed.to_string(),
284 "DSL_TYPE_ERROR: arity: one argument (render.alc:2:3)"
285 );
286 let no_position = Fail::new(Code::DslParseError, "bad_def: a name").in_file("lift.alc");
287 assert_eq!(
288 no_position.to_string(),
289 "DSL_PARSE_ERROR: bad_def: a name (in lift.alc)"
290 );
291 }
292
293 #[test]
294 fn codes_are_stable_names() {
295 for code in Code::ALL {
296 assert_eq!(Code::parse(code.as_str()), Some(code));
297 assert!(code
298 .as_str()
299 .bytes()
300 .all(|b| b.is_ascii_uppercase() || b == b'_'));
301 }
302 assert_eq!(Code::parse("nope"), None);
303 }
304
305 #[test]
306 fn json_shape() {
307 let f = Fail::limit("max_record_bytes", 64, "a row of 65 bytes")
308 .at_path(".rows[3]")
309 .committed();
310 let j = f.to_json();
311 assert_eq!(j["code"], "RESOURCE_LIMIT_EXCEEDED");
312 assert_eq!(j["limit"]["name"], "max_record_bytes");
313 assert_eq!(j["limit"]["value"], 64);
314 assert_eq!(j["path"], ".rows[3]");
315 assert_eq!(j["output"], "partial");
316 assert_eq!(
317 f.to_string(),
318 "RESOURCE_LIMIT_EXCEEDED: a row of 65 bytes at .rows[3] [max_record_bytes = 64]"
319 );
320 assert!(j.get("file").is_none(), "{j}");
323 let filed = Fail::new(Code::DslTypeError, "arity: one argument")
324 .at(2, 3)
325 .in_file("render.alc")
326 .to_json();
327 assert_eq!(filed["file"], "render.alc");
328 assert_eq!(
329 (filed["row"].clone(), filed["col"].clone()),
330 (2.into(), 3.into())
331 );
332 }
333}