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        // Counted, never read: nothing of the key, the value or the
134        // check is kept.
135        return FieldError {
136            kind: at.kind,
137            mark,
138            terse: true,
139            ..Default::default()
140        };
141    }
142    let path = join_path(at.path);
143    let mut err = FieldError {
144        path,
145        path_arr: at.path.to_vec(),
146        key: at.key.to_string(),
147        kind: at.kind,
148        value: at.value.clone(),
149        why: why.to_string(),
150        mark,
151        check: at.check.to_string(),
152        parent_arr: at.parent_arr,
153        absent: at.absent,
154        regexp_src: at.regexp_src.clone().unwrap_or_default(),
155        ..Default::default()
156    };
157    err.text = if text.is_empty() {
158        default_err_text(&err)
159    } else {
160        expand_err_text(text, &err.path, at.value, at.absent)
161    };
162    err
163}
164
165/// Expand a message template: `$PATH` and `$VALUE`, a missing value as
166/// "undefined".
167pub(crate) fn expand_err_text(text: &str, path: &str, val: &Value, absent: bool) -> String {
168    let valstr = if absent {
169        "undefined".to_string()
170    } else {
171        value_to_string(val)
172    };
173    text.replace("$PATH", path).replace("$VALUE", &valstr)
174}
175
176/// The text of a structural error, as TypeScript renders it.
177pub(crate) fn default_err_text(e: &FieldError) -> String {
178    let (valstr, valkind) = if e.absent {
179        ("undefined".to_string(), "value")
180    } else {
181        (value_to_string(&e.value), value_kind(&e.value))
182    };
183    // "index" when the value renders as an array or its parent is one.
184    let propkind = if e.parent_arr || valstr.starts_with('[') {
185        "index"
186    } else {
187        "property"
188    };
189    // A property that is not there is named, not rendered.
190    if e.why == WHY_REQUIRED && e.absent && !e.path.is_empty() {
191        let noun = if propkind == "index" {
192            "element"
193        } else {
194            "property"
195        };
196        return format!(
197            "Validation failed for {} \"{}\" because the {} is missing.",
198            propkind, e.path, noun
199        );
200    }
201    // Written once into one buffer: the head, then the clause of the why.
202    let mut out = String::with_capacity(96 + e.path.len() + valstr.len() + e.key.len());
203    out.push_str("Validation failed for ");
204    if !e.path.is_empty() {
205        out.push_str(propkind);
206        out.push_str(" \"");
207        out.push_str(&e.path);
208        out.push_str("\" with ");
209    }
210    out.push_str(valkind);
211    out.push_str(" \"");
212    out.push_str(&valstr);
213    out.push_str("\" because ");
214    match e.why.as_str() {
215        WHY_TYPE => {
216            out.push_str("the ");
217            out.push_str(valkind);
218            out.push_str(" is not of type ");
219            out.push_str(e.kind.as_str());
220            out.push('.');
221        }
222        WHY_REQUIRED => {
223            if matches!(e.value, Value::Str(ref s) if s.is_empty()) {
224                out.push_str("an empty string is not allowed.");
225            } else if e.value.is_null() || e.absent {
226                out.push_str("the value is required.");
227            } else {
228                out.push_str("the ");
229                out.push_str(valkind);
230                out.push_str(" is required.");
231            }
232        }
233        WHY_CLOSED => {
234            let (noun, verb) = if e.plural {
235                ("properties", "are")
236            } else {
237                (propkind, "is")
238            };
239            out.push_str("the ");
240            out.push_str(noun);
241            out.push_str(" \"");
242            out.push_str(&e.key);
243            out.push_str("\" ");
244            out.push_str(verb);
245            out.push_str(" not allowed.");
246        }
247        WHY_NEVER => out.push_str("no value is allowed."),
248        WHY_REGEXP => {
249            out.push_str("the ");
250            out.push_str(valkind);
251            out.push_str(" did not match ");
252            out.push_str(&e.regexp_src);
253            out.push('.');
254        }
255        _ => {
256            let name = if e.check.is_empty() {
257                e.why.as_str()
258            } else {
259                e.check.as_str()
260            };
261            out.push_str("check \"");
262            out.push_str(name);
263            out.push_str("\" failed.");
264        }
265    }
266    out
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    fn at<'a>(path: &'a [PathPart], value: &'a Value, absent: bool) -> At<'a> {
274        At {
275            path,
276            key: "k",
277            kind: Kind::String,
278            value,
279            parent_arr: false,
280            absent,
281            check: "",
282            regexp_src: None,
283            terse: false,
284        }
285    }
286
287    #[test]
288    fn texts() {
289        let p = vec![PathPart::Key("a".into())];
290        let v = Value::from(1);
291        let e = make_err(&at(&p, &v, false), WHY_TYPE, MARK_SCALAR_TYPE, "");
292        assert_eq!(e.text, "Validation failed for property \"a\" with number \"1\" because the number is not of type string.");
293        assert_eq!(e.to_string(), e.text);
294        let e = make_err(&at(&p, &Value::Undefined, true), WHY_REQUIRED, 0, "");
295        assert_eq!(
296            e.text,
297            "Validation failed for property \"a\" because the property is missing."
298        );
299        assert_eq!(e.mark, MARK_CUSTOM_CHECK_TEXT);
300        let e = make_err(&at(&[], &Value::Undefined, true), WHY_REQUIRED, 1, "");
301        assert_eq!(
302            e.text,
303            "Validation failed for value \"undefined\" because the value is required."
304        );
305        let e = make_err(&at(&p, &Value::from(""), false), WHY_REQUIRED, 1, "");
306        assert_eq!(e.text, "Validation failed for property \"a\" with string \"\" because an empty string is not allowed.");
307        let e = make_err(&at(&p, &Value::Null, false), WHY_REQUIRED, 1, "");
308        assert_eq!(e.text, "Validation failed for property \"a\" with value \"null\" because the value is required.");
309        let e = make_err(&at(&p, &Value::from(2), false), WHY_REQUIRED, 1, "");
310        assert_eq!(e.text, "Validation failed for property \"a\" with number \"2\" because the number is required.");
311        let mut e = make_err(&at(&p, &v, false), WHY_CLOSED, 1, "");
312        e.plural = true;
313        e.key = "x, y".into();
314        assert_eq!(default_err_text(&e), "Validation failed for property \"a\" with number \"1\" because the properties \"x, y\" are not allowed.");
315        e.plural = false;
316        assert_eq!(default_err_text(&e), "Validation failed for property \"a\" with number \"1\" because the property \"x, y\" is not allowed.");
317        let e = make_err(&at(&p, &v, false), WHY_NEVER, 1, "");
318        assert_eq!(
319            e.text,
320            "Validation failed for property \"a\" with number \"1\" because no value is allowed."
321        );
322        let mut a = at(&p, &v, false);
323        a.regexp_src = Some("/x/".into());
324        let e = make_err(&a, WHY_REGEXP, 1, "");
325        assert_eq!(e.text, "Validation failed for property \"a\" with number \"1\" because the number did not match /x/.");
326        let mut a = at(&p, &v, false);
327        a.check = "Min";
328        let e = make_err(&a, "", 1, "");
329        assert_eq!(
330            e.text,
331            "Validation failed for property \"a\" with number \"1\" because check \"Min\" failed."
332        );
333        let e = make_err(&at(&p, &v, false), "why", 1, "");
334        assert_eq!(
335            e.text,
336            "Validation failed for property \"a\" with number \"1\" because check \"why\" failed."
337        );
338        let e = make_err(
339            &at(&p, &v, false),
340            "why",
341            1,
342            "Value \"$VALUE\" at \"$PATH\" is bad.",
343        );
344        assert_eq!(e.text, "Value \"1\" at \"a\" is bad.");
345        assert_eq!(
346            expand_err_text("$VALUE", "", &Value::Null, true),
347            "undefined"
348        );
349        let arr = Value::Arr(vec![Value::from(1)]);
350        let e = make_err(&at(&p, &arr, false), WHY_TYPE, 1, "");
351        assert_eq!(e.text, "Validation failed for index \"a\" with array \"[1]\" because the array is not of type string.");
352        let mut a = at(&p, &v, false);
353        a.parent_arr = true;
354        a.absent = true;
355        let e = make_err(&a, WHY_REQUIRED, 1, "");
356        assert_eq!(
357            e.text,
358            "Validation failed for index \"a\" because the element is missing."
359        );
360        let mut a = at(&p, &v, false);
361        a.terse = true;
362        let e = make_err(&a, WHY_TYPE, 1, "");
363        assert!(e.terse && e.text.is_empty() && e.path.is_empty());
364        let plain = FieldError {
365            path: "a".into(),
366            why: "w".into(),
367            ..Default::default()
368        };
369        assert_eq!(plain.to_string(), "a: w");
370        let plain = FieldError {
371            why: "w".into(),
372            ..Default::default()
373        };
374        assert_eq!(plain.to_string(), "w");
375    }
376
377    #[test]
378    fn collector() {
379        let mut v = ValidationError::default();
380        assert!(!v.has_any());
381        v.add(FieldError {
382            text: "a".into(),
383            ..Default::default()
384        });
385        v.add(FieldError {
386            text: "b".into(),
387            ..Default::default()
388        });
389        assert_eq!(v.to_string(), "a\nb");
390        let mut t = ValidationError {
391            terse: true,
392            ..Default::default()
393        };
394        t.add(FieldError::default());
395        t.add(FieldError::default());
396        assert!(t.has_any() && t.issues.is_empty() && t.n == 2);
397        let _: &dyn std::error::Error = &v;
398    }
399}