use std::str::FromStr;
use serde::{Deserialize, Serialize};
use strum_macros::{Display, EnumString};
#[cfg(feature = "python")]
use pyo3::pyclass;
#[cfg(feature = "wasm")]
use tsify_next::Tsify;
#[derive(Debug, Clone, PartialEq, EnumString, Display)]
#[cfg_attr(feature = "python", pyclass(get_all, from_py_object))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
#[derive(Serialize, Deserialize)]
#[serde(try_from = "RawOption")]
#[serde(into = "RawOption")]
pub enum AttrOption {
Example(String),
#[strum(serialize = "minimum")]
MinimumValue(f64),
#[strum(serialize = "maximum")]
MaximumValue(f64),
#[strum(serialize = "minitems")]
MinItems(usize),
#[strum(serialize = "maxitems")]
MaxItems(usize),
#[strum(serialize = "minlength")]
MinLength(usize),
#[strum(serialize = "maxlength")]
MaxLength(usize),
#[strum(serialize = "pattern", serialize = "regex")]
Pattern(String),
#[strum(serialize = "unique")]
Unique(bool),
#[strum(serialize = "multipleof")]
MultipleOf(i32),
#[strum(serialize = "exclusiveminimum")]
ExclusiveMinimum(f64),
#[strum(serialize = "exclusivemaximum")]
ExclusiveMaximum(f64),
#[strum(serialize = "pk")]
PrimaryKey(bool),
#[strum(serialize = "readonly")]
ReadOnly(bool),
#[strum(serialize = "recommended")]
Recommended(bool),
Other {
key: String,
value: String,
},
}
impl AttrOption {
pub fn from_pair(key: &str, value: &str) -> Result<Self, Box<dyn std::error::Error>> {
match AttrOption::from_str(key) {
Ok(option) => match option {
AttrOption::MinimumValue(_) => Ok(AttrOption::MinimumValue(value.parse()?)),
AttrOption::MaximumValue(_) => Ok(AttrOption::MaximumValue(value.parse()?)),
AttrOption::MinItems(_) => Ok(AttrOption::MinItems(value.parse()?)),
AttrOption::MaxItems(_) => Ok(AttrOption::MaxItems(value.parse()?)),
AttrOption::MinLength(_) => Ok(AttrOption::MinLength(value.parse()?)),
AttrOption::MaxLength(_) => Ok(AttrOption::MaxLength(value.parse()?)),
AttrOption::Pattern(_) => Ok(AttrOption::Pattern(value.to_string())),
AttrOption::Unique(_) => Ok(AttrOption::Unique(value.parse()?)),
AttrOption::MultipleOf(_) => Ok(AttrOption::MultipleOf(value.parse()?)),
AttrOption::ExclusiveMinimum(_) => Ok(AttrOption::ExclusiveMinimum(value.parse()?)),
AttrOption::ExclusiveMaximum(_) => Ok(AttrOption::ExclusiveMaximum(value.parse()?)),
AttrOption::PrimaryKey(_) => Ok(AttrOption::PrimaryKey(value.parse()?)),
AttrOption::ReadOnly(_) => Ok(AttrOption::ReadOnly(value.parse()?)),
AttrOption::Recommended(_) => Ok(AttrOption::Recommended(value.parse()?)),
AttrOption::Example(_) => Ok(AttrOption::Example(value.to_string())),
AttrOption::Other { .. } => unreachable!(),
},
Err(_) => Ok(AttrOption::Other {
key: key.to_string(),
value: value.to_string(),
}),
}
}
pub fn to_pair(&self) -> (String, String) {
(self.key(), self.value())
}
pub fn key(&self) -> String {
match self {
AttrOption::Other { key, .. } => key.to_string(),
_ => self.to_string(),
}
}
pub fn value(&self) -> String {
match self {
AttrOption::Other { value, .. } => value.to_string(),
AttrOption::MinimumValue(value) => value.to_string(),
AttrOption::MaximumValue(value) => value.to_string(),
AttrOption::MinItems(value) => value.to_string(),
AttrOption::MaxItems(value) => value.to_string(),
AttrOption::MinLength(value) => value.to_string(),
AttrOption::MaxLength(value) => value.to_string(),
AttrOption::Pattern(value) => value.to_string(),
AttrOption::Unique(value) => value.to_string(),
AttrOption::MultipleOf(value) => value.to_string(),
AttrOption::ExclusiveMinimum(value) => value.to_string(),
AttrOption::ExclusiveMaximum(value) => value.to_string(),
AttrOption::PrimaryKey(value) => value.to_string(),
AttrOption::ReadOnly(value) => value.to_string(),
AttrOption::Recommended(value) => value.to_string(),
AttrOption::Example(value) => value.to_string(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "python", pyclass(get_all, from_py_object))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
pub struct RawOption {
pub key: String,
pub value: String,
}
impl RawOption {
pub fn new(key: String, value: String) -> Self {
Self {
key: key.to_lowercase(),
value,
}
}
pub fn key(&self) -> &str {
&self.key
}
pub fn value(&self) -> &str {
&self.value
}
}
impl TryFrom<RawOption> for AttrOption {
type Error = Box<dyn std::error::Error>;
fn try_from(option: RawOption) -> Result<Self, Self::Error> {
AttrOption::from_pair(&option.key, &option.value)
}
}
impl From<AttrOption> for RawOption {
fn from(option: AttrOption) -> Self {
RawOption::new(option.key(), option.value())
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::prelude::DataModel;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_from_pair_basic() {
let cases = vec![
("minimum", "10.5", AttrOption::MinimumValue(10.5)),
("maximum", "100.0", AttrOption::MaximumValue(100.0)),
("minitems", "5", AttrOption::MinItems(5)),
("maxitems", "10", AttrOption::MaxItems(10)),
("minlength", "3", AttrOption::MinLength(3)),
("maxlength", "20", AttrOption::MaxLength(20)),
(
"pattern",
"^[a-z]+$",
AttrOption::Pattern("^[a-z]+$".to_string()),
),
(
"regex",
"^[a-z]+$",
AttrOption::Pattern("^[a-z]+$".to_string()),
),
("unique", "true", AttrOption::Unique(true)),
("multipleof", "3", AttrOption::MultipleOf(3)),
("exclusiveminimum", "0.5", AttrOption::ExclusiveMinimum(0.5)),
(
"exclusivemaximum",
"99.9",
AttrOption::ExclusiveMaximum(99.9),
),
("pk", "true", AttrOption::PrimaryKey(true)),
("readonly", "false", AttrOption::ReadOnly(false)),
("recommended", "true", AttrOption::Recommended(true)),
];
for (key, value, expected) in cases {
let result = AttrOption::from_pair(key, value).unwrap();
assert_eq!(result, expected);
}
}
#[test]
fn test_from_pair_other() {
let result = AttrOption::from_pair("custom_option", "value").unwrap();
assert_eq!(
result,
AttrOption::Other {
key: "custom_option".to_string(),
value: "value".to_string()
}
);
}
#[test]
fn test_from_pair_invalid_values() {
assert!(AttrOption::from_pair("minimum", "not_a_number").is_err());
assert!(AttrOption::from_pair("minitems", "-1").is_err());
assert!(AttrOption::from_pair("multipleof", "3.5").is_err());
assert!(AttrOption::from_pair("unique", "not_a_bool").is_err());
assert!(AttrOption::from_pair("pk", "invalid").is_err());
}
#[test]
fn test_to_pair() {
let cases = vec![
(
AttrOption::MinimumValue(10.5),
("minimum".to_string(), "10.5".to_string()),
),
(
AttrOption::Pattern("^test$".to_string()),
("pattern".to_string(), "^test$".to_string()),
),
(
AttrOption::Other {
key: "custom".to_string(),
value: "test".to_string(),
},
("custom".to_string(), "test".to_string()),
),
];
for (option, expected) in cases {
assert_eq!(option.to_pair(), expected);
}
}
#[test]
fn test_raw_option_conversion() {
let raw = RawOption::new("minimum".to_string(), "10.5".to_string());
let attr: AttrOption = raw.try_into().unwrap();
assert_eq!(attr, AttrOption::MinimumValue(10.5));
let raw_back: RawOption = attr.into();
assert_eq!(raw_back.key(), "minimum");
assert_eq!(raw_back.value(), "10.5");
}
#[test]
fn test_raw_option_case_sensitivity() {
let raw = RawOption::new("MINIMUM".to_string(), "10.5".to_string());
let attr: AttrOption = raw.try_into().unwrap();
assert_eq!(attr, AttrOption::MinimumValue(10.5));
}
#[test]
fn test_raw_option_serialize() {
let raw = RawOption::new("minimum".to_string(), "10.5".to_string());
let serialized = serde_json::to_string(&raw).unwrap();
assert_eq!(serialized, r#"{"key":"minimum","value":"10.5"}"#);
}
#[test]
fn test_raw_option_deserialize() {
let serialized = r#"{"key":"minimum","value":"10.5"}"#;
let deserialized: RawOption = serde_json::from_str(serialized).unwrap();
assert_eq!(deserialized.key(), "minimum");
assert_eq!(deserialized.value(), "10.5");
}
#[test]
fn test_attr_option_from_str() {
let path = PathBuf::from("tests/data/model_options.md");
let model = DataModel::from_markdown(&path).expect("Failed to parse markdown file");
let attr = model.objects.first().unwrap();
let attribute = attr.attributes.first().unwrap();
let options = attribute
.options
.iter()
.map(|o| o.key())
.collect::<Vec<_>>();
let expected = vec![
"minimum",
"maximum",
"minitems",
"maxitems",
"minlength",
"maxlength",
"pattern",
"unique",
"multipleof",
"exclusiveminimum",
"exclusivemaximum",
"primarykey",
"readonly",
"recommended",
];
let mut missing = Vec::new();
for expected_option in expected {
if !options.contains(&expected_option.to_string()) {
missing.push(expected_option);
}
}
assert!(
missing.is_empty(),
"Expected options \n[{}]\nnot found in \n[{}]",
missing.join(", "),
options.join(", ")
);
let expected_options = vec![
AttrOption::Example("test".to_string()),
AttrOption::MinimumValue(0.0),
AttrOption::MaximumValue(100.0),
AttrOption::MinItems(1),
AttrOption::MaxItems(10),
AttrOption::MinLength(1),
AttrOption::MaxLength(100),
AttrOption::Pattern("^[a-zA-Z0-9]+$".to_string()),
AttrOption::Pattern("^[a-zA-Z0-9]+$".to_string()),
AttrOption::Unique(true),
AttrOption::MultipleOf(2),
AttrOption::ExclusiveMinimum(0.0),
AttrOption::ExclusiveMaximum(100.0),
AttrOption::PrimaryKey(true),
AttrOption::ReadOnly(true),
AttrOption::Recommended(true),
];
for expected_option in expected_options.iter() {
for option in attribute.options.iter() {
if option.key() == expected_option.key() {
assert_eq!(option, expected_option);
}
}
}
}
}