json-utils 0.3.5

An utility crate for working with JSON and JSON-schemas
Documentation

use crate::json::JsValue;

use super::CoercionError;

type CoercionResult = Result<JsValue, CoercionError>;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Coercion {
    #[serde(rename = "identity")]
    Identity,

    #[serde(rename = "replace_with_literal")]
    ReplaceWithLiteral(JsValue),

    #[serde(rename = "number_to_string")]
    NumberToString,

    #[serde(rename = "array")]
    Array(Box<Coercion>),

    #[serde(rename = "object")]
    Object(Vec<(String, Coercion)>),
}

impl Coercion {
    pub fn coerce(self, value: JsValue) -> CoercionResult {
        match self {
            Coercion::Identity => ok_value(value),
            Coercion::ReplaceWithLiteral(literal_value) => ok_value(literal_value),
            Coercion::NumberToString => number_to_string(self, value),
            Coercion::Array(item_coercion) => array_coercion(item_coercion, value),
            Coercion::Object(prop_coercions) => object_coercion(prop_coercions, value),
        }
    }

    pub fn is_subtyping_only(&self) -> bool {
        match *self {
            Coercion::Identity => true,
            Coercion::NumberToString => false,
            Coercion::ReplaceWithLiteral(_) => false,
            Coercion::Array(ref items) => items.is_subtyping_only(),
            Coercion::Object(ref props) => props.iter().all(|(_, prop)| prop.is_subtyping_only()),
        }
    }
}

fn number_to_string(coercion: Coercion, value: JsValue) -> CoercionResult {
    if let JsValue::Number(js_number) = value {
        js_number
            .as_f64()
            .map(|f| format!("{}", f))
            .map(JsValue::String)
            .ok_or(CoercionError::JsNumberError)
    } else {
        err_unexpectd_input(coercion, value)
    }
}

fn array_coercion(item_coercion: Box<Coercion>, value: JsValue) -> CoercionResult {
    if let JsValue::Array(items) = value {
        let item_coercion = *item_coercion;
        let items = items
            .into_iter()
            .map(move |item| item_coercion.clone().coerce(item))
            .collect::<Result<Vec<_>, _>>()?;
        ok_value(JsValue::Array(items))
    } else {
        err_unexpectd_input(Coercion::Array(item_coercion), value)
    }
}

fn object_coercion(prop_coercions: Vec<(String, Coercion)>, value: JsValue) -> CoercionResult {
    if let JsValue::Object(mut input_props) = value {
        let props_count = prop_coercions.len();
        let mut output_props = Vec::<(String, JsValue)>::with_capacity(props_count);

        for (prop_name, prop_coercion) in prop_coercions {
            let input_prop_value =
                input_props
                    .remove(&prop_name)
                    .ok_or(CoercionError::ObjectFieldsMissing(hashset![
                        prop_name.clone()
                    ]))?;
            let output_prop_value = prop_coercion.coerce(input_prop_value)?;

            output_props.push((prop_name, output_prop_value));
        }

        ok_value(JsValue::Object(output_props.into_iter().collect()))
    } else {
        err_unexpectd_input(Coercion::Object(prop_coercions), value)
    }
}

fn ok_value(js_value: JsValue) -> CoercionResult {
    Ok(js_value)
}

fn err_unexpectd_input(coercion: Coercion, input: JsValue) -> CoercionResult {
    Err(CoercionError::UnexpectedInput { coercion, input })
}