siderite_validation/
error.rs1use std::borrow::Cow;
4use std::fmt;
5
6use serde::ser::SerializeStruct;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(untagged)]
15pub enum LocationItem {
16 Key(String),
18 Index(usize),
20}
21
22impl LocationItem {
23 pub fn key(key: impl Into<String>) -> Self {
25 Self::Key(key.into())
26 }
27
28 pub fn index(index: usize) -> Self {
30 Self::Index(index)
31 }
32}
33
34impl fmt::Display for LocationItem {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 Self::Key(key) => f.write_str(key),
38 Self::Index(index) => write!(f, "{index}"),
39 }
40 }
41}
42
43impl From<String> for LocationItem {
44 fn from(key: String) -> Self {
45 Self::Key(key)
46 }
47}
48
49impl From<&str> for LocationItem {
50 fn from(key: &str) -> Self {
51 Self::Key(key.to_owned())
52 }
53}
54
55impl From<usize> for LocationItem {
56 fn from(index: usize) -> Self {
57 Self::Index(index)
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
63#[error("{code}: {message}")]
64pub struct FieldError {
65 pub location: Vec<LocationItem>,
67 pub code: Cow<'static, str>,
69 pub message: String,
71}
72
73impl FieldError {
74 pub fn new(code: impl Into<Cow<'static, str>>, message: impl Into<String>) -> Self {
76 Self {
77 location: Vec::new(),
78 code: code.into(),
79 message: message.into(),
80 }
81 }
82
83 #[must_use]
85 pub fn at(mut self, item: impl Into<LocationItem>) -> Self {
86 self.location.insert(0, item.into());
87 self
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Default, Error)]
99#[error("validation failed with {n} error(s)", n = self.errors.len())]
100pub struct ValidationError {
101 pub errors: Vec<FieldError>,
103}
104
105impl ValidationError {
106 pub fn new() -> Self {
108 Self::default()
109 }
110
111 pub fn push(&mut self, error: FieldError) {
113 self.errors.push(error);
114 }
115
116 pub fn is_empty(&self) -> bool {
118 self.errors.is_empty()
119 }
120
121 pub fn merge(&mut self, other: Self) {
123 self.errors.extend(other.errors);
124 }
125
126 #[must_use]
128 pub fn prefixed(self, item: impl Into<LocationItem>) -> Self {
129 let item = item.into();
130 Self {
131 errors: self
132 .errors
133 .into_iter()
134 .map(|error| error.at(item.clone()))
135 .collect(),
136 }
137 }
138
139 pub fn into_result(self) -> Result<(), Self> {
141 if self.is_empty() { Ok(()) } else { Err(self) }
142 }
143}
144
145impl From<FieldError> for ValidationError {
146 fn from(error: FieldError) -> Self {
147 Self {
148 errors: vec![error],
149 }
150 }
151}
152
153impl Serialize for ValidationError {
154 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
155 where
156 S: serde::Serializer,
157 {
158 let mut state = serializer.serialize_struct("ValidationError", 2)?;
159 state.serialize_field("type", "validation_error")?;
160 state.serialize_field("errors", &self.errors)?;
161 state.end()
162 }
163}
164
165pub type ValidationResult<T> = Result<T, ValidationError>;
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn at_prepends_location_segments() {
174 let error = FieldError::new("invalid_email", "invalid email address")
175 .at(LocationItem::key("email"))
176 .at(LocationItem::key("body"));
177 assert_eq!(
178 error.location,
179 vec![LocationItem::key("body"), LocationItem::key("email")]
180 );
181 }
182
183 #[test]
184 fn serialize_matches_documented_shape() {
185 let mut error = ValidationError::new();
186 error.push(
187 FieldError::new("invalid_email", "invalid email address")
188 .at("email")
189 .at("body"),
190 );
191 let json = serde_json::to_value(&error).unwrap();
192 assert_eq!(
193 json,
194 serde_json::json!({
195 "type": "validation_error",
196 "errors": [{
197 "location": ["body", "email"],
198 "code": "invalid_email",
199 "message": "invalid email address"
200 }]
201 })
202 );
203 }
204
205 #[test]
206 fn location_index_serializes_as_number() {
207 let error = FieldError::new("greater_than", "too small").at(LocationItem::index(0));
208 let json = serde_json::to_value(&error).unwrap();
209 assert_eq!(json["location"], serde_json::json!([0]));
210 }
211
212 #[test]
213 fn location_item_untagged_round_trip() {
214 let key: LocationItem = serde_json::from_value(serde_json::json!("email")).unwrap();
215 let index: LocationItem = serde_json::from_value(serde_json::json!(3)).unwrap();
216 assert_eq!(key, LocationItem::key("email"));
217 assert_eq!(index, LocationItem::index(3));
218 }
219
220 #[test]
221 fn into_result_ok_when_empty() {
222 assert!(ValidationError::new().into_result().is_ok());
223 }
224
225 #[test]
226 fn into_result_err_when_nonempty() {
227 let mut error = ValidationError::new();
228 error.push(FieldError::new("invalid_email", "invalid email address"));
229 assert!(error.into_result().is_err());
230 }
231
232 #[test]
233 fn merge_appends_errors() {
234 let mut left = ValidationError::from(FieldError::new("a", "A"));
235 left.merge(ValidationError::from(FieldError::new("b", "B")));
236 assert_eq!(left.errors.len(), 2);
237 assert_eq!(left.errors[0].code, "a");
238 assert_eq!(left.errors[1].code, "b");
239 }
240
241 #[test]
242 fn prefixed_prepends_to_every_error() {
243 let mut error = ValidationError::new();
244 error.push(FieldError::new("x", "X").at("email"));
245 error.push(FieldError::new("y", "Y").at("name"));
246 let error = error.prefixed("body");
247 assert_eq!(
248 error.errors[0].location,
249 vec![LocationItem::key("body"), LocationItem::key("email")]
250 );
251 assert_eq!(
252 error.errors[1].location,
253 vec![LocationItem::key("body"), LocationItem::key("name")]
254 );
255 }
256
257 #[test]
258 fn is_empty_tracks_contents() {
259 let mut error = ValidationError::new();
260 assert!(error.is_empty());
261 error.push(FieldError::new("x", "X"));
262 assert!(!error.is_empty());
263 }
264
265 #[test]
266 fn display_and_error_trait() {
267 let error =
268 ValidationError::from(FieldError::new("invalid_email", "invalid email address"));
269 assert_eq!(error.to_string(), "validation failed with 1 error(s)");
270 let _: &dyn std::error::Error = &error;
271 assert_eq!(
272 error.errors[0].to_string(),
273 "invalid_email: invalid email address"
274 );
275 }
276}