json_utils/schema_coercion/
schema_node_impl_coerce.rs

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(), // TODO: check ranges
138        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}