yaml_schema/schemas/
string.rs

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