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