Skip to main content

shape/
discriminated.rs

1//! A discriminated union: the branch is chosen by the value of a tag
2//! property and the value validated against that branch alone, so the
3//! errors are its own rather than a list of every alternative.
4
5use crate::builders::{buildize, fault_node};
6use crate::context::{State, Update, UpdateErr};
7use crate::error::ValidationError;
8use crate::node::{Kind, Node, Validator};
9use crate::normalize::literal_node;
10use crate::spec::Spec;
11use crate::validate::{validate_node, Cur, Walk};
12use crate::value::{json_render, Value};
13use std::sync::Arc;
14
15/// The tag of a discriminated union and its branch names, sorted. The
16/// branches themselves are the node's list, in that order.
17#[derive(Clone, Debug, Default, PartialEq, Eq)]
18pub struct Disc {
19    pub tag: String,
20    pub tags: Vec<String>,
21}
22
23/// A union choosing its branch by the value of the tag property. An
24/// object-shaped branch without the tag property has it added, as the
25/// literal it is keyed by.
26pub fn discriminated<I, K, S>(tag: impl Into<String>, branches: I) -> Node
27where
28    I: IntoIterator<Item = (K, S)>,
29    K: Into<String>,
30    S: Into<Spec>,
31{
32    let tag = tag.into();
33    let mut pairs: Vec<(String, Spec)> = branches
34        .into_iter()
35        .map(|(k, s)| (k.into(), s.into()))
36        .collect();
37    if tag.is_empty() || pairs.is_empty() {
38        return fault_node("Discriminated needs a tag property name and at least one branch");
39    }
40    pairs.sort_by(|a, b| a.0.cmp(&b.0));
41
42    let mut n = Node::of(Kind::List);
43    n.required = true;
44    n.required_set = true;
45    let mut tags = Vec::with_capacity(pairs.len());
46    for (name, spec) in pairs {
47        let mut bn = buildize(spec);
48        if bn.kind == Kind::Object && !bn.obj_children.contains_key(&tag) {
49            bn.obj_children
50                .insert(tag.clone(), literal_node(Value::Str(name.clone())));
51        }
52        tags.push(name);
53        n.list.push(bn);
54    }
55    n.disc = Some(Disc {
56        tag: tag.clone(),
57        tags,
58    });
59    let chosen = tag.clone();
60    n.befores.push(Validator {
61        name: "Discriminated".to_string(),
62        func: Arc::new(move |state: &mut State<'_>, update: &mut Update| {
63            choose(&chosen, state, update)
64        }),
65        args: vec![Value::Str(tag)],
66        suffix: None,
67        inner: None,
68    });
69    n
70}
71
72fn choose(tag: &str, state: &mut State<'_>, update: &mut Update) -> bool {
73    // Required or optional is for the structural check to say.
74    if state.absent {
75        return true;
76    }
77    let Some(tv) = state.value.as_obj().and_then(|m| m.get(tag)) else {
78        update.err = Some(UpdateErr::Text(format!(
79            "Value \"$VALUE\" for property \"$PATH\" is not an object with a \"{}\" property.",
80            tag
81        )));
82        update.why = Some("Discriminated".to_string());
83        return false;
84    };
85    let disc = state.node.disc.as_ref();
86    let found = match (tv, disc) {
87        (Value::Str(name), Some(d)) => d.tags.iter().position(|t| t == name),
88        _ => None,
89    };
90    let Some(i) = found else {
91        let names = disc.map(|d| d.tags.join(", ")).unwrap_or_default();
92        update.err = Some(UpdateErr::Text(format!(
93            "Value \"$VALUE\" for property \"$PATH\" has unknown \"{}\" {}, expected one of: {}.",
94            tag,
95            json_render(tv),
96            names
97        )));
98        update.why = Some("Discriminated".to_string());
99        return false;
100    };
101    let bn = &state.node.list[i];
102    let mut val = state.value.clone();
103    let mut sub = ValidationError::default();
104    let mut w = Walk {
105        ctx: state.ctx,
106        defs: state.defs,
107        is_match: state.is_match,
108        path: state.path_arr.to_vec(),
109        paths: true,
110    };
111    validate_node(
112        bn,
113        Cur::Mut(&mut val),
114        state.key,
115        state.parent_is_array,
116        &mut w,
117        &mut sub,
118    );
119    if sub.has_any() {
120        update.err = Some(UpdateErr::Fields(sub.issues));
121        return false;
122    }
123    update.val = Some(val);
124    true
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::builders::optional;
131    use crate::node::Token;
132    use crate::spec::obj;
133    use crate::stringify::stringify_node;
134    use crate::Schema;
135
136    fn j(s: &str) -> Value {
137        Value::from(serde_json::from_str::<serde_json::Value>(s).unwrap())
138    }
139
140    fn run(s: &Schema, input: &str) -> String {
141        match s.validate(j(input)) {
142            Ok(v) => serde_json::Value::from(v).to_string(),
143            Err(e) => format!("ERR {}", e),
144        }
145    }
146
147    fn pet() -> Node {
148        discriminated(
149            "kind",
150            [
151                ("fish", obj([("fins", Spec::from(Token::Number))])),
152                ("dog", obj([("bark", Spec::from(Token::Boolean))])),
153            ],
154        )
155    }
156
157    #[test]
158    fn chooses_the_branch_by_tag() {
159        let s = Schema::new(obj([("p", Spec::from(pet()))]));
160        assert_eq!(
161            run(&s, r#"{"p":{"bark":true,"kind":"dog"}}"#),
162            r#"{"p":{"bark":true,"kind":"dog"}}"#
163        );
164        assert_eq!(
165            run(&s, r#"{"p":{"fins":"x","kind":"fish"}}"#),
166            "ERR Validation failed for property \"p.fins\" with string \"x\" because the string is not of type number."
167        );
168        assert_eq!(
169            run(&s, r#"{"p":{"kind":"dog"}}"#),
170            "ERR Validation failed for property \"p.bark\" because the property is missing."
171        );
172        assert_eq!(
173            run(&s, r#"{"p":{"bark":true}}"#),
174            "ERR Value \"{bark:true}\" for property \"p\" is not an object with a \"kind\" property."
175        );
176        assert_eq!(
177            run(&s, r#"{"p":{"kind":"cat"}}"#),
178            "ERR Value \"{kind:cat}\" for property \"p\" has unknown \"kind\" \"cat\", expected one of: dog, fish."
179        );
180        assert_eq!(
181            run(&s, r#"{"p":{"kind":1}}"#),
182            "ERR Value \"{kind:1}\" for property \"p\" has unknown \"kind\" 1, expected one of: dog, fish."
183        );
184        assert_eq!(
185            run(&s, r#"{"p":1}"#),
186            "ERR Value \"1\" for property \"p\" is not an object with a \"kind\" property."
187        );
188        assert_eq!(
189            run(&s, "{}"),
190            "ERR Validation failed for property \"p\" because the property is missing."
191        );
192        assert_eq!(
193            run(
194                &Schema::new(obj([("p", Spec::from(optional(pet())))])),
195                "{}"
196            ),
197            "{}"
198        );
199        assert!(s.valid(&j(r#"{"p":{"bark":true,"kind":"dog"}}"#)));
200        assert!(!s.valid(&j(r#"{"p":{"kind":"cat"}}"#)));
201        assert_eq!(
202            s.error(&j(r#"{"p":{"kind":"cat"}}"#))[0].why,
203            "Discriminated"
204        );
205        assert_eq!(
206            stringify_node(&pet(), false),
207            "Discriminated(kind,dog,fish)"
208        );
209        // A branch that is not an object carries no tag of its own.
210        let s = Schema::new(discriminated("k", [("n", Spec::from(Token::Number))]));
211        assert_eq!(
212            run(&s, r#"{"k":"n"}"#),
213            "ERR Validation failed for object \"{k:n}\" because the object is not of type number."
214        );
215        assert_eq!(
216            run(&Schema::new(discriminated("", [("a", Spec::from(1))])), "1"),
217            "ERR Discriminated needs a tag property name and at least one branch"
218        );
219        assert_eq!(
220            run(
221                &Schema::new(discriminated::<[(&str, Spec); 0], &str, Spec>("k", [])),
222                "1"
223            ),
224            "ERR Discriminated needs a tag property name and at least one branch"
225        );
226        // A branch that declares the tag keeps its own declaration.
227        let s = Schema::new(discriminated(
228            "k",
229            [("a", obj([("k", Spec::from(Token::String))]))],
230        ));
231        assert_eq!(run(&s, r#"{"k":"a"}"#), r#"{"k":"a"}"#);
232    }
233}