Skip to main content

shape/
error.rs

1//! Errors: one per failing value, with the text TypeScript renders for it.
2
3use crate::context::{join_path, PathPart};
4use crate::node::Kind;
5use crate::value::{value_kind, value_to_string, Map, Value};
6use std::fmt;
7
8/// The marks TypeScript gives its structural errors.
9pub const MARK_OBJECT_REQUIRED: i64 = 1010;
10pub const MARK_OBJECT_TYPE: i64 = 1020;
11pub const MARK_ARRAY_REQUIRED: i64 = 1030;
12pub const MARK_ARRAY_TYPE: i64 = 1040;
13pub const MARK_CHECK_TYPE: i64 = 1045;
14pub const MARK_SCALAR_TYPE: i64 = 1050;
15pub const MARK_SCALAR_REQUIRED: i64 = 1060;
16pub const MARK_NEVER: i64 = 1070;
17pub const MARK_REGEXP: i64 = 1045;
18pub const MARK_UNDEF_REQUIRED: i64 = 1080;
19pub const MARK_ARRAY_CLOSED: i64 = 1090;
20pub const MARK_OBJECT_CLOSED: i64 = 1100;
21pub const MARK_CUSTOM_CHECK_ERR: i64 = 2010;
22pub const MARK_CUSTOM_CHECK_TEXT: i64 = 4000;
23
24/// The why codes.
25pub const WHY_TYPE: &str = "type";
26pub const WHY_REQUIRED: &str = "required";
27pub const WHY_CLOSED: &str = "closed";
28pub const WHY_CHECK: &str = "check";
29pub const WHY_NEVER: &str = "never";
30pub const WHY_REGEXP: &str = "regexp";
31
32/// One validation failure.
33#[derive(Clone, Debug, Default)]
34pub struct FieldError {
35    /// The dotted path of the failing value.
36    pub path: String,
37    /// The path as parts: array indices as indices, keys as keys.
38    pub path_arr: Vec<PathPart>,
39    /// The immediate key or index.
40    pub key: String,
41    /// The kind of the node that ran the check.
42    pub kind: Kind,
43    /// The failing value.
44    pub value: Value,
45    pub why: String,
46    pub mark: i64,
47    /// The message.
48    pub text: String,
49    /// Extra context of a custom check.
50    pub args: Map,
51    /// The name of the failing check.
52    pub check: String,
53    pub(crate) parent_arr: bool,
54    pub(crate) absent: bool,
55    pub(crate) regexp_src: String,
56    pub(crate) plural: bool,
57    pub(crate) terse: bool,
58}
59
60impl fmt::Display for FieldError {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        if !self.text.is_empty() {
63            return f.write_str(&self.text);
64        }
65        if self.path.is_empty() {
66            f.write_str(&self.why)
67        } else {
68            write!(f, "{}: {}", self.path, self.why)
69        }
70    }
71}
72
73/// The errors of one validation.
74#[derive(Clone, Debug, Default)]
75pub struct ValidationError {
76    pub issues: Vec<FieldError>,
77    /// A terse collector counts rather than stores.
78    pub(crate) terse: bool,
79    pub(crate) n: usize,
80}
81
82impl ValidationError {
83    pub(crate) fn add(&mut self, err: FieldError) {
84        if self.terse {
85            self.n += 1;
86            return;
87        }
88        self.issues.push(err);
89    }
90
91    pub fn has_any(&self) -> bool {
92        self.n > 0 || !self.issues.is_empty()
93    }
94
95    /// How many errors were added, counted or stored.
96    pub(crate) fn count(&self) -> usize {
97        self.n + self.issues.len()
98    }
99}
100
101impl fmt::Display for ValidationError {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        let parts: Vec<String> = self.issues.iter().map(|e| e.to_string()).collect();
104        f.write_str(&parts.join("\n"))
105    }
106}
107
108impl std::error::Error for ValidationError {}
109
110/// Where an error happened: what `make_err` needs of the state.
111pub(crate) struct At<'a> {
112    pub path: &'a [PathPart],
113    pub key: &'a str,
114    pub kind: Kind,
115    pub value: &'a Value,
116    pub parent_arr: bool,
117    pub absent: bool,
118    pub check: &'a str,
119    pub regexp_src: Option<String>,
120    pub terse: bool,
121}
122
123/// An error at `at`, with the default text for `why` or the `text` given
124/// (`$PATH` and `$VALUE` expanded).
125pub(crate) fn make_err(at: &At<'_>, why: &str, mark: i64, text: &str) -> FieldError {
126    let why = if why.is_empty() { WHY_CHECK } else { why };
127    let mark = if mark == 0 {
128        MARK_CUSTOM_CHECK_TEXT
129    } else {
130        mark
131    };
132    if at.terse {
133        return FieldError {
134            key: at.key.to_string(),
135            kind: at.kind,
136            value: at.value.clone(),
137            why: why.to_string(),
138            mark,
139            check: at.check.to_string(),
140            terse: true,
141            ..Default::default()
142        };
143    }
144    let path = join_path(at.path);
145    let mut err = FieldError {
146        path,
147        path_arr: at.path.to_vec(),
148        key: at.key.to_string(),
149        kind: at.kind,
150        value: at.value.clone(),
151        why: why.to_string(),
152        mark,
153        check: at.check.to_string(),
154        parent_arr: at.parent_arr,
155        absent: at.absent,
156        regexp_src: at.regexp_src.clone().unwrap_or_default(),
157        ..Default::default()
158    };
159    err.text = if text.is_empty() {
160        default_err_text(&err)
161    } else {
162        expand_err_text(text, &err.path, at.value, at.absent)
163    };
164    err
165}
166
167/// Expand a message template: `$PATH` and `$VALUE`, a missing value as
168/// "undefined".
169pub(crate) fn expand_err_text(text: &str, path: &str, val: &Value, absent: bool) -> String {
170    let valstr = if absent {
171        "undefined".to_string()
172    } else {
173        value_to_string(val)
174    };
175    text.replace("$PATH", path).replace("$VALUE", &valstr)
176}
177
178/// The text of a structural error, as TypeScript renders it.
179pub(crate) fn default_err_text(e: &FieldError) -> String {
180    let mut valstr = value_to_string(&e.value);
181    let mut valkind = value_kind(&e.value);
182    if e.absent {
183        valstr = "undefined".to_string();
184        valkind = "value";
185    }
186    // "index" when the value renders as an array or its parent is one.
187    let propkind = if e.parent_arr || valstr.starts_with('[') {
188        "index"
189    } else {
190        "property"
191    };
192    let path_part = if e.path.is_empty() {
193        String::new()
194    } else {
195        format!("{} \"{}\" with ", propkind, e.path)
196    };
197    let head = format!(
198        "Validation failed for {}{} \"{}\"",
199        path_part, valkind, valstr
200    );
201    match e.why.as_str() {
202        WHY_TYPE => format!(
203            "{} because the {} is not of type {}.",
204            head, valkind, e.kind
205        ),
206        WHY_REQUIRED => {
207            // A property that is not there is named, not rendered.
208            if e.absent && !e.path.is_empty() {
209                let noun = if propkind == "index" {
210                    "element"
211                } else {
212                    "property"
213                };
214                return format!(
215                    "Validation failed for {} \"{}\" because the {} is missing.",
216                    propkind, e.path, noun
217                );
218            }
219            if matches!(e.value, Value::Str(ref s) if s.is_empty()) {
220                format!("{} because an empty string is not allowed.", head)
221            } else if e.value.is_null() || e.absent {
222                format!("{} because the value is required.", head)
223            } else {
224                format!("{} because the {} is required.", head, valkind)
225            }
226        }
227        WHY_CLOSED => {
228            let (noun, verb) = if e.plural {
229                ("properties", "are")
230            } else {
231                (propkind, "is")
232            };
233            format!(
234                "{} because the {} \"{}\" {} not allowed.",
235                head, noun, e.key, verb
236            )
237        }
238        WHY_NEVER => format!("{} because no value is allowed.", head),
239        WHY_REGEXP => format!(
240            "{} because the {} did not match {}.",
241            head, valkind, e.regexp_src
242        ),
243        _ => {
244            let name = if e.check.is_empty() {
245                e.why.as_str()
246            } else {
247                e.check.as_str()
248            };
249            format!("{} because check \"{}\" failed.", head, name)
250        }
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    fn at<'a>(path: &'a [PathPart], value: &'a Value, absent: bool) -> At<'a> {
259        At {
260            path,
261            key: "k",
262            kind: Kind::String,
263            value,
264            parent_arr: false,
265            absent,
266            check: "",
267            regexp_src: None,
268            terse: false,
269        }
270    }
271
272    #[test]
273    fn texts() {
274        let p = vec![PathPart::Key("a".into())];
275        let v = Value::from(1);
276        let e = make_err(&at(&p, &v, false), WHY_TYPE, MARK_SCALAR_TYPE, "");
277        assert_eq!(e.text, "Validation failed for property \"a\" with number \"1\" because the number is not of type string.");
278        assert_eq!(e.to_string(), e.text);
279        let e = make_err(&at(&p, &Value::Undefined, true), WHY_REQUIRED, 0, "");
280        assert_eq!(
281            e.text,
282            "Validation failed for property \"a\" because the property is missing."
283        );
284        assert_eq!(e.mark, MARK_CUSTOM_CHECK_TEXT);
285        let e = make_err(&at(&[], &Value::Undefined, true), WHY_REQUIRED, 1, "");
286        assert_eq!(
287            e.text,
288            "Validation failed for value \"undefined\" because the value is required."
289        );
290        let e = make_err(&at(&p, &Value::from(""), false), WHY_REQUIRED, 1, "");
291        assert_eq!(e.text, "Validation failed for property \"a\" with string \"\" because an empty string is not allowed.");
292        let e = make_err(&at(&p, &Value::Null, false), WHY_REQUIRED, 1, "");
293        assert_eq!(e.text, "Validation failed for property \"a\" with value \"null\" because the value is required.");
294        let e = make_err(&at(&p, &Value::from(2), false), WHY_REQUIRED, 1, "");
295        assert_eq!(e.text, "Validation failed for property \"a\" with number \"2\" because the number is required.");
296        let mut e = make_err(&at(&p, &v, false), WHY_CLOSED, 1, "");
297        e.plural = true;
298        e.key = "x, y".into();
299        assert_eq!(default_err_text(&e), "Validation failed for property \"a\" with number \"1\" because the properties \"x, y\" are not allowed.");
300        e.plural = false;
301        assert_eq!(default_err_text(&e), "Validation failed for property \"a\" with number \"1\" because the property \"x, y\" is not allowed.");
302        let e = make_err(&at(&p, &v, false), WHY_NEVER, 1, "");
303        assert_eq!(
304            e.text,
305            "Validation failed for property \"a\" with number \"1\" because no value is allowed."
306        );
307        let mut a = at(&p, &v, false);
308        a.regexp_src = Some("/x/".into());
309        let e = make_err(&a, WHY_REGEXP, 1, "");
310        assert_eq!(e.text, "Validation failed for property \"a\" with number \"1\" because the number did not match /x/.");
311        let mut a = at(&p, &v, false);
312        a.check = "Min";
313        let e = make_err(&a, "", 1, "");
314        assert_eq!(
315            e.text,
316            "Validation failed for property \"a\" with number \"1\" because check \"Min\" failed."
317        );
318        let e = make_err(&at(&p, &v, false), "why", 1, "");
319        assert_eq!(
320            e.text,
321            "Validation failed for property \"a\" with number \"1\" because check \"why\" failed."
322        );
323        let e = make_err(
324            &at(&p, &v, false),
325            "why",
326            1,
327            "Value \"$VALUE\" at \"$PATH\" is bad.",
328        );
329        assert_eq!(e.text, "Value \"1\" at \"a\" is bad.");
330        assert_eq!(
331            expand_err_text("$VALUE", "", &Value::Null, true),
332            "undefined"
333        );
334        let arr = Value::Arr(vec![Value::from(1)]);
335        let e = make_err(&at(&p, &arr, false), WHY_TYPE, 1, "");
336        assert_eq!(e.text, "Validation failed for index \"a\" with array \"[1]\" because the array is not of type string.");
337        let mut a = at(&p, &v, false);
338        a.parent_arr = true;
339        a.absent = true;
340        let e = make_err(&a, WHY_REQUIRED, 1, "");
341        assert_eq!(
342            e.text,
343            "Validation failed for index \"a\" because the element is missing."
344        );
345        let mut a = at(&p, &v, false);
346        a.terse = true;
347        let e = make_err(&a, WHY_TYPE, 1, "");
348        assert!(e.terse && e.text.is_empty() && e.path.is_empty());
349        let plain = FieldError {
350            path: "a".into(),
351            why: "w".into(),
352            ..Default::default()
353        };
354        assert_eq!(plain.to_string(), "a: w");
355        let plain = FieldError {
356            why: "w".into(),
357            ..Default::default()
358        };
359        assert_eq!(plain.to_string(), "w");
360    }
361
362    #[test]
363    fn collector() {
364        let mut v = ValidationError::default();
365        assert!(!v.has_any());
366        v.add(FieldError {
367            text: "a".into(),
368            ..Default::default()
369        });
370        v.add(FieldError {
371            text: "b".into(),
372            ..Default::default()
373        });
374        assert_eq!(v.to_string(), "a\nb");
375        let mut t = ValidationError {
376            terse: true,
377            ..Default::default()
378        };
379        t.add(FieldError::default());
380        t.add(FieldError::default());
381        assert!(t.has_any() && t.issues.is_empty() && t.n == 2);
382        let _: &dyn std::error::Error = &v;
383    }
384}