cooplan-definition-schema-validator 0.2.2

Validate JSON values against a definition
Documentation
use std::collections::HashMap;

use cooplan_definitions_lib::{
    definition::Definition, validated_source_attribute::ValidatedSourceAttribute,
    validated_source_category::ValidatedSourceCategory,
};
use serde_json::{Map, Value};

use crate::category_chain::{build_from_definition_and_category, CategoryChain};
use crate::{
    definition_value::DefinitionValue,
    error::{Error, ErrorKind},
    validations::{validate_boolean, validate_decimal, validate_integer, validate_string},
};

const VALUE_VERSION: &str = "version";
const VALUE_TYPE: &str = "type";

type Validation = Box<dyn Fn(&Value) -> Result<(), Error> + Send>;

pub struct SchemaValidator {
    categories: HashMap<String, ValidatedSourceCategory>,
    validations: HashMap<String, Validation>,
}

impl SchemaValidator {
    fn initialize_base_validations(validations: &mut HashMap<String, Validation>) {
        validations.insert("string".to_string(), Box::new(validate_string));
        validations.insert("integer".to_string(), Box::new(validate_integer));
        validations.insert("decimal".to_string(), Box::new(validate_decimal));
        validations.insert("boolean".to_string(), Box::new(validate_boolean));
    }

    pub fn register_validation(&mut self, data_type: String, validation: Validation) {
        self.validations.insert(data_type, validation);
    }

    pub fn validate(
        &mut self,
        value: String,
        definition: Definition,
    ) -> Result<DefinitionValue, Error> {
        match serde_json::from_str(value.as_str()) {
            Ok::<Map<String, Value>, _>(mut object) => {
                let value_definition_version = match self.try_get_version(&object) {
                    Ok(version) => version,
                    Err(error) => return Err(error),
                };

                if definition.version().to_lowercase() != value_definition_version.to_lowercase() {
                    return Err(Error::new(
                        ErrorKind::ValueDefinitionMismatch,
                        format!(
                            "value's version '{}' != definition's version '{}'",
                            value_definition_version,
                            definition.version()
                        ),
                    ));
                }

                let value_type = match self.try_get_type(&object) {
                    Ok(value_type) => value_type,
                    Err(error) => return Err(error),
                };

                let attributes: Vec<ValidatedSourceAttribute> =
                    match self.try_get_attributes_from_definition(&definition, &value_type) {
                        Ok(attributes) => attributes,
                        Err(error) => return Err(error),
                    };

                let mut scoped_value: Map<String, Value> = Map::new();

                for attribute in attributes.as_slice() {
                    let attribute_value = match object.remove(&attribute.id) {
                        Some(attribute_value) => attribute_value,
                        None => {
                            return Err(Error::new(
                                ErrorKind::InvalidValue,
                                format!(
                                    "failed to find attribute id '{}' within value",
                                    attribute.id
                                ),
                            ))
                        }
                    };

                    match self.validations.get(&attribute.data_type) {
                        Some(validation) => match validation(&attribute_value) {
                            Ok(_) => {
                                scoped_value.insert(attribute.id.clone(), attribute_value);
                            }
                            Err(error) => return Err(error),
                        },
                        None => {
                            return Err(Error::new(
                                ErrorKind::ValidationNotRegistered,
                                format!(
                                    "no validation found for data type '{}'",
                                    attribute.data_type
                                ),
                            ))
                        }
                    }
                }

                let category_chain = build_from_definition_and_category(&definition, &value_type);

                let definition_value =
                    DefinitionValue::try_new(&definition, category_chain, scoped_value)?;

                Ok(definition_value)
            }
            Err(error) => Err(Error::new(
                ErrorKind::DeserializationFailure,
                format!("failed to deserialize value: {}", error),
            )),
        }
    }

    fn try_get_version(&self, object: &Map<String, Value>) -> Result<String, Error> {
        match object.get(VALUE_VERSION) {
            Some(version) => match version.as_str() {
                Some(version_string) => Ok(version_string.to_string()),
                None => Err(Error::new(
                    ErrorKind::InvalidValue,
                    "version attribute's value cannot be read as a string".to_string(),
                )),
            },
            None => Err(Error::new(
                ErrorKind::VersionAttributeMissing,
                "version attribute is missing".to_string(),
            )),
        }
    }

    fn try_get_type(&self, object: &Map<String, Value>) -> Result<String, Error> {
        match object.get(VALUE_TYPE) {
            Some(value_type) => match value_type.as_str() {
                Some(type_string) => Ok(type_string.to_string()),
                None => Err(Error::new(
                    ErrorKind::InvalidValue,
                    "type attribute's value cannot be read as a string".to_string(),
                )),
            },
            None => Err(Error::new(
                ErrorKind::TypeAttributeMissing,
                "type attribute is missing".to_string(),
            )),
        }
    }

    fn try_get_attributes_from_definition(
        &mut self,
        definition: &Definition,
        category_id: &String,
    ) -> Result<Vec<ValidatedSourceAttribute>, Error> {
        self.categories.clear();

        for category in definition.categories() {
            self.categories.insert(category.id.clone(), category);
        }

        let mut attributes: Vec<ValidatedSourceAttribute> = Vec::new();

        match self.add_attributes_from_category(&mut attributes, category_id) {
            Ok(_) => Ok(attributes),
            Err(error) => Err(error),
        }
    }

    fn add_attributes_from_category(
        &self,
        attributes: &mut Vec<ValidatedSourceAttribute>,
        category_id: &String,
    ) -> Result<(), Error> {
        let category = match self.categories.get(category_id) {
            Some(category) => category,
            None => {
                return Err(Error::new(
                    ErrorKind::InvalidValue,
                    format!("cannot found category id '{}'", category_id),
                ))
            }
        };

        for attribute in category.attributes.as_slice() {
            attributes.push(attribute.clone());
        }

        match &category.parent {
            Some(parent_category_id) => {
                self.add_attributes_from_category(attributes, &parent_category_id)
            }
            None => Ok(()),
        }
    }
}

impl Default for SchemaValidator {
    fn default() -> Self {
        let mut validations: HashMap<String, Validation> = HashMap::new();

        SchemaValidator::initialize_base_validations(&mut validations);

        SchemaValidator {
            categories: HashMap::new(),
            validations,
        }
    }
}