1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
use serde_json::{self, Value};

use crate::schema::{PrimitiveType, Schema};

pub use error::ValidationError;
use scope::ScopedSchema;
pub use state::ValidationState;

mod error;
mod path;
mod scope;
mod state;
mod types;

pub trait Validator {
    fn validate(&self, data: Option<&Value>) -> ValidationState;
}

macro_rules! bail_if_invalid {
    ($state:expr) => {{
        let state = $state;
        if !state.is_valid() {
            return state;
        }
    }};
}

fn validate_optional(scope: &ScopedSchema, data: Option<&Value>) -> ValidationState {
    let value_exists = match data {
        Some(Value::Null) => false,
        None => false,
        _ => true,
    };

    if !value_exists && scope.schema().r#type().is_required() {
        return scope
            .error(
                "type",
                format!("'{}' is not an optional type", scope.schema().r#type().to_string()),
            )
            .into();
    }

    ValidationState::new()
}

fn validate_const(scope: &ScopedSchema, data: &Value) -> ValidationState {
    match scope.schema().r#const() {
        Some(constant) if constant != data => scope.error("const", "value does not match").into(),
        _ => ValidationState::new(),
    }
}

fn validate_enum(scope: &ScopedSchema, data: &Value) -> ValidationState {
    let enum_entries = scope.schema().r#enum();

    if enum_entries.is_empty() {
        return ValidationState::new();
    }

    let valid_count = enum_entries
        .iter()
        .fold(0, |acc, item| acc + (item.value() == data) as usize);

    match valid_count {
        1 => ValidationState::new(),
        0 => scope.error("enum", "does not match any value").into(),
        _ => scope.error("enum", "matches multiple values").into(),
    }
}

impl<'a> Validator for ScopedSchema<'a> {
    fn validate(&self, data: Option<&Value>) -> ValidationState {
        bail_if_invalid!(validate_optional(self, data));

        let data = match data {
            Some(x) => x,
            None => return ValidationState::new(),
        };

        bail_if_invalid!(validate_const(self, data));
        bail_if_invalid!(validate_enum(self, data));

        match self.schema().r#type().primitive_type() {
            PrimitiveType::String => types::validate_as_string(self, data),
            PrimitiveType::Array => types::validate_as_array(self, data),
            PrimitiveType::Boolean => types::validate_as_boolean(self, data),
            PrimitiveType::Integer => types::validate_as_integer(self, data),
            PrimitiveType::Number => types::validate_as_number(self, data),
            PrimitiveType::Object => types::validate_as_object(self, data),
            PrimitiveType::Port => types::validate_as_port(self, data),
            PrimitiveType::Text => types::validate_as_text(self, data),
            PrimitiveType::Password => types::validate_as_password(self, data),
            PrimitiveType::Hostname => types::validate_as_hostname(self, data),
            PrimitiveType::Date => types::validate_as_date(self, data),
            PrimitiveType::DateTime => types::validate_as_datetime(self, data),
            PrimitiveType::Time => types::validate_as_time(self, data),
            PrimitiveType::Email => types::validate_as_email(self, data),
            PrimitiveType::IPv4 => types::validate_as_ipv4(self, data),
            PrimitiveType::IPv6 => types::validate_as_ipv6(self, data),
            PrimitiveType::Uri => types::validate_as_uri(self, data),
            PrimitiveType::File => types::validate_as_file(self, data),
            PrimitiveType::ChronyAddress => types::validate_as_chrony_address(self, data),
            PrimitiveType::DNSMasqAddress => types::validate_as_dnsmasq_address(self, data),
            PrimitiveType::IPTablesAddress => types::validate_as_iptables_address(self, data),
            PrimitiveType::StringList => types::validate_as_stringlist(self, data),
        }
    }
}

impl Validator for Schema {
    fn validate(&self, data: Option<&Value>) -> ValidationState {
        ScopedSchema::new(self).validate(data)
    }
}

pub fn validate(schema: &Schema, data: &Value) -> ValidationState {
    schema.validate(Some(data))
}