yaml_schema/schemas/
string.rs1use 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#[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 "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}
161fn 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}