1use log::debug;
2use regex::Regex;
3
4use crate::Context;
5use crate::Result;
6use crate::Validator;
7use crate::schemas::StringFormat;
8use crate::schemas::StringSchema;
9use crate::utils::humanize_yaml_data;
10use crate::validation::formats;
11
12impl Validator for StringSchema {
13 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
14 let errors = self.do_validate(value);
15 if !errors.is_empty() {
16 for error in errors {
17 context.add_error(value, error);
18 }
19 }
20 Ok(())
21 }
22}
23
24impl StringSchema {
25 fn do_validate(&self, value: &saphyr::MarkedYaml) -> Vec<String> {
26 debug!("do_validate: {:?}", value.data);
27 let mut errors = Vec::new();
28
29 if let saphyr::YamlData::Value(scalar) = &value.data
30 && let saphyr::Scalar::String(s) = scalar
31 {
32 let enum_strings = None;
34 debug!("enum_strings: {enum_strings:?}");
35 validate_string(
36 &mut errors,
37 self.min_length,
38 self.max_length,
39 self.pattern.as_ref(),
40 self.format.as_ref(),
41 enum_strings.as_ref(),
42 s,
43 );
44 } else {
45 errors.push(format!(
46 "Expected a string, but got: {}",
47 humanize_yaml_data(&value.data)
48 ));
49 }
50 errors
51 }
52}
53
54pub fn validate_string(
56 errors: &mut Vec<String>,
57 min_length: Option<usize>,
58 max_length: Option<usize>,
59 pattern: Option<&Regex>,
60 format: Option<&StringFormat>,
61 r#enum: Option<&Vec<String>>,
62 str_value: &str,
63) {
64 let char_len =
67 (min_length.is_some() || max_length.is_some()).then(|| str_value.chars().count());
68 if let Some(n) = char_len
69 && let Some(min_length) = min_length
70 && n < min_length
71 {
72 errors.push(format!("String is too short! (min length: {min_length})"));
73 }
74 if let Some(n) = char_len
75 && let Some(max_length) = max_length
76 && n > max_length
77 {
78 errors.push(format!("String is too long! (max length: {max_length})"));
79 }
80 if let Some(regex) = pattern
81 && !regex.is_match(str_value)
82 {
83 errors.push(format!(
84 "String does not match regular expression {}!",
85 regex.as_str()
86 ));
87 }
88 if let Some(fmt) = format
89 && let Some(err) = formats::validate_format(fmt, str_value)
90 {
91 errors.push(err);
92 }
93 if let Some(enum_values) = r#enum
94 && !enum_values.contains(&str_value.to_string())
95 {
96 errors.push(format!("String is not in enum: {enum_values:?}"));
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use crate::Engine;
103 use crate::RootSchema;
104 use crate::YamlSchema;
105 use saphyr::LoadableYamlNode;
106
107 use super::*;
108
109 #[test]
110 fn test_engine_validate_string() {
111 let schema = StringSchema::default();
112 let root_schema = RootSchema::new(YamlSchema::typed_string(schema));
113 let context = Engine::evaluate(&root_schema, "some string", false).unwrap();
114 assert!(!context.has_errors());
115 }
116
117 #[test]
118 fn test_engine_validate_string_with_min_length() {
119 let schema = StringSchema {
120 min_length: Some(5),
121 ..Default::default()
122 };
123 let root_schema = RootSchema::new(YamlSchema::typed_string(schema));
124 let context = Engine::evaluate(&root_schema, "hello", false).unwrap();
125 assert!(!context.has_errors());
126 let context = Engine::evaluate(&root_schema, "hell", false).unwrap();
127 assert!(context.has_errors());
128 }
129
130 #[test]
131 fn test_validate_string() {
132 let mut errors = Vec::new();
133 validate_string(&mut errors, None, None, None, None, None, "hello");
134 assert!(errors.is_empty());
135 }
136
137 #[test]
138 fn test_validate_string_with_min_length() {
139 let mut errors = Vec::new();
140 validate_string(&mut errors, Some(5), None, None, None, None, "hello");
141 assert!(errors.is_empty());
142 validate_string(&mut errors, Some(5), None, None, None, None, "hell");
143 assert!(!errors.is_empty());
144 assert_eq!(
145 errors.first().unwrap(),
146 "String is too short! (min length: 5)"
147 );
148 }
149
150 #[test]
152 fn test_validate_string_length_counts_unicode_scalars_not_utf8_bytes() {
153 let greek = "αβγ";
155 assert_eq!(greek.len(), 6);
156 assert_eq!(greek.chars().count(), 3);
157
158 let mut errors = Vec::new();
159 validate_string(&mut errors, None, Some(3), None, None, None, greek);
160 assert!(
161 errors.is_empty(),
162 "maxLength 3 must allow three characters (not three bytes)"
163 );
164
165 let mut errors = Vec::new();
166 validate_string(&mut errors, None, Some(2), None, None, None, greek);
167 assert_eq!(errors.len(), 1);
168
169 let mut errors = Vec::new();
170 validate_string(&mut errors, Some(4), None, None, None, None, greek);
171 assert_eq!(
172 errors.first().map(|s| s.as_str()),
173 Some("String is too short! (min length: 4)")
174 );
175 }
176
177 #[test]
178 fn test_string_schema_validation() {
179 let schema = StringSchema::default();
180 let docs = saphyr::MarkedYaml::load_from_str("Washington").unwrap();
181 let value = docs.first().unwrap();
182 let context = Context::default();
183 let result = schema.validate(&context, value);
184 assert!(result.is_ok());
185 }
186
187 #[test]
188 fn test_string_schema_doesnt_validate_object() {
189 let yaml = "an: [arbitrarily, nested, data, structure]";
190 let doc = saphyr::MarkedYaml::load_from_str(yaml).unwrap();
191 let marked_yaml = doc.first().unwrap();
192 let string_schema: StringSchema = StringSchema::default();
193 let context = Context::default();
194 let result = string_schema.validate(&context, marked_yaml);
195 assert!(result.is_ok());
196 assert!(context.has_errors());
197 }
198
199 #[test]
200 fn test_validate_string_with_format() {
201 let mut errors = Vec::new();
202 let fmt = StringFormat::Email;
203 validate_string(
204 &mut errors,
205 None,
206 None,
207 None,
208 Some(&fmt),
209 None,
210 "user@example.com",
211 );
212 assert!(errors.is_empty());
213
214 validate_string(
215 &mut errors,
216 None,
217 None,
218 None,
219 Some(&fmt),
220 None,
221 "not-an-email",
222 );
223 assert_eq!(errors.len(), 1);
224 assert!(errors[0].contains("email"));
225 }
226
227 #[test]
228 fn test_engine_validate_string_with_format() {
229 let schema = StringSchema {
230 format: Some(StringFormat::Date),
231 ..Default::default()
232 };
233 let root_schema = RootSchema::new(YamlSchema::typed_string(schema));
234 let context = Engine::evaluate(&root_schema, "2024-01-15", false).unwrap();
235 assert!(!context.has_errors());
236
237 let context = Engine::evaluate(&root_schema, "not-a-date", false).unwrap();
238 assert!(context.has_errors());
239 }
240
241 #[test]
242 fn test_validate_string_unknown_format_always_passes() {
243 let mut errors = Vec::new();
244 let fmt = StringFormat::Unknown("custom".to_string());
245 validate_string(&mut errors, None, None, None, Some(&fmt), None, "anything");
246 assert!(errors.is_empty());
247 }
248}