yaml-schema 0.9.1

A YAML schema validator
Documentation
use log::debug;

use saphyr::AnnotatedSequence;
use saphyr::MarkedYaml;
use saphyr::YamlData;

use crate::ConstValue;
use crate::Context;
use crate::Result;
use crate::Validator;
use crate::utils::format_vec;
use crate::utils::format_yaml_data;

/// An enum schema represents a set of constant values
#[derive(Debug, Default, PartialEq)]
pub struct EnumSchema {
    pub r#enum: Vec<ConstValue>,
}

impl std::fmt::Display for EnumSchema {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Enum {{ enum: {} }}", format_vec(&self.r#enum))
    }
}

impl TryFrom<&MarkedYaml<'_>> for EnumSchema {
    type Error = crate::Error;

    fn try_from(value: &MarkedYaml<'_>) -> crate::Result<Self> {
        if let YamlData::Sequence(values) = &value.data {
            let enum_values = load_enum_values(values)?;
            Ok(EnumSchema {
                r#enum: enum_values,
            })
        } else {
            Err(generic_error!(
                "enum: Expected a sequence, but got: {}",
                format_yaml_data(&value.data)
            ))
        }
    }
}

pub fn load_enum_values(values: &AnnotatedSequence<MarkedYaml>) -> Result<Vec<ConstValue>> {
    values.iter().map(|v| v.try_into()).collect()
}

impl Validator for EnumSchema {
    fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
        debug!("[EnumSchema] self: {self}");
        let data = &value.data;
        debug!("[EnumSchema] Validating value: {data:?}");
        let const_value: ConstValue = match ConstValue::try_from(data) {
            Ok(const_value) => const_value,
            Err(_) => {
                context.add_error(
                    value,
                    format!(
                        "Unable to convert value: {} to ConstValue",
                        format_yaml_data(data)
                    ),
                );
                return Ok(());
            }
        };
        debug!("[EnumSchema] const_value: {const_value}");
        for value in &self.r#enum {
            debug!("[EnumSchema] value: {value}");
            if value.eq(&const_value) {
                return Ok(());
            }
        }
        if !self.r#enum.contains(&const_value) {
            let value_str = format_yaml_data(data);
            let enum_values = self
                .r#enum
                .iter()
                .map(|v| format!("{v}"))
                .collect::<Vec<String>>()
                .join(", ");
            let error = format!("Value {value_str} is not in the enum: [{enum_values}]");
            debug!("[EnumSchema] error: {error}");
            context.add_error(value, error);
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use crate::loader;

    use super::*;
    use saphyr::LoadableYamlNode;

    #[test]
    fn test_enum_schema() {
        let schema = EnumSchema {
            r#enum: vec![ConstValue::String("NW".to_string())],
        };
        let docs = saphyr::MarkedYaml::load_from_str("NW").unwrap();
        let value = docs.first().unwrap();
        let context = Context::default();
        let result = schema.validate(&context, value);
        assert!(result.is_ok());
    }

    #[test]
    fn test_loading_enum_schema() {
        let schema = r#"
        enum:
          - red
          - amber
          - green
        "#;
        let schema = loader::load_from_str(schema).expect("Failed to load schema");

        let docs = MarkedYaml::load_from_str(
            r#"
        red
        "#,
        )
        .unwrap();
        let value = docs.first().unwrap();
        let context = Context::default();
        let result = schema.validate(&context, value);
        assert!(result.is_ok());
        assert!(!context.has_errors());

        let docs = MarkedYaml::load_from_str(
            r#"
        blue
        "#,
        )
        .unwrap();
        let value = docs.first().unwrap();
        let context = Context::default();
        let result = schema.validate(&context, value);
        assert!(result.is_ok());
        assert!(context.has_errors());
        let errors = context.errors.borrow();
        assert_eq!(errors.len(), 1);
        assert_eq!(
            errors[0].error,
            "Value \"blue\" is not in the enum: [\"red\", \"amber\", \"green\"]"
        );
    }
}