json-utils 0.3.5

An utility crate for working with JSON and JSON-schemas
Documentation
use std::collections::HashSet;

use crate::json::JsValue;
use crate::schema::*;

use super::Coercion;
use super::CoercionError;

type CoercionResult = Result<Coercion, CoercionError>;

impl SchemaNode {
    pub fn coerce(&self, target: &SchemaNode) -> CoercionResult {
        match (self, target) {
            (SchemaNode::ValidNode(ref source), SchemaNode::ValidNode(ref target)) => {
                coerce_valid_nodes(source, target)
            }

            (source, target) => Err(CoercionError::IncompatibleSchemas {
                source: source.clone(),
                target: target.clone(),
            }),
        }
    }
}

fn coerce_valid_nodes(source: &ValidNode, target: &ValidNode) -> CoercionResult {
    match (source, target) {
        (source, ValidNode::AnyNode(any_node)) => coerce_into_any_node(source, any_node),

        (source, ValidNode::NullNode(null_node)) => coerce_into_null_node(source, null_node),

        (source, ValidNode::BooleanNode(bool_node)) => coerce_into_bool_node(source, bool_node),

        (source, ValidNode::IntegerNode(integer_node)) => {
            coerce_into_integer_node(source, integer_node)
        }

        (source, ValidNode::NumberNode(number_node)) => {
            coerce_into_number_node(source, number_node)
        }

        (source, ValidNode::StringNode(string_node)) => {
            coerce_into_string_node(source, string_node)
        }

        (source, ValidNode::ArrayNode(array_node)) => coerce_into_array_node(source, array_node),

        (source, ValidNode::ObjectNode(object_node)) => {
            coerce_into_object_node(source, object_node)
        }
    }
}

fn coerce_into_object_node(source: &ValidNode, object_node: &ObjectNode) -> CoercionResult {
    match *source {
        ValidNode::ObjectNode(ObjectNode {
            properties: ref source_properties,
            required: ref source_required,
            ..
        }) => {
            let &ObjectNode {
                properties: ref target_properties,
                required: ref target_required,
                ..
            } = object_node;

            let props_missing = target_required - source_required;
            if !props_missing.is_empty() {
                Err(CoercionError::ObjectFieldsMissing(props_missing))?;
            }

            let mut source_prop_names: HashSet<&String> = source_properties.keys().collect();
            let mut prop_coercions: Vec<(String, Coercion)> = Vec::new();

            for (target_prop_name, target_prop_schema) in target_properties {
                if let Some(source_prop_schema) = source_properties.get(target_prop_name) {
                    let prop_coercion = source_prop_schema.coerce(target_prop_schema)?;
                    let pair = (target_prop_name.to_owned(), prop_coercion);
                    prop_coercions.push(pair);
                    source_prop_names.remove(target_prop_name);
                }
            }
            
            if prop_coercions.is_empty() {
                ok_identity()
            } else {
                ok_object(prop_coercions)
            }
        }

        ref source => err_incompatible(source, object_node),
    }
}

fn coerce_into_array_node(source: &ValidNode, array_node: &ArrayNode) -> CoercionResult {
    let &ArrayNode {
        items: ref target_items,
        ..
    } = array_node;

    match *source {
        ValidNode::ArrayNode(ref source_array_node) => {
            let &ArrayNode {
                items: ref source_items,
                ..
            } = source_array_node;

            match source_items.coerce(target_items)? {
                Coercion::Identity => ok_identity(),

                coercion => ok_array(coercion),
            }
        }
        ref source => err_incompatible(source, array_node),
    }
}

fn coerce_into_string_node(source: &ValidNode, string_node: &StringNode) -> CoercionResult {
    match *source {
        ValidNode::StringNode(_) => ok_identity(),
        ValidNode::NumberNode(_) => ok_number_to_string(),
        ValidNode::IntegerNode(_) => ok_number_to_string(),
        ref source => err_incompatible(source, string_node),
    }
}

fn coerce_into_number_node(source: &ValidNode, number_node: &NumberNode) -> CoercionResult {
    match *source {
        ValidNode::NumberNode(_) => ok_identity(),
        ValidNode::IntegerNode(_) => ok_identity(),
        ref source => err_incompatible(source, number_node),
    }
}

