1use 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
18pub struct Disc {
19 pub tag: String,
20 pub tags: Vec<String>,
21}
22
23pub 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 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 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 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}