Skip to main content

yaml_schema/schemas/
string.rs

1use std::collections::HashMap;
2
3use log::debug;
4use regex::Regex;
5use saphyr::AnnotatedMapping;
6use saphyr::MarkedYaml;
7use saphyr::Scalar;
8use saphyr::YamlData;
9
10use crate::loader;
11use crate::utils::format_hash_map;
12use crate::utils::format_marker;
13
14/// A string schema
15#[derive(Default)]
16pub struct StringSchema {
17    pub min_length: Option<usize>,
18    pub max_length: Option<usize>,
19    pub pattern: Option<Regex>,
20}
21
22impl std::fmt::Debug for StringSchema {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        let mut h = HashMap::new();
25        if let Some(min_length) = self.min_length {
26            h.insert("minLength".to_string(), min_length.to_string());
27        }
28        if let Some(max_length) = self.max_length {
29            h.insert("maxLength".to_string(), max_length.to_string());
30        }
31        if let Some(pattern) = &self.pattern {
32            h.insert("pattern".to_string(), pattern.as_str().to_string());
33        }
34        write!(f, "StringSchema {}", format_hash_map(&h))
35    }
36}
37
38impl StringSchema {
39    pub fn builder() -> StringSchemaBuilder {
40        StringSchemaBuilder::new()
41    }
42}
43
44impl PartialEq for StringSchema {
45    fn eq(&self, other: &Self) -> bool {
46        self.min_length == other.min_length
47            && self.max_length == other.max_length
48            && are_patterns_equivalent(&self.pattern, &other.pattern)
49    }
50}
51
52impl TryFrom<&MarkedYaml<'_>> for StringSchema {
53    type Error = crate::Error;
54
55    fn try_from(value: &MarkedYaml) -> Result<StringSchema, Self::Error> {
56        if let YamlData::Mapping(mapping) = &value.data {
57            Ok(StringSchema::try_from(mapping)?)
58        } else {
59            Err(expected_mapping!(value))
60        }
61    }
62}
63
64impl TryFrom<&AnnotatedMapping<'_, MarkedYaml<'_>>> for StringSchema {
65    type Error = crate::Error;
66
67    fn try_from(mapping: &AnnotatedMapping<'_, MarkedYaml<'_>>) -> crate::Result<Self> {
68        let mut string_schema = StringSchema::default();
69        for (key, value) in mapping.iter() {
70            if let YamlData::Value(Scalar::String(key)) = &key.data {
71                match key.as_ref() {
72                    "minLength" => {
73                        if let Ok(i) = loader::load_integer_marked(value) {
74                            string_schema.min_length = Some(i as usize);
75                        } else {
76                            return Err(unsupported_type!(
77                                "minLength expected integer, but got: {:?}",
78                                value
79                            ));
80                        }
81                    }
82                    "maxLength" => {
83                        if let Ok(i) = loader::load_integer_marked(value) {
84                            string_schema.max_length = Some(i as usize);
85                        } else {
86                            return Err(unsupported_type!(
87                                "maxLength expected integer, but got: {:?}",
88                                value
89                            ));
90                        }
91                    }
92                    "pattern" => {
93                        if let YamlData::Value(Scalar::String(s)) = &value.data {
94                            let regex = regex::Regex::new(s.as_ref())?;
95                            string_schema.pattern = Some(regex);
96                        } else {
97                            return Err(unsupported_type!(
98                                "pattern expected string, but got: {:?}",
99                                value
100                            ));
101                        }
102                    }
103                    // Maybe this should be handled by the base schema?
104                    "type" => {
105                        if let YamlData::Value(Scalar::String(s)) = &value.data {
106                            if s != "string" {
107                                return Err(unsupported_type!(
108                                    "Expected type: string, but got: {}",
109                                    s
110                                ));
111                            }
112                        } else if let YamlData::Sequence(values) = &value.data {
113                            if !values
114                                .iter()
115                                .any(|v| v.data == MarkedYaml::value_from_str("string").data)
116                            {
117                                return Err(unsupported_type!(
118                                    "Expected type: string, but got: {:?}",
119                                    value
120                                ));
121                            }
122                        } else {
123                            return Err(expected_type_is_string!(value));
124                        }
125                    }
126                    _ => {
127                        debug!("[StringSchema] Unsupported key for `type: string`: {key}");
128                    }
129                }
130            } else {
131                return Err(expected_scalar!(
132                    "{} Expected a scalar key, got: {:?}",
133                    format_marker(&key.span.start),
134                    key
135                ));
136            }
137        }
138        Ok(string_schema)
139    }
140}
141/// 'Naive' check to see if two regexes are equal, by comparing their string representations
142/// We do it this way because we can't `impl PartialEq for Regex` and don't want to have to
143/// alias or wrap the `regex::Regex` type
144fn are_patterns_equivalent(a: &Option<Regex>, b: &Option<Regex>) -> bool {
145    match (a, b) {
146        (Some(a), Some(b)) => a.as_str() == b.as_str(),
147        (None, None) => true,
148        _ => false,
149    }
150}
151
152impl std::fmt::Display for StringSchema {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        write!(
155            f,
156            "StringSchema {{ min_length: {:?}, max_length: {:?}, pattern: {:?} }}",
157            self.min_length, self.max_length, self.pattern
158        )
159    }
160}
161
162pub struct StringSchemaBuilder(StringSchema);
163
164impl Default for StringSchemaBuilder {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170impl StringSchemaBuilder {
171    pub fn new() -> Self {
172        Self(StringSchema::default())
173    }
174
175    pub fn build(&mut self) -> StringSchema {
176        std::mem::take(&mut self.0)
177    }
178
179    pub fn min_length(&mut self, min_length: usize) -> &mut Self {
180        self.0.min_length = Some(min_length);
181        self
182    }
183
184    pub fn max_length(&mut self, max_length: usize) -> &mut Self {
185        self.0.max_length = Some(max_length);
186        self
187    }
188
189    pub fn pattern(&mut self, pattern: Regex) -> &mut Self {
190        self.0.pattern = Some(pattern);
191        self
192    }
193}