yaml_schema/validation/
strings.rs

1use log::debug;
2use regex::Regex;
3
4use crate::ConstValue;
5use crate::Context;
6use crate::Result;
7use crate::StringSchema;
8use crate::Validator;
9
10impl Validator for StringSchema {
11    fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
12        let errors = self.do_validate(value);
13        if !errors.is_empty() {
14            for error in errors {
15                context.add_error(value, error);
16            }
17        }
18        Ok(())
19    }
20}
21
22impl StringSchema {
23    fn do_validate(&self, value: &saphyr::MarkedYaml) -> Vec<String> {
24        debug!("do_validate: {:?}", value.data);
25        let mut errors = Vec::new();
26
27        if let saphyr::YamlData::Value(scalar) = &value.data
28            && let saphyr::Scalar::String(s) = scalar
29        {
30            let enum_strings = self.base.r#enum.as_ref().map(|enum_values| {
31                enum_values
32                    .iter()
33                    .filter_map(|v| {
34                        if let ConstValue::String(s) = v {
35                            Some(s.clone())
36                        } else {
37                            None
38                        }
39                    })
40                    .collect()
41            });
42            debug!("enum_strings: {enum_strings:?}");
43            validate_string(
44                &mut errors,
45                self.min_length,
46                self.max_length,
47                self.pattern.as_ref(),
48                enum_strings.as_ref(),
49                s,
50            );
51        } else {
52            errors.push(format!("Expected a string, but got: {:?}", value.data));
53        }
54        errors
55    }
56}
57
58/// Just trying to isolate the actual validation into a function that doesn't take a context
59pub fn validate_string(
60    errors: &mut Vec<String>,
61    min_length: Option<usize>,
62    max_length: Option<usize>,
63    pattern: Option<&Regex>,
64    r#enum: Option<&Vec<String>>,
65    str_value: &str,
66) {
67    if let Some(min_length) = min_length
68        && str_value.len() < min_length
69    {
70        errors.push(format!("String is too short! (min length: {min_length})"));
71    }
72    if let Some(max_length) = max_length
73        && str_value.len() > max_length
74    {
75        errors.push(format!("String is too long! (max length: {max_length})"));
76    }
77    if let Some(regex) = pattern
78        && !regex.is_match(str_value)
79    {
80        errors.push(format!(
81            "String does not match regular expression {}!",
82            regex.as_str()
83        ));
84    }
85    if let Some(enum_values) = r#enum
86        && !enum_values.contains(&str_value.to_string())
87    {
88        errors.push(format!("String is not in enum: {enum_values:?}"));
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use crate::Engine;
95    use crate::RootSchema;
96    use crate::Schema;
97    use saphyr::LoadableYamlNode;
98
99    use super::*;
100
101    #[test]
102    fn test_engine_validate_string() {
103        let schema = StringSchema::default();
104        let root_schema = RootSchema::new_with_schema(Schema::typed_string(schema));
105        let context = Engine::evaluate(&root_schema, "some string", false).unwrap();
106        assert!(!context.has_errors());
107    }
108
109    #[test]
110    fn test_engine_validate_string_with_min_length() {
111        let schema = StringSchema {
112            min_length: Some(5),
113            ..Default::default()
114        };
115        let root_schema = RootSchema::new_with_schema(Schema::typed_string(schema));
116        let context = Engine::evaluate(&root_schema, "hello", false).unwrap();
117        assert!(!context.has_errors());
118        let context = Engine::evaluate(&root_schema, "hell", false).unwrap();
119        assert!(context.has_errors());
120    }
121
122    #[test]
123    fn test_validate_string() {
124        let mut errors = Vec::new();
125        validate_string(&mut errors, None, None, None, None, "hello");
126        assert!(errors.is_empty());
127    }
128
129    #[test]
130    fn test_validate_string_with_min_length() {
131        let mut errors = Vec::new();
132        validate_string(&mut errors, Some(5), None, None, None, "hello");
133        assert!(errors.is_empty());
134        validate_string(&mut errors, Some(5), None, None, None, "hell");
135        assert!(!errors.is_empty());
136        assert_eq!(
137            errors.first().unwrap(),
138            "String is too short! (min length: 5)"
139        );
140    }
141
142    #[test]
143    fn test_string_schema_validation() {
144        let schema = StringSchema::default();
145        let docs = saphyr::MarkedYaml::load_from_str("Washington").unwrap();
146        let value = docs.first().unwrap();
147        let context = Context::default();
148        let result = schema.validate(&context, value);
149        assert!(result.is_ok());
150    }
151
152    #[test]
153    fn test_string_schema_doesnt_validate_object() {
154        let yaml = "an: [arbitrarily, nested, data, structure]";
155        let doc = saphyr::MarkedYaml::load_from_str(yaml).unwrap();
156        let marked_yaml = doc.first().unwrap();
157        let string_schema: StringSchema = StringSchema::default();
158        let context = Context::default();
159        let result = string_schema.validate(&context, &marked_yaml);
160        assert!(result.is_ok());
161        assert!(context.has_errors());
162    }
163}