Skip to main content

seam_core/
error.rs

1//! `path` and `code` are public API in every binding. `code` is stable and
2//! changes only on a major version; `message` is for humans, never parse it.
3
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum Segment {
8    Key(String),
9    Index(usize),
10}
11
12impl fmt::Display for Segment {
13    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14        match self {
15            Segment::Key(k) => write!(f, "{k}"),
16            Segment::Index(i) => write!(f, "[{i}]"),
17        }
18    }
19}
20
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct Path(pub Vec<Segment>);
23
24impl Path {
25    pub fn render(&self) -> String {
26        let mut out = String::new();
27        for seg in &self.0 {
28            match seg {
29                Segment::Key(k) => {
30                    if !out.is_empty() {
31                        out.push('.');
32                    }
33                    out.push_str(k);
34                }
35                Segment::Index(i) => {
36                    out.push('[');
37                    out.push_str(&i.to_string());
38                    out.push(']');
39                }
40            }
41        }
42        if out.is_empty() {
43            "<root>".to_string()
44        } else {
45            out
46        }
47    }
48}
49
50impl fmt::Display for Path {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        write!(f, "{}", self.render())
53    }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum Code {
58    Required,
59    NullNotAllowed,
60    TypeMismatch,
61    OutOfRange,
62    UnsafeInteger,
63    IntegerTooWide,
64    NotFinite,
65    TooShort,
66    TooLong,
67    TooFewItems,
68    TooManyItems,
69    NotInEnum,
70    /// The string did not have the shape its `@format` names.
71    InvalidFormat,
72    InvalidDate,
73    InvalidDateTime,
74    MissingTimezone,
75    UnknownField,
76    /// The tag named a variant the union does not declare.
77    UnknownVariant,
78    DepthExceeded,
79    SizeExceeded,
80    UnknownType,
81}
82
83impl Code {
84    pub fn as_str(self) -> &'static str {
85        match self {
86            Code::Required => "required",
87            Code::NullNotAllowed => "null_not_allowed",
88            Code::TypeMismatch => "type_mismatch",
89            Code::OutOfRange => "out_of_range",
90            Code::UnsafeInteger => "unsafe_integer",
91            Code::IntegerTooWide => "integer_too_wide",
92            Code::NotFinite => "not_finite",
93            Code::TooShort => "too_short",
94            Code::TooLong => "too_long",
95            Code::TooFewItems => "too_few_items",
96            Code::TooManyItems => "too_many_items",
97            Code::NotInEnum => "not_in_enum",
98            Code::InvalidFormat => "invalid_format",
99            Code::InvalidDate => "invalid_date",
100            Code::InvalidDateTime => "invalid_datetime",
101            Code::MissingTimezone => "missing_timezone",
102            Code::UnknownField => "unknown_field",
103            Code::UnknownVariant => "unknown_variant",
104            Code::DepthExceeded => "depth_exceeded",
105            Code::SizeExceeded => "size_exceeded",
106            Code::UnknownType => "unknown_type",
107        }
108    }
109}
110
111impl fmt::Display for Code {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        f.write_str(self.as_str())
114    }
115}
116
117#[derive(Debug, Clone, PartialEq)]
118pub struct Issue {
119    pub path: Path,
120    pub code: Code,
121    pub message: String,
122}
123
124impl fmt::Display for Issue {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(
127            f,
128            "{}: {} ({})",
129            self.path.render(),
130            self.message,
131            self.code
132        )
133    }
134}
135
136/// Every failure from one pass. Validation does not stop at the first, because
137/// one issue per round trip is a bad way to debug a boundary.
138#[derive(Debug, Clone, PartialEq)]
139pub struct ValidationError {
140    pub issues: Vec<Issue>,
141}
142
143impl ValidationError {
144    pub fn is_empty(&self) -> bool {
145        self.issues.is_empty()
146    }
147}
148
149impl fmt::Display for ValidationError {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        match self.issues.as_slice() {
152            [] => write!(f, "validation failed with no issues recorded"),
153            [one] => write!(f, "{one}"),
154            many => {
155                write!(f, "{} validation issues:", many.len())?;
156                for issue in many {
157                    write!(f, "\n  - {issue}")?;
158                }
159                Ok(())
160            }
161        }
162    }
163}
164
165impl std::error::Error for ValidationError {}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn paths_render_in_the_documented_form() {
173        let p = Path(vec![
174            Segment::Key("user".into()),
175            Segment::Key("tags".into()),
176            Segment::Index(2),
177        ]);
178        assert_eq!(p.render(), "user.tags[2]");
179        assert_eq!(Path::default().render(), "<root>");
180    }
181
182    #[test]
183    fn codes_are_snake_case_and_stable() {
184        assert_eq!(Code::NullNotAllowed.as_str(), "null_not_allowed");
185        assert_eq!(Code::MissingTimezone.as_str(), "missing_timezone");
186    }
187}