1use std::str::FromStr;
25
26use serde::{Deserialize, Serialize};
27use strum_macros::{Display, EnumString};
28
29#[cfg(feature = "python")]
30use pyo3::pyclass;
31
32#[cfg(feature = "wasm")]
33use tsify_next::Tsify;
34
35#[derive(Debug, Clone, PartialEq, EnumString, Display)]
47#[cfg_attr(feature = "python", pyclass(get_all, from_py_object))]
48#[cfg_attr(feature = "wasm", derive(Tsify))]
49#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
50#[derive(Serialize, Deserialize)]
51#[serde(try_from = "RawOption")]
52#[serde(into = "RawOption")]
53pub enum AttrOption {
54 Example(String),
56
57 #[strum(serialize = "minimum")]
60 MinimumValue(f64),
61 #[strum(serialize = "maximum")]
63 MaximumValue(f64),
64 #[strum(serialize = "minitems")]
66 MinItems(usize),
67 #[strum(serialize = "maxitems")]
69 MaxItems(usize),
70 #[strum(serialize = "minlength")]
72 MinLength(usize),
73 #[strum(serialize = "maxlength")]
75 MaxLength(usize),
76 #[strum(serialize = "pattern", serialize = "regex")]
78 Pattern(String),
79 #[strum(serialize = "unique")]
81 Unique(bool),
82 #[strum(serialize = "multipleof")]
84 MultipleOf(i32),
85 #[strum(serialize = "exclusiveminimum")]
87 ExclusiveMinimum(f64),
88 #[strum(serialize = "exclusivemaximum")]
90 ExclusiveMaximum(f64),
91
92 #[strum(serialize = "pk")]
95 PrimaryKey(bool),
96
97 #[strum(serialize = "readonly")]
100 ReadOnly(bool),
101 #[strum(serialize = "recommended")]
103 Recommended(bool),
104
105 Other {
108 key: String,
110 value: String,
112 },
113}
114
115impl AttrOption {
116 pub fn from_pair(key: &str, value: &str) -> Result<Self, Box<dyn std::error::Error>> {
131 match AttrOption::from_str(key) {
132 Ok(option) => match option {
133 AttrOption::MinimumValue(_) => Ok(AttrOption::MinimumValue(value.parse()?)),
134 AttrOption::MaximumValue(_) => Ok(AttrOption::MaximumValue(value.parse()?)),
135 AttrOption::MinItems(_) => Ok(AttrOption::MinItems(value.parse()?)),
136 AttrOption::MaxItems(_) => Ok(AttrOption::MaxItems(value.parse()?)),
137 AttrOption::MinLength(_) => Ok(AttrOption::MinLength(value.parse()?)),
138 AttrOption::MaxLength(_) => Ok(AttrOption::MaxLength(value.parse()?)),
139 AttrOption::Pattern(_) => Ok(AttrOption::Pattern(value.to_string())),
140 AttrOption::Unique(_) => Ok(AttrOption::Unique(value.parse()?)),
141 AttrOption::MultipleOf(_) => Ok(AttrOption::MultipleOf(value.parse()?)),
142 AttrOption::ExclusiveMinimum(_) => Ok(AttrOption::ExclusiveMinimum(value.parse()?)),
143 AttrOption::ExclusiveMaximum(_) => Ok(AttrOption::ExclusiveMaximum(value.parse()?)),
144 AttrOption::PrimaryKey(_) => Ok(AttrOption::PrimaryKey(value.parse()?)),
145 AttrOption::ReadOnly(_) => Ok(AttrOption::ReadOnly(value.parse()?)),
146 AttrOption::Recommended(_) => Ok(AttrOption::Recommended(value.parse()?)),
147 AttrOption::Example(_) => Ok(AttrOption::Example(value.to_string())),
148 AttrOption::Other { .. } => unreachable!(),
149 },
150 Err(_) => Ok(AttrOption::Other {
151 key: key.to_string(),
152 value: value.to_string(),
153 }),
154 }
155 }
156
157 pub fn to_pair(&self) -> (String, String) {
163 (self.key(), self.value())
164 }
165
166 pub fn key(&self) -> String {
175 match self {
176 AttrOption::Other { key, .. } => key.to_string(),
177 _ => self.to_string(),
178 }
179 }
180
181 pub fn value(&self) -> String {
187 match self {
188 AttrOption::Other { value, .. } => value.to_string(),
189 AttrOption::MinimumValue(value) => value.to_string(),
190 AttrOption::MaximumValue(value) => value.to_string(),
191 AttrOption::MinItems(value) => value.to_string(),
192 AttrOption::MaxItems(value) => value.to_string(),
193 AttrOption::MinLength(value) => value.to_string(),
194 AttrOption::MaxLength(value) => value.to_string(),
195 AttrOption::Pattern(value) => value.to_string(),
196 AttrOption::Unique(value) => value.to_string(),
197 AttrOption::MultipleOf(value) => value.to_string(),
198 AttrOption::ExclusiveMinimum(value) => value.to_string(),
199 AttrOption::ExclusiveMaximum(value) => value.to_string(),
200 AttrOption::PrimaryKey(value) => value.to_string(),
201 AttrOption::ReadOnly(value) => value.to_string(),
202 AttrOption::Recommended(value) => value.to_string(),
203 AttrOption::Example(value) => value.to_string(),
204 }
205 }
206}
207
208#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
214#[cfg_attr(feature = "python", pyclass(get_all, from_py_object))]
215#[cfg_attr(feature = "wasm", derive(Tsify))]
216#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
217pub struct RawOption {
218 pub key: String,
220 pub value: String,
222}
223
224impl RawOption {
225 pub fn new(key: String, value: String) -> Self {
234 Self {
235 key: key.to_lowercase(),
236 value,
237 }
238 }
239
240 pub fn key(&self) -> &str {
246 &self.key
247 }
248
249 pub fn value(&self) -> &str {
255 &self.value
256 }
257}
258
259impl TryFrom<RawOption> for AttrOption {
260 type Error = Box<dyn std::error::Error>;
261
262 fn try_from(option: RawOption) -> Result<Self, Self::Error> {
263 AttrOption::from_pair(&option.key, &option.value)
264 }
265}
266
267impl From<AttrOption> for RawOption {
268 fn from(option: AttrOption) -> Self {
269 RawOption::new(option.key(), option.value())
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use std::path::PathBuf;
276
277 use crate::prelude::DataModel;
278 use pretty_assertions::assert_eq;
279
280 use super::*;
281
282 #[test]
283 fn test_from_pair_basic() {
284 let cases = vec![
285 ("minimum", "10.5", AttrOption::MinimumValue(10.5)),
286 ("maximum", "100.0", AttrOption::MaximumValue(100.0)),
287 ("minitems", "5", AttrOption::MinItems(5)),
288 ("maxitems", "10", AttrOption::MaxItems(10)),
289 ("minlength", "3", AttrOption::MinLength(3)),
290 ("maxlength", "20", AttrOption::MaxLength(20)),
291 (
292 "pattern",
293 "^[a-z]+$",
294 AttrOption::Pattern("^[a-z]+$".to_string()),
295 ),
296 (
297 "regex",
298 "^[a-z]+$",
299 AttrOption::Pattern("^[a-z]+$".to_string()),
300 ),
301 ("unique", "true", AttrOption::Unique(true)),
302 ("multipleof", "3", AttrOption::MultipleOf(3)),
303 ("exclusiveminimum", "0.5", AttrOption::ExclusiveMinimum(0.5)),
304 (
305 "exclusivemaximum",
306 "99.9",
307 AttrOption::ExclusiveMaximum(99.9),
308 ),
309 ("pk", "true", AttrOption::PrimaryKey(true)),
310 ("readonly", "false", AttrOption::ReadOnly(false)),
311 ("recommended", "true", AttrOption::Recommended(true)),
312 ];
313
314 for (key, value, expected) in cases {
315 let result = AttrOption::from_pair(key, value).unwrap();
316 assert_eq!(result, expected);
317 }
318 }
319
320 #[test]
321 fn test_from_pair_other() {
322 let result = AttrOption::from_pair("custom_option", "value").unwrap();
323 assert_eq!(
324 result,
325 AttrOption::Other {
326 key: "custom_option".to_string(),
327 value: "value".to_string()
328 }
329 );
330 }
331
332 #[test]
333 fn test_from_pair_invalid_values() {
334 assert!(AttrOption::from_pair("minimum", "not_a_number").is_err());
336 assert!(AttrOption::from_pair("minitems", "-1").is_err());
337 assert!(AttrOption::from_pair("multipleof", "3.5").is_err());
338
339 assert!(AttrOption::from_pair("unique", "not_a_bool").is_err());
341 assert!(AttrOption::from_pair("pk", "invalid").is_err());
342 }
343
344 #[test]
345 fn test_to_pair() {
346 let cases = vec![
347 (
348 AttrOption::MinimumValue(10.5),
349 ("minimum".to_string(), "10.5".to_string()),
350 ),
351 (
352 AttrOption::Pattern("^test$".to_string()),
353 ("pattern".to_string(), "^test$".to_string()),
354 ),
355 (
356 AttrOption::Other {
357 key: "custom".to_string(),
358 value: "test".to_string(),
359 },
360 ("custom".to_string(), "test".to_string()),
361 ),
362 ];
363
364 for (option, expected) in cases {
365 assert_eq!(option.to_pair(), expected);
366 }
367 }
368
369 #[test]
370 fn test_raw_option_conversion() {
371 let raw = RawOption::new("minimum".to_string(), "10.5".to_string());
372 let attr: AttrOption = raw.try_into().unwrap();
373 assert_eq!(attr, AttrOption::MinimumValue(10.5));
374
375 let raw_back: RawOption = attr.into();
376 assert_eq!(raw_back.key(), "minimum");
377 assert_eq!(raw_back.value(), "10.5");
378 }
379
380 #[test]
381 fn test_raw_option_case_sensitivity() {
382 let raw = RawOption::new("MINIMUM".to_string(), "10.5".to_string());
383 let attr: AttrOption = raw.try_into().unwrap();
384 assert_eq!(attr, AttrOption::MinimumValue(10.5));
385 }
386
387 #[test]
388 fn test_raw_option_serialize() {
389 let raw = RawOption::new("minimum".to_string(), "10.5".to_string());
390 let serialized = serde_json::to_string(&raw).unwrap();
391 assert_eq!(serialized, r#"{"key":"minimum","value":"10.5"}"#);
392 }
393
394 #[test]
395 fn test_raw_option_deserialize() {
396 let serialized = r#"{"key":"minimum","value":"10.5"}"#;
397 let deserialized: RawOption = serde_json::from_str(serialized).unwrap();
398 assert_eq!(deserialized.key(), "minimum");
399 assert_eq!(deserialized.value(), "10.5");
400 }
401
402 #[test]
403 fn test_attr_option_from_str() {
404 let path = PathBuf::from("tests/data/model_options.md");
405 let model = DataModel::from_markdown(&path).expect("Failed to parse markdown file");
406 let attr = model.objects.first().unwrap();
407 let attribute = attr.attributes.first().unwrap();
408 let options = attribute
409 .options
410 .iter()
411 .map(|o| o.key())
412 .collect::<Vec<_>>();
413
414 let expected = vec![
415 "minimum",
416 "maximum",
417 "minitems",
418 "maxitems",
419 "minlength",
420 "maxlength",
421 "pattern",
422 "unique",
423 "multipleof",
424 "exclusiveminimum",
425 "exclusivemaximum",
426 "primarykey",
427 "readonly",
428 "recommended",
429 ];
430
431 let mut missing = Vec::new();
432 for expected_option in expected {
433 if !options.contains(&expected_option.to_string()) {
434 missing.push(expected_option);
435 }
436 }
437 assert!(
438 missing.is_empty(),
439 "Expected options \n[{}]\nnot found in \n[{}]",
440 missing.join(", "),
441 options.join(", ")
442 );
443
444 let expected_options = vec![
446 AttrOption::Example("test".to_string()),
447 AttrOption::MinimumValue(0.0),
448 AttrOption::MaximumValue(100.0),
449 AttrOption::MinItems(1),
450 AttrOption::MaxItems(10),
451 AttrOption::MinLength(1),
452 AttrOption::MaxLength(100),
453 AttrOption::Pattern("^[a-zA-Z0-9]+$".to_string()),
454 AttrOption::Pattern("^[a-zA-Z0-9]+$".to_string()),
455 AttrOption::Unique(true),
456 AttrOption::MultipleOf(2),
457 AttrOption::ExclusiveMinimum(0.0),
458 AttrOption::ExclusiveMaximum(100.0),
459 AttrOption::PrimaryKey(true),
460 AttrOption::ReadOnly(true),
461 AttrOption::Recommended(true),
462 ];
463
464 for expected_option in expected_options.iter() {
465 for option in attribute.options.iter() {
466 if option.key() == expected_option.key() {
467 assert_eq!(option, expected_option);
468 }
469 }
470 }
471 }
472}