fn coerce_into_integer_node(source: &ValidNode, integer_node: &IntegerNode) -> CoercionResult {
    match *source {
        ValidNode::IntegerNode(_) => ok_identity(), // TODO: check ranges
        ref source => err_incompatible(source, integer_node),
    }
}

fn coerce_into_bool_node(source: &ValidNode, bool_node: &BooleanNode) -> CoercionResult {
    match *source {
        ValidNode::BooleanNode(_) => ok_identity(),
        ref source => err_incompatible(source, bool_node),
    }
}

fn coerce_into_null_node(source: &ValidNode, _target: &NullNode) -> CoercionResult {
    match *source {
        ValidNode::NullNode(_) => ok_identity(),
        _ => ok_replace_with_literal(JsValue::Null),
    }
}

fn coerce_into_any_node(_source: &ValidNode, _target: &AnyNode) -> CoercionResult {
    ok_identity()
}

fn ok_identity() -> CoercionResult {
    Ok(Coercion::Identity)
}

fn ok_array(items_coercion: Coercion) -> CoercionResult {
    Ok(Coercion::Array(Box::new(items_coercion)))
}

fn ok_replace_with_literal(literal_value: JsValue) -> CoercionResult {
    Ok(Coercion::ReplaceWithLiteral(literal_value))
}

fn ok_number_to_string() -> CoercionResult {
    Ok(Coercion::NumberToString)
}

fn ok_object<
    I: Iterator<Item = (String, Coercion)>,
    II: IntoIterator<Item = (String, Coercion), IntoIter = I>,
>(
    ii: II,
) -> CoercionResult {
    Ok(Coercion::Object(ii.into_iter().collect()))
}

fn err_incompatible<Source, Target>(source: &Source, target: &Target) -> CoercionResult
where
    Source: Clone + Into<SchemaNode>,
    Target: Clone + Into<SchemaNode>,
{
    Err(CoercionError::IncompatibleSchemas {
        source: source.clone().into(),
        target: target.clone().into(),
    })
}

#[test]
fn an_object_with_properties_coercion_omits_unmentioned_fields() {
    let source_schema: SchemaNode = SchemaNode::object()
        .add_property("a-field", SchemaNode::string())
        .add_property("wont-be-seen", SchemaNode::string())
        .into();
    let target_schema: SchemaNode = SchemaNode::object().add_property("a-field", SchemaNode::string()).into();
    let coercion = source_schema.coerce(&target_schema).expect("coercion creation failure");

    let input = json!({
        "a-field": "a-value",
        "wont-be-seen": "in this field icouldpleasureahorse (c)JClarkson",
    });
    let expected_output = json!({
        "a-field": "a-value",
    });

    let actual_output = coercion.coerce(input).expect("coercion application failure");
    
    assert_eq!(actual_output, expected_output);
}

#[test]
fn an_object_with_no_properties_coercion_keeps_all_the_fields() {
    let source_schema: SchemaNode = SchemaNode::object().into();
    let target_schema: SchemaNode = SchemaNode::object()
        .add_property("a-field", SchemaNode::string())
        .add_property("will-be-seen", SchemaNode::string())
        .into();
    let coercion = source_schema.coerce(&target_schema).expect("coercion creation failure");

    let input = json!({
        "a-field": "a-value",
        "will-be-seen": "ahem",
    });
    let expected_output = input.clone();
    let actual_output = coercion.coerce(input).expect("coercion application failure");
    
    assert_eq!(actual_output, expected_output);
}

#[test]
fn basic_coercions() {
    let inputs = basic_inputs();

    for (source, target, coercion_opt) in inputs {
        eprintln!("trying {:?} into {:?}", source, target);
        assert_eq!(source.coerce(&target).ok(), coercion_opt);
    }
}

