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::schemas::StringFormat;
12use crate::utils::format_hash_map;
13use crate::utils::format_marker;
14
15/// A string schema
16#[derive(Default)]
17pub struct StringSchema {
18    pub min_length: Option<usize>,
19    pub max_length: Option<usize>,
20    pub pattern: Option<Regex>,
21    pub format: Option<StringFormat>,
22}
23
24impl std::fmt::Debug for StringSchema {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        let mut h = HashMap::new();
27        if let Some(min_length) = self.min_length {
28            h.insert("minLength".to_string(), min_length.to_string());
29        }
30        if let Some(max_length) = self.max_length {
31            h.insert("maxLength".to_string(), max_length.to_string());
32        }
33        if let Some(pattern) = &self.pattern {
34            h.insert("pattern".to_string(), pattern.as_str().to_string());
35        }
36        if let Some(format) = &self.format {
37            h.insert("format".to_string(), format.to_string());
38        }
39        write!(f, "StringSchema {}", format_hash_map(&h))
40    }
41}
42
43impl StringSchema {
44    pub fn builder() -> StringSchemaBuilder {
45        StringSchemaBuilder::new()
46    }
47}
48
49impl PartialEq for StringSchema {
50    fn eq(&self, other: &Self) -> bool {
51        self.min_length == other.min_length
52            && self.max_length == other.max_length
53            && are_patterns_equivalent(&self.pattern, &other.pattern)
54            && self.format == other.format
55    }
56}
57
58impl TryFrom<&MarkedYaml<'_>> for StringSchema {
59    type Error = crate::Error;
60
61    fn try_from(value: &MarkedYaml) -> Result<StringSchema, Self::Error> {
62        if let YamlData::Mapping(mapping) = &value.data {
63            Ok(StringSchema::try_from(mapping)?)
64        } else {
65            Err(expected_mapping!(value))
66        }
67    }
68}
69
70impl TryFrom<&AnnotatedMapping<'_, MarkedYaml<'_>>> for StringSchema {
71    type Error = crate::Error;
72
73    fn try_from(mapping: &AnnotatedMapping<'_, MarkedYaml<'_>>) -> crate::Result<Self> {
74        let mut string_schema = StringSchema::default();
75        for (key, value) in mapping.iter() {
76            if let YamlData::Value(Scalar::String(key)) = &key.data {
77                match key.as_ref() {
78                    "minLength" => {
79                        if let Ok(i) = loader::load_integer_marked(value) {
80                            string_schema.min_length = Some(i as usize);
81                        } else {
82                            return Err(unsupported_type!(
83                                "minLength expected integer, but got: {:?}",
84                                value
85                            ));
86                        }
87                    }
88                    "maxLength" => {
89                        if let Ok(i) = loader::load_integer_marked(value) {
90                            string_schema.max_length = Some(i as usize);
91                        } else {
92                            return Err(unsupported_type!(
93                                "maxLength expected integer, but got: {:?}",
94                                value
95                            ));
96                        }
97                    }
98                    "pattern" => {
99                        if let YamlData::Value(Scalar::String(s)) = &value.data {
100                            let regex = regex::Regex::new(s.as_ref())?;
101                            string_schema.pattern = Some(regex);
102                        } else {
103                            return Err(unsupported_type!(
104                                "pattern expected string, but got: {:?}",
105                                value
106                            ));
107                        }
108                    }
109                    "format" => {
110                        if let YamlData::Value(Scalar::String(s)) = &value.data {
111                            string_schema.format = Some(
112                                s.as_ref()
113                                    .parse::<StringFormat>()
114                                    .unwrap_or_else(|e| match e {}),
115                            );
116                        } else {
117                            return Err(unsupported_type!(
118                                "format expected string, but got: {:?}",
119                                value
120                            ));
121                        }
122                    }
123                    // Maybe this should be handled by the base schema?
124                    "type" => {
125                        if let YamlData::Value(Scalar::String(s)) = &value.data {
126                            if s != "string" {
127                                return Err(unsupported_type!(
128                                    "Expected type: string, but got: {}",
129                                    s
130                                ));
131                            }
132                        } else if let YamlData::Sequence(values) = &value.data {
133                            if !values
134                                .iter()
135                                .any(|v| v.data == MarkedYaml::value_from_str("string").data)
136                            {
137                                return Err(unsupported_type!(
138                                    "Expected type: string, but got: {:?}",
139                                    value
140                                ));
141                            }
142                        } else {
143                            return Err(expected_type_is_string!(value));
144                        }
145                    }
146                    _ => {
147                        debug!("[StringSchema] Unsupported key for `type: string`: {key}");
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: {:?}, format: {:?} }}",
177            self.min_length, self.max_length, self.pattern, self.format
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 format(&mut self, format: StringFormat) -> &mut Self {
215        self.0.format = Some(format);
216        self
217    }
218}