1use 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 InvalidFormat,
72 InvalidDate,
73 InvalidDateTime,
74 MissingTimezone,
75 UnknownField,
76 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#[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}