#[test]
fn array_coercions() {
    let inputs: Vec<(SchemaNode, SchemaNode, Option<Coercion>)> = basic_inputs()
        .into_iter()
        .map(|(source, target, coercion_opt)| {
            (
                SchemaNode::array(source).into(),
                SchemaNode::array(target).into(),
                coercion_opt,
            )
        })
        .collect();

    for (source, target, coercion_opt) in inputs {
        let coercion_opt = coercion_opt.map(|coercion| match coercion {
            Coercion::Identity => Coercion::Identity,
            other => Coercion::Array(Box::new(other)),
        });

        eprintln!("trying {:?} into {:?}", source, target);
        assert_eq!(source.coerce(&target).ok(), coercion_opt);
    }
}

#[test]
fn object_failing_coercion() {
    let inputs: Vec<(SchemaNode, SchemaNode)> = vec![
        (SchemaNode::any().into(), SchemaNode::object().into()),
        (SchemaNode::null().into(), SchemaNode::object().into()),
        (SchemaNode::boolean().into(), SchemaNode::object().into()),
        (SchemaNode::integer().into(), SchemaNode::object().into()),
        (SchemaNode::number().into(), SchemaNode::object().into()),
        (SchemaNode::string().into(), SchemaNode::object().into()),
        (
            SchemaNode::array(SchemaNode::string()).into(),
            SchemaNode::object().into(),
        ),
        (
            SchemaNode::object().into(),
            SchemaNode::object()
                .add_property("a_field", SchemaNode::any())
                .add_required("a_field")
                .into(),
        ),
        (
            SchemaNode::object()
                .add_property("a_field", SchemaNode::any())
                .into(),
            SchemaNode::object()
                .add_property("a_field", SchemaNode::any())
                .add_required("a_field")
                .into(),
        ),
    ];

    for (source, target) in inputs {
        eprintln!("coercing {:?} to {:?}", source, target);
        assert!(source.coerce(&target).is_err())
    }
}

#[test]
fn object_non_trivial_coercion() {
    let inputs: Vec<(SchemaNode, SchemaNode)> = vec![
        (
            SchemaNode::object()
                .add_property("a_bool", SchemaNode::boolean())
                .into(),
            SchemaNode::object().into(),
        ),
        (
            SchemaNode::object()
                .add_property("a_bool", SchemaNode::boolean())
                .into(),
            SchemaNode::object()
                .add_property("a_bool", SchemaNode::boolean())
                .into(),
        ),
        (
            SchemaNode::object()
                .add_property("a_bool", SchemaNode::boolean())
                .add_required("a_bool")
                .into(),
            SchemaNode::object()
                .add_property("a_bool", SchemaNode::boolean())
                .add_required("a_bool")
                .into(),
        ),
        (
            SchemaNode::object()
                .add_property("to_string", SchemaNode::integer())
                .add_required("to_string")
                .into(),
            SchemaNode::object()
                .add_property("to_string", SchemaNode::string())
                .add_required("to_string")
                .into(),
        ),
    ];

    for (source, target) in inputs {
        assert!(source.coerce(&target).is_ok())
    }
}

#[cfg(test)]
fn basic_inputs() -> Vec<(SchemaNode, SchemaNode, Option<Coercion>)> {
    vec![
        (
            SchemaNode::null().into(),
            SchemaNode::null().into(),
            Some(Coercion::Identity),
        ),
        (
            SchemaNode::any().into(),
            SchemaNode::any().into(),
            Some(Coercion::Identity),
        ),
        (
            SchemaNode::boolean().into(),
            SchemaNode::boolean().into(),
            Some(Coercion::Identity),
        ),
        (
            SchemaNode::integer().into(),
            SchemaNode::integer().into(),
            Some(Coercion::Identity),
        ),
        (
            SchemaNode::number().into(),
            SchemaNode::number().into(),
            Some(Coercion::Identity),
        ),
        (
            SchemaNode::string().into(),
            SchemaNode::string().into(),
            Some(Coercion::Identity),
        ),
        (
            SchemaNode::integer().into(),
            SchemaNode::null().into(),
            Some(Coercion::ReplaceWithLiteral(JsValue::Null)),
        ),
        (
            SchemaNode::integer().into(),
            SchemaNode::number().into(),
            Some(Coercion::Identity),
        ),
        (
            SchemaNode::number().into(),
            SchemaNode::integer().into(),
            None,
        ),
    ]
}