1use std::collections::HashSet;
2
3use crate::json::JsValue;
4use crate::schema::*;
5
6use super::Coercion;
7use super::CoercionError;
8
9type CoercionResult = Result<Coercion, CoercionError>;
10
11impl SchemaNode {
12 pub fn coerce(&self, target: &SchemaNode) -> CoercionResult {
13 match (self, target) {
14 (SchemaNode::ValidNode(ref source), SchemaNode::ValidNode(ref target)) => {
15 coerce_valid_nodes(source, target)
16 }
17
18 (source, target) => Err(CoercionError::IncompatibleSchemas {
19 source: source.clone(),
20 target: target.clone(),
21 }),
22 }
23 }
24}
25
26fn coerce_valid_nodes(source: &ValidNode, target: &ValidNode) -> CoercionResult {
27 match (source, target) {
28 (source, ValidNode::AnyNode(any_node)) => coerce_into_any_node(source, any_node),
29
30 (source, ValidNode::NullNode(null_node)) => coerce_into_null_node(source, null_node),
31
32 (source, ValidNode::BooleanNode(bool_node)) => coerce_into_bool_node(source, bool_node),
33
34 (source, ValidNode::IntegerNode(integer_node)) => {
35 coerce_into_integer_node(source, integer_node)
36 }
37
38 (source, ValidNode::NumberNode(number_node)) => {
39 coerce_into_number_node(source, number_node)
40 }
41
42 (source, ValidNode::StringNode(string_node)) => {
43 coerce_into_string_node(source, string_node)
44 }
45
46 (source, ValidNode::ArrayNode(array_node)) => coerce_into_array_node(source, array_node),
47
48 (source, ValidNode::ObjectNode(object_node)) => {
49 coerce_into_object_node(source, object_node)
50 }
51 }
52}
53
54fn coerce_into_object_node(source: &ValidNode, object_node: &ObjectNode) -> CoercionResult {
55 match *source {
56 ValidNode::ObjectNode(ObjectNode {
57 properties: ref source_properties,
58 required: ref source_required,
59 ..
60 }) => {
61 let &ObjectNode {
62 properties: ref target_properties,
63 required: ref target_required,
64 ..
65 } = object_node;
66
67 let props_missing = target_required - source_required;
68 if !props_missing.is_empty() {
69 Err(CoercionError::ObjectFieldsMissing(props_missing))?;
70 }
71
72 let mut source_prop_names: HashSet<&String> = source_properties.keys().collect();
73 let mut prop_coercions: Vec<(String, Coercion)> = Vec::new();
74
75 for (target_prop_name, target_prop_schema) in target_properties {
76 if let Some(source_prop_schema) = source_properties.get(target_prop_name) {
77 let prop_coercion = source_prop_schema.coerce(target_prop_schema)?;
78 let pair = (target_prop_name.to_owned(), prop_coercion);
79 prop_coercions.push(pair);
80 source_prop_names.remove(target_prop_name);
81 }
82 }
83
84 if prop_coercions.is_empty() {
85 ok_identity()
86 } else {
87 ok_object(prop_coercions)
88 }
89 }
90
91 ref source => err_incompatible(source, object_node),
92 }
93}
94
95fn coerce_into_array_node(source: &ValidNode, array_node: &ArrayNode) -> CoercionResult {
96 let &ArrayNode {
97 items: ref target_items,
98 ..
99 } = array_node;
100
101 match *source {
102 ValidNode::ArrayNode(ref source_array_node) => {
103 let &ArrayNode {
104 items: ref source_items,
105 ..
106 } = source_array_node;
107
108 match source_items.coerce(target_items)? {
109 Coercion::Identity => ok_identity(),
110
111 coercion => ok_array(coercion),
112 }
113 }
114 ref source => err_incompatible(source, array_node),
115 }
116}
117
118fn coerce_into_string_node(source: &ValidNode, string_node: &StringNode) -> CoercionResult {
119 match *source {
120 ValidNode::StringNode(_) => ok_identity(),
121 ValidNode::NumberNode(_) => ok_number_to_string(),
122 ValidNode::IntegerNode(_) => ok_number_to_string(),
123 ref source => err_incompatible(source, string_node),
124 }
125}
126
127fn coerce_into_number_node(source: &ValidNode, number_node: &NumberNode) -> CoercionResult {
128 match *source {
129 ValidNode::NumberNode(_) => ok_identity(),
130 ValidNode::IntegerNode(_) => ok_identity(),
131 ref source => err_incompatible(source, number_node),
132 }
133}
134
135fn coerce_into_integer_node(source: &ValidNode, integer_node: &IntegerNode) -> CoercionResult {
136 match *source {
137 ValidNode::IntegerNode(_) => ok_identity(), ref source => err_incompatible(source, integer_node),
139 }
140}
141
142fn coerce_into_bool_node(source: &ValidNode, bool_node: &BooleanNode) -> CoercionResult {
143 match *source {
144 ValidNode::BooleanNode(_) => ok_identity(),
145 ref source => err_incompatible(source, bool_node),
146 }
147}
148
149fn coerce_into_null_node(source: &ValidNode, _target: &NullNode) -> CoercionResult {
150 match *source {
151 ValidNode::NullNode(_) => ok_identity(),
152 _ => ok_replace_with_literal(JsValue::Null),
153 }
154}
155
156fn coerce_into_any_node(_source: &ValidNode, _target: &AnyNode) -> CoercionResult {
157 ok_identity()
158}
159
160fn ok_identity() -> CoercionResult {
161 Ok(Coercion::Identity)
162}
163
164fn ok_array(items_coercion: Coercion) -> CoercionResult {
165 Ok(Coercion::Array(Box::new(items_coercion)))
166}
167
168fn ok_replace_with_literal(literal_value: JsValue) -> CoercionResult {
169 Ok(Coercion::ReplaceWithLiteral(literal_value))
170}
171
172fn ok_number_to_string() -> CoercionResult {
173 Ok(Coercion::NumberToString)
174}
175
176fn ok_object<
177 I: Iterator<Item = (String, Coercion)>,
178 II: IntoIterator<Item = (String, Coercion), IntoIter = I>,
179>(
180 ii: II,
181) -> CoercionResult {
182 Ok(Coercion::Object(ii.into_iter().collect()))
183}
184
185fn err_incompatible<Source, Target>(source: &Source, target: &Target) -> CoercionResult
186where
187 Source: Clone + Into<SchemaNode>,
188 Target: Clone + Into<SchemaNode>,
189{
190 Err(CoercionError::IncompatibleSchemas {
191 source: source.clone().into(),
192 target: target.clone().into(),
193 })
194}
195
196#[test]
197fn an_object_with_properties_coercion_omits_unmentioned_fields() {
198 let source_schema: SchemaNode = SchemaNode::object()
199 .add_property("a-field", SchemaNode::string())
200 .add_property("wont-be-seen", SchemaNode::string())
201 .into();
202 let target_schema: SchemaNode = SchemaNode::object().add_property("a-field", SchemaNode::string()).into();
203 let coercion = source_schema.coerce(&target_schema).expect("coercion creation failure");
204
205 let input = json!({
206 "a-field": "a-value",
207 "wont-be-seen": "in this field icouldpleasureahorse (c)JClarkson",
208 });
209 let expected_output = json!({
210 "a-field": "a-value",
211 });
212
213 let actual_output = coercion.coerce(input).expect("coercion application failure");
214
215 assert_eq!(actual_output, expected_output);
216}
217
218#[test]
219fn an_object_with_no_properties_coercion_keeps_all_the_fields() {
220 let source_schema: SchemaNode = SchemaNode::object().into();
221 let target_schema: SchemaNode = SchemaNode::object()
222 .add_property("a-field", SchemaNode::string())
223 .add_property("will-be-seen", SchemaNode::string())
224 .into();
225 let coercion = source_schema.coerce(&target_schema).expect("coercion creation failure");
226
227 let input = json!({
228 "a-field": "a-value",
229 "will-be-seen": "ahem",
230 });
231 let expected_output = input.clone();
232 let actual_output = coercion.coerce(input).expect("coercion application failure");
233
234 assert_eq!(actual_output, expected_output);
235}
236
237#[test]
238fn basic_coercions() {
239 let inputs = basic_inputs();
240
241 for (source, target, coercion_opt) in inputs {
242 eprintln!("trying {:?} into {:?}", source, target);
243 assert_eq!(source.coerce(&target).ok(), coercion_opt);
244 }
245}
246
247#[test]
248fn array_coercions() {
249 let inputs: Vec<(SchemaNode, SchemaNode, Option<Coercion>)> = basic_inputs()
250 .into_iter()
251 .map(|(source, target, coercion_opt)| {
252 (
253 SchemaNode::array(source).into(),
254 SchemaNode::array(target).into(),
255 coercion_opt,
256 )
257 })
258 .collect();
259
260 for (source, target, coercion_opt) in inputs {
261 let coercion_opt = coercion_opt.map(|coercion| match coercion {
262 Coercion::Identity => Coercion::Identity,
263 other => Coercion::Array(Box::new(other)),
264 });
265
266 eprintln!("trying {:?} into {:?}", source, target);
267 assert_eq!(source.coerce(&target).ok(), coercion_opt);
268 }
269}
270
271#[test]
272fn object_failing_coercion() {
273 let inputs: Vec<(SchemaNode, SchemaNode)> = vec![
274 (SchemaNode::any().into(), SchemaNode::object().into()),
275 (SchemaNode::null().into(), SchemaNode::object().into()),
276 (SchemaNode::boolean().into(), SchemaNode::object().into()),
277 (SchemaNode::integer().into(), SchemaNode::object().into()),
278 (SchemaNode::number().into(), SchemaNode::object().into()),
279 (SchemaNode::string().into(), SchemaNode::object().into()),
280 (
281 SchemaNode::array(SchemaNode::string()).into(),
282 SchemaNode::object().into(),
283 ),
284 (
285 SchemaNode::object().into(),
286 SchemaNode::object()
287 .add_property("a_field", SchemaNode::any())
288 .add_required("a_field")
289 .into(),
290 ),
291 (
292 SchemaNode::object()
293 .add_property("a_field", SchemaNode::any())
294 .into(),
295 SchemaNode::object()
296 .add_property("a_field", SchemaNode::any())
297 .add_required("a_field")
298 .into(),
299 ),
300 ];
301
302 for (source, target) in inputs {
303 eprintln!("coercing {:?} to {:?}", source, target);
304 assert!(source.coerce(&target).is_err())
305 }
306}
307
308#[test]
309fn object_non_trivial_coercion() {
310 let inputs: Vec<(SchemaNode, SchemaNode)> = vec![
311 (
312 SchemaNode::object()
313 .add_property("a_bool", SchemaNode::boolean())
314 .into(),
315 SchemaNode::object().into(),
316 ),
317 (
318 SchemaNode::object()
319 .add_property("a_bool", SchemaNode::boolean())
320 .into(),
321 SchemaNode::object()
322 .add_property("a_bool", SchemaNode::boolean())
323 .into(),
324 ),
325 (
326 SchemaNode::object()
327 .add_property("a_bool", SchemaNode::boolean())
328 .add_required("a_bool")
329 .into(),
330 SchemaNode::object()
331 .add_property("a_bool", SchemaNode::boolean())
332 .add_required("a_bool")
333 .into(),
334 ),
335 (
336 SchemaNode::object()
337 .add_property("to_string", SchemaNode::integer())
338 .add_required("to_string")
339 .into(),
340 SchemaNode::object()
341 .add_property("to_string", SchemaNode::string())
342 .add_required("to_string")
343 .into(),
344 ),
345 ];
346
347 for (source, target) in inputs {
348 assert!(source.coerce(&target).is_ok())
349 }
350}
351
352#[cfg(test)]
353fn basic_inputs() -> Vec<(SchemaNode, SchemaNode, Option<Coercion>)> {
354 vec![
355 (
356 SchemaNode::null().into(),
357 SchemaNode::null().into(),
358 Some(Coercion::Identity),
359 ),
360 (
361 SchemaNode::any().into(),
362 SchemaNode::any().into(),
363 Some(Coercion::Identity),
364 ),
365 (
366 SchemaNode::boolean().into(),
367 SchemaNode::boolean().into(),
368 Some(Coercion::Identity),
369 ),
370 (
371 SchemaNode::integer().into(),
372 SchemaNode::integer().into(),
373 Some(Coercion::Identity),
374 ),
375 (
376 SchemaNode::number().into(),
377 SchemaNode::number().into(),
378 Some(Coercion::Identity),
379 ),
380 (
381 SchemaNode::string().into(),
382 SchemaNode::string().into(),
383 Some(Coercion::Identity),
384 ),
385 (
386 SchemaNode::integer().into(),
387 SchemaNode::null().into(),
388 Some(Coercion::ReplaceWithLiteral(JsValue::Null)),
389 ),
390 (
391 SchemaNode::integer().into(),
392 SchemaNode::number().into(),
393 Some(Coercion::Identity),
394 ),
395 (
396 SchemaNode::number().into(),
397 SchemaNode::integer().into(),
398 None,
399 ),
400 ]
401}