schematic 0.14.5

A layered serde configuration and schema library.
Documentation
#![allow(dead_code)]

use schematic::*;
use std::collections::HashMap;

fn test_string<T, C>(value: &str, _: &T, _: &C, _: bool) -> Result<(), ValidateError> {
    if value.is_empty() {
        return Ok(());
    }

    Err(ValidateError::new("invalid string"))
}

#[derive(Config)]
pub struct NestedValidate {
    #[setting(validate = test_string)]
    string2: String,
}

#[derive(Config)]
pub struct Validate {
    #[setting(validate = test_string)]
    string1: String,
    #[setting(nested)]
    nested: NestedValidate,
}

#[test]
fn errors_for_field() {
    let error = ConfigLoader::<Validate>::new()
        .code(r#"{ "string1": "abc" }"#, Format::Json)
        .unwrap()
        .load()
        .err()
        .unwrap();

    assert_eq!(
        error.to_full_string(),
        "Failed to validate Validate. \n  string1: invalid string"
    )
}

#[test]
fn errors_for_nested_field() {
    let error = ConfigLoader::<Validate>::new()
        .code(
            r#"{ "string1": "abc", "nested": { "string2": "abc" } }"#,
            Format::Json,
        )
        .unwrap()
        .load()
        .err()
        .unwrap();

    assert_eq!(
        error.to_full_string(),
        "Failed to validate Validate. \n  string1: invalid string\n  nested.string2: invalid string"
    )
}

fn test_string_path<T, C>(_: &String, _: &T, _: &C) -> Result<(), ValidateError> {
    Err(ValidateError::with_segments(
        "invalid string",
        [PathSegment::Index(1), PathSegment::Key("foo".to_owned())],
    ))
}

#[derive(Config)]
pub struct ValidateFuncs {
    #[setting(validate = validate::alphanumeric)]
    alnum: String,
    #[setting(validate = validate::ascii)]
    ascii: String,
    #[setting(validate = validate::contains("foo"))]
    contains: String,
    #[setting(validate = validate::email)]
    email: String,
    #[setting(validate = validate::ip)]
    ip: String,
    #[setting(validate = validate::ip_v4)]
    ip_v4: String,
    #[setting(validate = validate::ip_v6)]
    ip_v6: String,
    #[setting(validate = validate::regex("^foo$"))]
    regex: String,
    #[setting(validate = validate::min_length(1))]
    min: String,
    #[setting(validate = validate::max_length(1))]
    max: String,
    #[setting(validate = validate::in_length(1, 5))]
    len: String,
    #[setting(validate = validate::url)]
    url: String,
    #[setting(validate = validate::url_secure)]
    url_secure: String,
    #[setting(validate = validate::in_range(1, 5))]
    range: i32,
    #[setting(validate = validate::extends_string)]
    ext_str: String,
    #[setting(validate = validate::extends_list)]
    ext_list: Vec<String>,
    #[setting(validate = validate::extends_from)]
    ext_from: ExtendsFrom,
}

#[test]
fn runs_the_validator_funcs() {
    let error = ConfigLoader::<ValidateFuncs>::new()
        .code(r#"{}"#, Format::Json)
        .unwrap()
        .load()
        .err()
        .unwrap();

    assert_eq!(
        error.to_full_string(),
        "Failed to validate ValidateFuncs. \n  contains: does not contain \"foo\"\n  email: not a valid email: value is empty\n  ip: not a valid IP address\n  ip_v4: not a valid IPv4 address\n  ip_v6: not a valid IPv6 address\n  regex: does not match pattern /^foo$/\n  min: length is lower than 1\n  len: length is lower than 1\n  url: not a valid url: relative URL without a base\n  url_secure: not a valid url: relative URL without a base\n  range: lower than 1\n  ext_str: only file paths and URLs can be extended"
    )
}

#[derive(Config)]
pub struct ValidateOptional {
    #[setting(validate = test_string)]
    string1: Option<String>,
}

#[test]
fn skips_optional_fields() {
    let result = ConfigLoader::<ValidateOptional>::new()
        .code(r#"{}"#, Format::Json)
        .unwrap()
        .load();

    assert!(result.is_ok());
}

#[test]
fn errors_for_optional_field() {
    let error = ConfigLoader::<ValidateOptional>::new()
        .code(r#"{ "string1": "abc" }"#, Format::Json)
        .unwrap()
        .load()
        .err()
        .unwrap();

    assert_eq!(
        error.to_full_string(),
        "Failed to validate ValidateOptional. \n  string1: invalid string"
    )
}

#[derive(Config)]
pub struct ValidateCollections {
    #[setting(nested)]
    list: Vec<NestedValidate>,
    #[setting(nested)]
    map: HashMap<String, NestedValidate>,
}

#[test]
fn errors_for_nested_field_collections() {
    let error = ConfigLoader::<ValidateCollections>::new()
        .code(
            r#"{ "list": [ {"string2": "abc"} ], "map": { "key": {"string2": "abc"} } }"#,
            Format::Json,
        )
        .unwrap()
        .load()
        .err()
        .unwrap();

    assert_eq!(
        error.to_full_string(),
        "Failed to validate ValidateCollections. \n  list[0].string2: invalid string\n  map.key.string2: invalid string"
    )
}

#[derive(Config)]
pub struct ValidateRequired {
    #[setting(required)]
    string: Option<String>,
}

#[derive(Config)]
pub enum ValidateEnumRequired {
    Optional(Option<String>),
    #[setting(required)]
    Required(Option<String>),
}

#[test]
fn doesnt_error_if_required_and_notempty() {
    let result = ConfigLoader::<ValidateRequired>::new()
        .code(r#"{ "string": "abc" }"#, Format::Json)
        .unwrap()
        .load();

    assert!(result.is_ok());

    let result = ConfigLoader::<ValidateEnumRequired>::new()
        .code(r#"{ "required": "abc" }"#, Format::Json)
        .unwrap()
        .load();

    assert!(result.is_ok());

    let result = ConfigLoader::<ValidateEnumRequired>::new()
        .code(r#"{ "optional": null }"#, Format::Json)
        .unwrap()
        .load();

    assert!(result.is_ok());
}

#[test]
fn errors_if_required_and_empty() {
    let error = ConfigLoader::<ValidateRequired>::new()
        .code(r#"{}"#, Format::Json)
        .unwrap()
        .load()
        .err()
        .unwrap();

    assert_eq!(
        error.to_full_string(),
        "Failed to validate ValidateRequired. \n  string: this setting is required"
    );

    let error = ConfigLoader::<ValidateEnumRequired>::new()
        .code(r#"{ "required": null }"#, Format::Json)
        .unwrap()
        .load()
        .err()
        .unwrap();

    assert_eq!(
        error.to_full_string(),
        "Failed to validate ValidateEnumRequired. \n  required: this setting is required"
    );
}