Skip to main content

shape/
context.rs

1//! What flows through a validation: the caller's context, the state a
2//! validator sees at its node, and the update it fills in.
3
4use crate::error::FieldError;
5use crate::node::Node;
6use crate::value::Value;
7use std::any::Any;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// A path element: an object key or an array index.
12#[derive(Clone, Debug, PartialEq)]
13pub enum PathPart {
14    /// A property key, shared with the node that declares it.
15    Key(Arc<str>),
16    Index(usize),
17}
18
19impl PathPart {
20    pub fn as_key(&self) -> String {
21        match self {
22            PathPart::Key(k) => k.to_string(),
23            PathPart::Index(i) => i.to_string(),
24        }
25    }
26
27    fn write_to(&self, out: &mut String) {
28        match self {
29            PathPart::Key(k) => out.push_str(k),
30            PathPart::Index(i) => out.push_str(&i.to_string()),
31        }
32    }
33}
34
35/// The caller's context: per-call state custom validators may share, and
36/// the definitions `Define` records for `Refer`.
37#[derive(Default)]
38pub struct Context {
39    /// Anything a validator wants to keep across nodes: a counter, a handle,
40    /// a domain object. Type-erased, as TypeScript takes any property and Go
41    /// a `map[string]any`.
42    pub custom: HashMap<String, Box<dyn Any + Send + Sync>>,
43    /// The nodes `Define` recorded during this call.
44    pub refs: HashMap<String, Arc<Node>>,
45    /// The schema's own definitions, as of its compile.
46    pub(crate) defs: Arc<HashMap<String, Arc<Node>>>,
47    /// The errors of the call, when the caller asked for them.
48    pub err: Vec<FieldError>,
49    /// The caller wants a verdict only, so an error is recorded without its
50    /// path, text or value rendering.
51    pub(crate) terse: bool,
52}
53
54impl Context {
55    pub fn new() -> Context {
56        Context::default()
57    }
58
59    /// A typed read of `custom`.
60    pub fn get<T: 'static>(&self, key: &str) -> Option<&T> {
61        self.custom.get(key).and_then(|b| b.downcast_ref::<T>())
62    }
63
64    /// A typed write to `custom`.
65    pub fn set<T: Any + Send + Sync>(&mut self, key: &str, value: T) {
66        self.custom.insert(key.to_string(), Box::new(value));
67    }
68}
69
70/// What a custom validator sees: where it is, what it is looking at.
71pub struct State<'a> {
72    /// The path from the root, the current key last: array indices as
73    /// indices, keys as keys.
74    pub path_arr: &'a [PathPart],
75    /// The immediate key or index.
76    pub key: &'a str,
77    /// The value under validation.
78    pub value: &'a Value,
79    /// The node of the value.
80    pub node: &'a Node,
81    /// Whether the parent is an array (an index) or an object (a property).
82    pub parent_is_array: bool,
83    /// True on a `matches` or `valid` call: nothing is produced.
84    pub is_match: bool,
85    pub ctx: &'a mut Context,
86    /// The value is missing, rather than present and null.
87    pub absent: bool,
88    /// The name of the validator running, for `check "<name>" failed`.
89    pub check_name: &'a str,
90}
91
92impl<'a> State<'a> {
93    /// The path as dotted text.
94    pub fn path_str(&self) -> String {
95        join_path(self.path_arr)
96    }
97
98    /// The path as keys, every index as its digits.
99    pub fn path_keys(&self) -> Vec<String> {
100        self.path_arr.iter().map(|p| p.as_key()).collect()
101    }
102}
103
104/// The dotted form of a path, empty segments skipped.
105pub fn join_path(path: &[PathPart]) -> String {
106    let mut out = String::new();
107    for p in path {
108        if matches!(p, PathPart::Key(k) if k.is_empty()) {
109            continue;
110        }
111        if !out.is_empty() {
112            out.push('.');
113        }
114        p.write_to(&mut out);
115    }
116    out
117}
118
119/// An error a validator reports.
120#[derive(Clone, Debug)]
121pub enum UpdateErr {
122    /// A message; `$PATH` and `$VALUE` are expanded.
123    Text(String),
124    /// A ready error, its path filled in when empty.
125    Field(Box<FieldError>),
126    /// Several ready errors.
127    Fields(Vec<FieldError>),
128}
129
130/// The bag a custom validator fills in.
131#[derive(Default)]
132pub struct Update {
133    /// Stop running further checks and the structural check.
134    pub done: bool,
135    /// The why code on failure.
136    pub why: Option<String>,
137    /// The numeric mark on failure.
138    pub mark: i64,
139    pub err: Option<UpdateErr>,
140    /// A replacement value.
141    pub val: Option<Value>,
142    /// A replacement node (used by `Refer`).
143    pub node: Option<Arc<Node>>,
144    /// The failure ends the whole walk's errors here.
145    pub fatal: bool,
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn context_custom_is_typed() {
154        let mut c = Context::new();
155        c.set("n", 3u32);
156        assert_eq!(c.get::<u32>("n"), Some(&3));
157        assert_eq!(c.get::<String>("n"), None);
158        assert_eq!(c.get::<u32>("m"), None);
159    }
160
161    #[test]
162    fn paths_join_and_render() {
163        let k = |s: &str| PathPart::Key(s.into());
164        assert_eq!(
165            join_path(&[k("a"), k(""), k("b"), PathPart::Index(1)]),
166            "a.b.1"
167        );
168        assert_eq!(join_path(&[]), "");
169        assert_eq!(PathPart::Key("k".into()).as_key(), "k");
170        assert_eq!(PathPart::Index(2).as_key(), "2");
171        let mut ctx = Context::new();
172        let node = Node::default();
173        let path = [k("a")];
174        let s = State {
175            path_arr: &path,
176            key: "a",
177            value: &Value::Null,
178            node: &node,
179            parent_is_array: false,
180            is_match: false,
181            ctx: &mut ctx,
182            absent: false,
183            check_name: "",
184        };
185        assert_eq!(s.path_str(), "a");
186        assert_eq!(s.path_keys(), vec!["a".to_string()]);
187        let u = Update::default();
188        assert!(!u.done && u.val.is_none());
189    }
190}