yaml_schema/schemas/
string.rs

1use regex::Regex;
2use saphyr::AnnotatedMapping;
3use saphyr::MarkedYaml;
4use saphyr::Scalar;
5use saphyr::YamlData;
6
7use crate::ConstValue;
8use crate::Schema;
9use crate::YamlSchema;
10use crate::loader;
11use crate::schemas::SchemaMetadata;
12use crate::schemas::base::BaseSchema;
13use crate::utils::format_hash_map;
14use crate::utils::format_marker;
15
16/// A string schema
17#[derive(Default)]
18pub struct StringSchema {
19    pub base: BaseSchema,
20    pub min_length: Option<usize>,
21    pub max_length: Option<usize>,
22    pub pattern: Option<Regex>,
23}
24
25impl SchemaMetadata for StringSchema {
26    fn get_accepted_keys() -> &'static [&'static str] {
27        &["minLength", "maxLength", "pattern"]
28    }
29}
30
31impl std::fmt::Debug for StringSchema {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        let mut h = self.base.as_hash_map();
34        if let Some(min_length) = self.min_length {
35            h.insert("minLength".to_string(), min_length.to_string());
36        }
37        if let Some(max_length) = self.max_length {
38            h.insert("maxLength".to_string(), max_length.to_string());
39        }
40        if let Some(pattern) = &self.pattern {
41            h.insert("pattern".to_string(), pattern.as_str().to_string());
42        }
43        write!(f, "StringSchema {}", format_hash_map(&h))
44    }
45}
46
47impl StringSchema {
48    pub fn from_base(base: BaseSchema) -> Self {
49        Self {
50            base,
51            ..Default::default()
52        }
53    }
54
55    pub fn builder() -> StringSchemaBuilder {
56        StringSchemaBuilder::new()
57    }
58}
59
60impl PartialEq for StringSchema {
61    fn eq(&self, other: &Self) -> bool {
62        self.min_length == other.min_length
63            && self.max_length == other.max_length
64            && are_patterns_equivalent(&self.pattern, &other.pattern)
65    }
66}
67
68impl From<StringSchema> for YamlSchema {
69    fn from(value: StringSchema) -> Self {
70        YamlSchema {
71            schema: Some(Schema::typed_string(value)),
72            ..Default::default()
73        }
74    }
75}
76
77impl TryFrom<&MarkedYaml<'_>> for StringSchema {
78    type Error = crate::Error;
79
80    fn try_from(value: &MarkedYaml) -> Result<StringSchema, Self::Error> {
81        if let YamlData::Mapping(mapping) = &value.data {
82            Ok(StringSchema::try_from(mapping)?)
83        } else {
84            Err(expected_mapping!(value))
85        }
86    }
87}
88
89impl TryFrom<&AnnotatedMapping<'_, MarkedYaml<'_>>> for StringSchema {
90    type Error = crate::Error;
91
92    fn try_from(mapping: &AnnotatedMapping<'_, MarkedYaml<'_>>) -> crate::Result<Self> {
93        let mut string_schema = StringSchema::from_base(BaseSchema::try_from(mapping)?);
94        for (key, value) in mapping.iter() {
95            if let YamlData::Value(Scalar::String(key)) = &key.data {
96                if string_schema.base.handle_key_value(key, value)?.is_none() {
97                    match key.as_ref() {
98                        "minLength" => {
99                            if let Ok(i) = loader::load_integer_marked(value) {
100                                string_schema.min_length = Some(i as usize);
101                            } else {
102                                return Err(unsupported_type!(
103                                    "minLength expected integer, but got: {:?}",
104                                    value
105                                ));
106                            }
107                        }
108                        "maxLength" => {
109                            if let Ok(i) = loader::load_integer_marked(value) {
110                                string_schema.max_length = Some(i as usize);
111                            } else {
112                                return Err(unsupported_type!(
113                                    "maxLength expected integer, but got: {:?}",
114                                    value
115                                ));
116                            }
117                        }
118                        "pattern" => {
119                            if let YamlData::Value(Scalar::String(s)) = &value.data {
120                                let regex = regex::Regex::new(s.as_ref())?;
121                                string_schema.pattern = Some(regex);
122                            } else {
123                                return Err(unsupported_type!(
124                                    "pattern expected string, but got: {:?}",
125                                    value
126                                ));
127                            }
128                        }
129                        // Maybe this should be handled by the base schema?
130                        "type" => {
131                            if let YamlData::Value(Scalar::String(s)) = &value.data {
132                                if s != "string" {
133                                    return Err(unsupported_type!(
134                                        "Expected type: string, but got: {}",
135                                        s
136                                    ));
137                                }
138                            } else {
139                                return Err(expected_type_is_string!(value));
140                            }
141                        }
142                        _ => {
143                            return Err(schema_loading_error!(
144                                "Unsupported key for type: string: {}",
145                                key
146                            ));
147                        }
148                    }
149                }
150            } else {
151                return Err(expected_scalar!(
152                    "{} Expected a scalar key, got: {:#?}",
153                    format_marker(&key.span.start),
154                    key
155                ));
156            }
157        }
158        Ok(string_schema)
159    }
160}
161/// 'Naive' check to see if two regexes are equal, by comparing their string representations
162/// We do it this way because we can't `impl PartialEq for Regex` and don't want to have to
163/// alias or wrap the `regex::Regex` type
164fn are_patterns_equivalent(a: &Option<Regex>, b: &Option<Regex>) -> bool {
165    match (a, b) {
166        (Some(a), Some(b)) => a.as_str() == b.as_str(),
167        (None, None) => true,
168        _ => false,
169    }
170}
171
172impl std::fmt::Display for StringSchema {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        write!(
175            f,
176            "StringSchema {{ min_length: {:?}, max_length: {:?}, pattern: {:?} }}",
177            self.min_length, self.max_length, self.pattern
178        )
179    }
180}
181
182pub struct StringSchemaBuilder(StringSchema);
183
184impl Default for StringSchemaBuilder {
185    fn default() -> Self {
186        Self::new()
187    }
188}
189
190impl StringSchemaBuilder {
191    pub fn new() -> Self {
192        Self(StringSchema::default())
193    }
194
195    pub fn build(&mut self) -> StringSchema {
196        std::mem::take(&mut self.0)
197    }
198
199    pub fn min_length(&mut self, min_length: usize) -> &mut Self {
200        self.0.min_length = Some(min_length);
201        self
202    }
203
204    pub fn max_length(&mut self, max_length: usize) -> &mut Self {
205        self.0.max_length = Some(max_length);
206        self
207    }
208
209    pub fn pattern(&mut self, pattern: Regex) -> &mut Self {
210        self.0.pattern = Some(pattern);
211        self
212    }
213
214    pub fn r#enum(&mut self, r#enum: Vec<String>) -> &mut Self {
215        self.0.base.r#enum = Some(r#enum.into_iter().map(ConstValue::string).collect());
216        self
217    }
218
219    pub fn add_enum<S>(&mut self, s: S) -> &mut Self
220    where
221        S: Into<String>,
222    {
223        if let Some(r#enum) = self.0.base.r#enum.as_mut() {
224            r#enum.push(ConstValue::string(s.into()));
225            self
226        } else {
227            self.r#enum(vec![s.into()])
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_string_schema_builder() {
238        let schema = StringSchema::builder()
239            .add_enum("foo")
240            .add_enum("bar")
241            .build();
242        assert_eq!(
243            StringSchema {
244                base: BaseSchema {
245                    r#enum: Some(vec![ConstValue::string("foo"), ConstValue::string("bar")]),
246                    ..Default::default()
247                },
248                ..Default::default()
249            },
250            schema
251        );
252    }
253}