use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Code {
DslParseError,
DslTypeError,
StreamReused,
StreamabilityUnknown,
InputOrderViolation,
CaptureOverlapUnsupported,
MissingValue,
DuplicateMember,
InvalidNumber,
ProtocolOrderError,
TargetValueUnrepresentable,
ResourceLimitExceeded,
InputInvalid,
OutputFailed,
Aborted,
}
impl Code {
pub fn as_str(self) -> &'static str {
match self {
Code::DslParseError => "DSL_PARSE_ERROR",
Code::DslTypeError => "DSL_TYPE_ERROR",
Code::StreamReused => "STREAM_REUSED",
Code::StreamabilityUnknown => "STREAMABILITY_UNKNOWN",
Code::InputOrderViolation => "INPUT_ORDER_VIOLATION",
Code::CaptureOverlapUnsupported => "CAPTURE_OVERLAP_UNSUPPORTED",
Code::MissingValue => "MISSING_VALUE",
Code::DuplicateMember => "DUPLICATE_MEMBER",
Code::InvalidNumber => "INVALID_NUMBER",
Code::ProtocolOrderError => "PROTOCOL_ORDER_ERROR",
Code::TargetValueUnrepresentable => "TARGET_VALUE_UNREPRESENTABLE",
Code::ResourceLimitExceeded => "RESOURCE_LIMIT_EXCEEDED",
Code::InputInvalid => "INPUT_INVALID",
Code::OutputFailed => "OUTPUT_FAILED",
Code::Aborted => "ABORTED",
}
}
pub const ALL: [Code; 15] = [
Code::DslParseError,
Code::DslTypeError,
Code::StreamReused,
Code::StreamabilityUnknown,
Code::InputOrderViolation,
Code::CaptureOverlapUnsupported,
Code::MissingValue,
Code::DuplicateMember,
Code::InvalidNumber,
Code::ProtocolOrderError,
Code::TargetValueUnrepresentable,
Code::ResourceLimitExceeded,
Code::InputInvalid,
Code::OutputFailed,
Code::Aborted,
];
pub fn parse(text: &str) -> Option<Code> {
Code::ALL.iter().copied().find(|c| c.as_str() == text)
}
}
impl fmt::Display for Code {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Limit {
pub name: &'static str,
pub value: u64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Fail {
pub code: Code,
pub message: String,
pub path: Option<String>,
pub limit: Option<Box<Limit>>,
pub row: Option<u64>,
pub column: Option<u64>,
pub file: Option<Box<str>>,
pub committed_output: bool,
}
impl Fail {
pub fn new(code: Code, message: impl Into<String>) -> Fail {
Fail {
code,
message: message.into(),
path: None,
limit: None,
row: None,
column: None,
file: None,
committed_output: false,
}
}
pub fn in_file(mut self, file: impl Into<Box<str>>) -> Fail {
self.file = Some(file.into());
self
}
pub fn at_path(mut self, path: impl Into<String>) -> Fail {
self.path = Some(path.into());
self
}
pub fn at(mut self, row: u64, column: u64) -> Fail {
self.row = Some(row);
self.column = Some(column);
self
}
pub fn committed(mut self) -> Fail {
self.committed_output = true;
self
}
pub fn limit(name: &'static str, value: u64, message: impl Into<String>) -> Fail {
Fail {
limit: Some(Box::new(Limit { name, value })),
..Fail::new(Code::ResourceLimitExceeded, message)
}
}
pub fn protocol(message: impl Into<String>) -> Fail {
Fail::new(Code::ProtocolOrderError, message)
}
pub fn input(message: impl Into<String>) -> Fail {
Fail::new(Code::InputInvalid, message)
}
pub fn output(message: impl Into<String>) -> Fail {
Fail::new(Code::OutputFailed, message)
}
pub fn aborted() -> Fail {
Fail::new(Code::Aborted, "the run was cancelled")
}
#[cfg(feature = "tabnas")]
pub fn from_tabnas(e: &tabnas::TabnasError) -> Fail {
let mut f = Fail::new(
Code::InputInvalid,
format!("{}: {}", e.code, e.detail.trim_end()),
);
if e.row > 0 {
f.row = Some(e.row as u64);
f.column = Some(e.col as u64);
}
f
}
pub fn to_json(&self) -> serde_json::Value {
let mut m = serde_json::Map::new();
m.insert("code".into(), self.code.as_str().into());
m.insert("message".into(), self.message.clone().into());
if let Some(p) = &self.path {
m.insert("path".into(), p.clone().into());
}
if let Some(l) = &self.limit {
m.insert(
"limit".into(),
serde_json::json!({ "name": l.name, "value": l.value }),
);
}
if let Some(r) = self.row {
m.insert("row".into(), r.into());
}
if let Some(c) = self.column {
m.insert("col".into(), c.into());
}
if let Some(file) = &self.file {
m.insert("file".into(), file.to_string().into());
}
m.insert(
"output".into(),
if self.committed_output {
"partial"
} else {
"none"
}
.into(),
);
serde_json::Value::Object(m)
}
}
impl fmt::Display for Fail {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.code, self.message)?;
if let Some(p) = &self.path {
write!(f, " at {p}")?;
}
match (&self.file, self.row, self.column) {
(Some(file), Some(r), Some(c)) => write!(f, " ({file}:{r}:{c})")?,
(None, Some(r), Some(c)) => write!(f, " ({r}:{c})")?,
(Some(file), _, _) => write!(f, " (in {file})")?,
(None, _, _) => {}
}
if let Some(l) = &self.limit {
write!(f, " [{} = {}]", l.name, l.value)?;
}
Ok(())
}
}
impl std::error::Error for Fail {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_position_names_its_file_when_it_has_one() {
let plain = Fail::new(Code::DslTypeError, "arity: one argument").at(2, 3);
assert_eq!(
plain.to_string(),
"DSL_TYPE_ERROR: arity: one argument (2:3)"
);
let filed = Fail::new(Code::DslTypeError, "arity: one argument")
.at(2, 3)
.in_file("render.alc");
assert_eq!(
filed.to_string(),
"DSL_TYPE_ERROR: arity: one argument (render.alc:2:3)"
);
let no_position = Fail::new(Code::DslParseError, "bad_def: a name").in_file("lift.alc");
assert_eq!(
no_position.to_string(),
"DSL_PARSE_ERROR: bad_def: a name (in lift.alc)"
);
}
#[test]
fn codes_are_stable_names() {
for code in Code::ALL {
assert_eq!(Code::parse(code.as_str()), Some(code));
assert!(code
.as_str()
.bytes()
.all(|b| b.is_ascii_uppercase() || b == b'_'));
}
assert_eq!(Code::parse("nope"), None);
}
#[test]
fn json_shape() {
let f = Fail::limit("max_record_bytes", 64, "a row of 65 bytes")
.at_path(".rows[3]")
.committed();
let j = f.to_json();
assert_eq!(j["code"], "RESOURCE_LIMIT_EXCEEDED");
assert_eq!(j["limit"]["name"], "max_record_bytes");
assert_eq!(j["limit"]["value"], 64);
assert_eq!(j["path"], ".rows[3]");
assert_eq!(j["output"], "partial");
assert_eq!(
f.to_string(),
"RESOURCE_LIMIT_EXCEEDED: a row of 65 bytes at .rows[3] [max_record_bytes = 64]"
);
assert!(j.get("file").is_none(), "{j}");
let filed = Fail::new(Code::DslTypeError, "arity: one argument")
.at(2, 3)
.in_file("render.alc")
.to_json();
assert_eq!(filed["file"], "render.alc");
assert_eq!(
(filed["row"].clone(), filed["col"].clone()),
(2.into(), 3.into())
);
}
}