Skip to main content

mdmodels_core/
attribute.rs

1/*
2 * Copyright (c) 2025 Jan Range
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 *
22 */
23
24use crate::{
25    markdown::{parser::OptionKey, position::Position},
26    option::{AttrOption, RawOption},
27    validation::BASIC_TYPES,
28    xmltype::XMLType,
29};
30use convert_case::{Case, Casing};
31use serde::{de::Visitor, Deserialize, Serialize};
32use std::{error::Error, fmt, str::FromStr};
33
34#[cfg(feature = "python")]
35use pyo3::{pyclass, pymethods};
36
37#[cfg(feature = "wasm")]
38use tsify_next::Tsify;
39
40/// Represents an attribute with various properties and options.
41#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
42#[cfg_attr(feature = "python", pyclass(get_all, from_py_object))]
43#[cfg_attr(feature = "wasm", derive(Tsify))]
44#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
45pub struct Attribute {
46    /// The name of the attribute.
47    pub name: String,
48    /// Indicates if the attribute is an array.
49    #[serde(rename = "multiple")]
50    pub is_array: bool,
51    /// Is an identifier or not
52    pub is_id: bool,
53    /// Data types associated with the attribute.
54    pub dtypes: Vec<String>,
55    /// Documentation string for the attribute.
56    pub docstring: String,
57    /// List of additional options for the attribute.
58    pub options: Vec<AttrOption>,
59    /// Term associated with the attribute, if any.
60    pub term: Option<String>,
61    /// Indicates if the attribute is required.
62    pub required: bool,
63    /// Default value for the attribute.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub default: Option<DataType>,
66    /// XML type information for the attribute.
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub xml: Option<XMLType>,
69    /// Is an enumeration or not
70    pub is_enum: bool,
71    /// The line number of the attribute
72    pub position: Option<Position>,
73    /// The prefix of the attribute, if it is an import
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub import_prefix: Option<String>,
76}
77
78impl Attribute {
79    /// Creates a new `Attribute` with the given name and required status.
80    ///
81    /// # Arguments
82    ///
83    /// * `name` - The name of the attribute.
84    /// * `required` - Indicates if the attribute is required.
85    pub fn new(name: String, required: bool) -> Self {
86        Attribute {
87            name: name.clone().replace(" ", "_"),
88            dtypes: Vec::new(),
89            docstring: String::new(),
90            options: Vec::new(),
91            is_array: false,
92            is_id: false,
93            term: None,
94            required,
95            xml: Some(XMLType::from_str(name.as_str()).unwrap()),
96            default: None,
97            is_enum: false,
98            position: None,
99            import_prefix: None,
100        }
101    }
102
103    /// Sets the documentation string for the attribute.
104    ///
105    /// # Arguments
106    ///
107    /// * `docstring` - The documentation string to set.
108    pub fn set_docstring(&mut self, docstring: String) {
109        self.docstring = docstring;
110    }
111
112    /// Sets the line number of the attribute.
113    ///
114    /// # Arguments
115    ///
116    /// * `position` - The position to set.
117    pub fn set_position(&mut self, position: Position) {
118        self.position = Some(position);
119    }
120
121    /// Adds an option to the attribute.
122    ///
123    /// # Arguments
124    ///
125    /// * `option` - The option to add.
126    pub fn add_option(&mut self, option: RawOption) -> Result<(), Box<dyn Error>> {
127        match OptionKey::from_str(option.key.as_str()) {
128            OptionKey::Type => self.set_dtype(option.value)?,
129            OptionKey::Term => self.term = Some(option.value),
130            OptionKey::Description => self.docstring = option.value,
131            OptionKey::Default => self.default = Some(DataType::from_str(&option.value)?),
132            OptionKey::Multiple => self.is_array = option.value.to_lowercase() == "true",
133            OptionKey::Other => self.options.push(option.try_into()?),
134            OptionKey::Xml => {
135                self.set_xml(XMLType::from_str(&option.value).expect("Invalid XML type"))
136            }
137        }
138
139        Ok(())
140    }
141
142    /// Sets the data type for the attribute.
143    ///
144    /// # Arguments
145    ///
146    /// * `dtype` - The data type to set.
147    pub(crate) fn set_dtype(&mut self, dtype: String) -> Result<(), Box<dyn Error>> {
148        let dtypes = self.break_up_dtypes(&dtype);
149
150        self.validate_dtypes(&dtypes)?;
151
152        let mut new_dtypes = Vec::new();
153
154        for mut dtype in dtypes {
155            dtype = dtype.trim().to_string();
156            if self.is_identifier(&dtype) {
157                dtype = self.process_identifier(&dtype).to_string();
158            }
159
160            if let Some((prefix, name)) = dtype.split_once('.') {
161                self.import_prefix = Some(prefix.to_string());
162                dtype = name.to_string();
163            }
164
165            if dtype.ends_with("[]") {
166                self.is_array = true;
167                dtype = dtype.trim_end_matches("[]").to_string();
168            }
169
170            if !BASIC_TYPES.contains(&dtype.as_str()) {
171                dtype = dtype.replace(" ", "_").to_case(Case::Pascal);
172            }
173
174            new_dtypes.push(dtype);
175        }
176
177        self.dtypes = new_dtypes;
178
179        Ok(())
180    }
181
182    /// Splits a data type string into a vector of strings based on commas.
183    ///
184    /// # Arguments
185    ///
186    /// * `dtype` - A string representing the data types, separated by commas.
187    ///
188    /// # Returns
189    ///
190    /// A vector of strings, each representing a separate data type.
191    fn break_up_dtypes(&self, dtype: &str) -> Vec<String> {
192        dtype.split(",").map(|s| s.to_string()).collect()
193    }
194
195    /// Validates a vector of data type strings to ensure consistency in array notation.
196    ///
197    /// # Arguments
198    ///
199    /// * `dtypes` - A reference to a vector of strings representing data types.
200    ///
201    /// # Returns
202    ///
203    /// A `Result` indicating success or an error if the validation fails.
204    fn validate_dtypes(&self, dtypes: &[String]) -> Result<(), Box<dyn Error>> {
205        let has_multiple_dtypes = dtypes.len() > 1;
206        let contains_array_dtype = dtypes.iter().any(|dtype| dtype.ends_with("[]"));
207
208        if has_multiple_dtypes && contains_array_dtype {
209            return Err(
210                "If more than one dtype is provided, none can be array valued by []. \
211                Use the keyword 'Multiple' instead."
212                    .into(),
213            );
214        }
215
216        Ok(())
217    }
218
219    /// Checks if a data type string represents an identifier.
220    ///
221    /// # Arguments
222    ///
223    /// * `dtype` - A string representing a data type.
224    ///
225    /// # Returns
226    ///
227    /// `true` if the data type is an identifier, `false` otherwise.
228    fn is_identifier(&self, dtype: &str) -> bool {
229        dtype.to_lowercase().starts_with("identifier")
230    }
231
232    /// Processes a data type string to replace 'identifier' with 'string'.
233    ///
234    /// # Arguments
235    ///
236    /// * `dtype` - A string representing a data type.
237    ///
238    /// # Returns
239    ///
240    /// A new string with 'identifier' replaced by 'string'.
241    fn process_identifier(&mut self, dtype: &str) -> String {
242        self.is_id = true;
243        // Regex replace identifier or Identifier with string
244        let pattern = regex::Regex::new(r"[I|i]dentifier").unwrap();
245        pattern.replace_all(dtype, "string").to_string()
246    }
247
248    /// Converts the attribute to a JSON schema.
249    ///
250    /// # Returns
251    ///
252    /// A JSON string representing the attribute schema.
253    pub fn to_json_schema(&self) -> String {
254        serde_json::to_string_pretty(&self).unwrap()
255    }
256
257    /// Checks if the attribute has an associated term.
258    ///
259    /// # Returns
260    ///
261    /// `true` if the attribute has a term, `false` otherwise.
262    pub fn has_term(&self) -> bool {
263        self.term.is_some()
264    }
265
266    /// Sets the XML type for the attribute.
267    ///
268    /// # Arguments
269    ///
270    /// * `xml` - The XML type to set.
271    pub fn set_xml(&mut self, xml: XMLType) {
272        self.xml = Some(xml);
273    }
274}
275
276#[derive(Debug, Clone)]
277#[cfg_attr(feature = "python", pyclass(from_py_object))]
278#[cfg_attr(feature = "wasm", derive(Tsify))]
279#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
280pub enum DataType {
281    Boolean(bool),
282    Integer(i64),
283    Float(f64),
284    String(String),
285}
286
287#[cfg_attr(feature = "python", pymethods)]
288impl DataType {
289    pub fn is_boolean(&self) -> bool {
290        matches!(self, DataType::Boolean(_))
291    }
292
293    pub fn is_integer(&self) -> bool {
294        matches!(self, DataType::Integer(_))
295    }
296
297    pub fn is_float(&self) -> bool {
298        matches!(self, DataType::Float(_))
299    }
300
301    pub fn is_string(&self) -> bool {
302        matches!(self, DataType::String(_))
303    }
304
305    pub fn as_boolean(&self) -> Option<bool> {
306        if let DataType::Boolean(value) = self {
307            Some(*value)
308        } else {
309            None
310        }
311    }
312
313    pub fn as_integer(&self) -> Option<i64> {
314        if let DataType::Integer(value) = self {
315            Some(*value)
316        } else {
317            None
318        }
319    }
320
321    pub fn as_float(&self) -> Option<f64> {
322        if let DataType::Float(value) = self {
323            Some(*value)
324        } else {
325            None
326        }
327    }
328
329    pub fn as_string(&self) -> Option<String> {
330        if let DataType::String(value) = self {
331            Some(value.clone())
332        } else {
333            None
334        }
335    }
336}
337
338impl PartialEq for DataType {
339    fn eq(&self, other: &Self) -> bool {
340        match (self, other) {
341            (DataType::Boolean(a), DataType::Boolean(b)) => a == b,
342            (DataType::Integer(a), DataType::Integer(b)) => a == b,
343            (DataType::Float(a), DataType::Float(b)) => a == b,
344            (DataType::String(a), DataType::String(b)) => a == b,
345            _ => false,
346        }
347    }
348}
349
350impl FromStr for DataType {
351    type Err = String;
352
353    /// Converts a string to a DataType (Boolean, Integer, Float, or String).
354    fn from_str(s: &str) -> Result<Self, Self::Err> {
355        let lower_s = s.to_lowercase();
356        let lower_s = lower_s.trim_matches('"');
357
358        if let Ok(b) = lower_s.parse::<bool>() {
359            Ok(DataType::Boolean(b))
360        } else if let Ok(i) = lower_s.parse::<i64>() {
361            Ok(DataType::Integer(i))
362        } else if let Ok(f) = lower_s.parse::<f64>() {
363            Ok(DataType::Float(f))
364        } else if !lower_s.is_empty() {
365            Ok(DataType::String(format!("\"{s}\"")))
366        } else {
367            Err("Invalid data type".to_string())
368        }
369    }
370}
371
372impl Serialize for DataType {
373    /// Serializes a DataType to a string.
374    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
375    where
376        S: serde::Serializer,
377    {
378        match self {
379            DataType::Boolean(b) => serializer.serialize_bool(*b),
380            DataType::Integer(i) => serializer.serialize_i64(*i),
381            DataType::Float(f) => serializer.serialize_f64(*f),
382            DataType::String(s) => serializer.serialize_str(s),
383        }
384    }
385}
386
387#[allow(clippy::needless_lifetimes)]
388impl<'de> Deserialize<'de> for DataType {
389    /// Deserializes a DataType from a string.
390    fn deserialize<D>(deserializer: D) -> Result<DataType, D::Error>
391    where
392        D: serde::Deserializer<'de>,
393    {
394        struct DataTypeVisitor;
395        impl<'de> Visitor<'de> for DataTypeVisitor {
396            type Value = DataType;
397
398            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
399                formatter.write_str("a boolean, integer, float, or string")
400            }
401
402            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E> {
403                Ok(DataType::Boolean(v))
404            }
405
406            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> {
407                Ok(DataType::Integer(v))
408            }
409
410            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
411                Ok(DataType::Integer(v as i64))
412            }
413
414            fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E> {
415                Ok(DataType::Float(v))
416            }
417
418            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> {
419                Ok(DataType::String(v.to_string()))
420            }
421        }
422
423        deserializer.deserialize_any(DataTypeVisitor)
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use crate::xmltype::XMLType;
430    use pretty_assertions::assert_eq;
431
432    use super::*;
433
434    #[test]
435    fn test_attribute_new() {
436        let attr = Attribute::new("name".to_string(), false);
437        assert_eq!(attr.name, "name");
438        assert_eq!(attr.dtypes.len(), 0);
439        assert_eq!(attr.docstring, "");
440        assert_eq!(attr.options.len(), 0);
441        assert_eq!(attr.is_array, false);
442        assert_eq!(attr.term, None);
443        assert_eq!(attr.required, false);
444    }
445
446    #[test]
447    fn test_attribute_set_docstring() {
448        let mut attr = Attribute::new("name".to_string(), false);
449        attr.set_docstring("This is a test".to_string());
450        assert_eq!(attr.docstring, "This is a test");
451        assert_eq!(attr.required, false);
452    }
453
454    #[test]
455    fn test_attribute_add_type_option() {
456        let mut attr = Attribute::new("name".to_string(), false);
457        let option = RawOption::new("type".to_string(), "string".to_string());
458        attr.add_option(option).expect("Failed to add option");
459        assert_eq!(attr.dtypes.len(), 1);
460        assert_eq!(attr.dtypes[0], "string");
461    }
462
463    #[test]
464    fn test_attribute_add_term_option() {
465        let mut attr = Attribute::new("name".to_string(), false);
466        let option = RawOption::new("term".to_string(), "string".to_string());
467        attr.add_option(option).expect("Failed to add option");
468        assert_eq!(attr.term, Some("string".to_string()));
469    }
470
471    #[test]
472    fn test_attribute_add_option() {
473        let mut attr = Attribute::new("name".to_string(), false);
474        let option = RawOption::new("description".to_string(), "This is a test".to_string());
475        attr.add_option(option).expect("Failed to add option");
476        let option = RawOption::new("something".to_string(), "something".to_string());
477        attr.add_option(option).expect("Failed to add option");
478
479        assert_eq!(attr.options.len(), 1);
480
481        if let Some(option) = attr.options.first() {
482            if let AttrOption::Other { key, value } = option {
483                assert_eq!(key, "something");
484                assert_eq!(value, "something");
485            } else {
486                panic!("Option is not an AttributeOption::Other");
487            }
488        } else {
489            panic!("Option not found");
490        }
491
492        assert_eq!(attr.docstring, "This is a test");
493    }
494
495    #[test]
496    fn test_attribute_set_dtype() {
497        let mut attr = Attribute::new("name".to_string(), false);
498        attr.set_dtype("string".to_string())
499            .expect("Failed to set dtype");
500        assert_eq!(attr.dtypes.len(), 1);
501        assert_eq!(attr.dtypes[0], "string");
502        assert_eq!(attr.is_array, false);
503    }
504
505    #[test]
506    fn test_attribute_set_array_dtype() {
507        let mut attr = Attribute::new("name".to_string(), false);
508        attr.set_dtype("string[]".to_string())
509            .expect("Failed to set dtype");
510        assert_eq!(attr.dtypes.len(), 1);
511        assert_eq!(attr.dtypes[0], "string");
512        assert_eq!(attr.is_array, true);
513    }
514
515    #[test]
516    fn test_attribute_set_xml_attr() {
517        let mut attr = Attribute::new("name".to_string(), false);
518        let xml = XMLType::from_str("@name").expect("Could not parse XMLType");
519        attr.set_xml(xml);
520        assert_eq!(
521            attr.xml.expect("Could not find XML option"),
522            XMLType::Attribute {
523                is_attr: true,
524                name: "name".to_string(),
525            },
526            "XMLType is not correct. Expected an attribute type."
527        );
528    }
529
530    #[test]
531    fn test_attribute_set_xml_element() {
532        let mut attr = Attribute::new("name".to_string(), false);
533        let xml = XMLType::from_str("name").expect("Could not parse XMLType");
534        attr.set_xml(xml);
535        assert_eq!(
536            attr.xml.expect("Could not find XML option"),
537            XMLType::Element {
538                is_attr: false,
539                name: "name".to_string(),
540            },
541            "XMLType is not correct. Expected an element type."
542        );
543    }
544
545    #[test]
546    fn test_default_xml_type() {
547        let attr = Attribute::new("name".to_string(), false);
548        assert_eq!(
549            attr.xml.unwrap(),
550            XMLType::Element {
551                is_attr: false,
552                name: "name".to_string(),
553            }
554        );
555    }
556
557    #[test]
558    fn test_serialize_data_type() {
559        // Test string
560        let dt = DataType::String("string".to_string());
561        let serialized = serde_json::to_string(&dt).expect("Failed to serialize DataType");
562        assert_eq!(serialized, "\"string\"");
563
564        // Test integer
565        let dt = DataType::Integer(1);
566        let serialized = serde_json::to_string(&dt).expect("Failed to serialize DataType");
567        assert_eq!(serialized, "1");
568
569        // Test float
570        let dt = DataType::Float(1.0);
571        let serialized = serde_json::to_string(&dt).expect("Failed to serialize DataType");
572        assert_eq!(serialized, "1.0");
573
574        // Test boolean
575        let dt = DataType::Boolean(true);
576        let serialized = serde_json::to_string(&dt).expect("Failed to serialize DataType");
577        assert_eq!(serialized, "true");
578    }
579
580    #[test]
581    fn test_deserialize_data_type() {
582        // Test string
583        let deserialized: DataType =
584            serde_json::from_str("\"string\"").expect("Failed to deserialize string DataType");
585        assert_eq!(deserialized, DataType::String("string".to_string()));
586
587        // Test integer
588        let deserialized: DataType =
589            serde_json::from_str("1").expect("Failed to deserialize integer DataType");
590        assert_eq!(deserialized, DataType::Integer(1));
591
592        // Test float
593        let deserialized: DataType =
594            serde_json::from_str("1.0").expect("Failed to deserialize float DataType");
595        assert_eq!(deserialized, DataType::Float(1.0));
596
597        // Test boolean
598        let deserialized: DataType =
599            serde_json::from_str("true").expect("Failed to deserialize bool DataType");
600        assert_eq!(deserialized, DataType::Boolean(true));
601    }
602
603    #[test]
604    fn is_boolean_returns_true_for_boolean() {
605        let dt = DataType::Boolean(true);
606        assert!(dt.is_boolean());
607    }
608
609    #[test]
610    fn is_boolean_returns_false_for_non_boolean() {
611        let dt = DataType::Integer(1);
612        assert!(!dt.is_boolean());
613    }
614
615    #[test]
616    fn is_integer_returns_true_for_integer() {
617        let dt = DataType::Integer(1);
618        assert!(dt.is_integer());
619    }
620
621    #[test]
622    fn is_integer_returns_false_for_non_integer() {
623        let dt = DataType::Boolean(true);
624        assert!(!dt.is_integer());
625    }
626
627    #[test]
628    fn is_float_returns_true_for_float() {
629        let dt = DataType::Float(1.0);
630        assert!(dt.is_float());
631    }
632
633    #[test]
634    fn is_float_returns_false_for_non_float() {
635        let dt = DataType::String("string".to_string());
636        assert!(!dt.is_float());
637    }
638
639    #[test]
640    fn is_string_returns_true_for_string() {
641        let dt = DataType::String("string".to_string());
642        assert!(dt.is_string());
643    }
644
645    #[test]
646    fn is_string_returns_false_for_non_string() {
647        let dt = DataType::Float(1.0);
648        assert!(!dt.is_string());
649    }
650
651    #[test]
652    fn as_boolean_returns_some_for_boolean() {
653        let dt = DataType::Boolean(true);
654        assert_eq!(dt.as_boolean(), Some(true));
655    }
656
657    #[test]
658    fn as_boolean_returns_none_for_non_boolean() {
659        let dt = DataType::Integer(1);
660        assert_eq!(dt.as_boolean(), None);
661    }
662
663    #[test]
664    fn as_integer_returns_some_for_integer() {
665        let dt = DataType::Integer(1);
666        assert_eq!(dt.as_integer(), Some(1));
667    }
668
669    #[test]
670    fn as_integer_returns_none_for_non_integer() {
671        let dt = DataType::Boolean(true);
672        assert_eq!(dt.as_integer(), None);
673    }
674
675    #[test]
676    fn as_float_returns_some_for_float() {
677        let dt = DataType::Float(1.0);
678        assert_eq!(dt.as_float(), Some(1.0));
679    }
680
681    #[test]
682    fn as_float_returns_none_for_non_float() {
683        let dt = DataType::String("string".to_string());
684        assert_eq!(dt.as_float(), None);
685    }
686
687    #[test]
688    fn as_string_returns_some_for_string() {
689        let dt = DataType::String("string".to_string());
690        assert_eq!(dt.as_string(), Some("string".to_string()));
691    }
692
693    #[test]
694    fn as_string_returns_none_for_non_string() {
695        let dt = DataType::Float(1.0);
696        assert_eq!(dt.as_string(), None);
697    }
698
699    #[test]
700    fn from_str_parses_boolean() {
701        let dt = DataType::from_str("true").unwrap();
702        assert_eq!(dt, DataType::Boolean(true));
703    }
704
705    #[test]
706    fn from_str_parses_integer() {
707        let dt = DataType::from_str("42").unwrap();
708        assert_eq!(dt, DataType::Integer(42));
709    }
710
711    #[test]
712    fn from_str_parses_float() {
713        let dt = DataType::from_str("3.5").unwrap();
714        assert_eq!(dt, DataType::Float(3.5));
715    }
716
717    #[test]
718    fn from_str_parses_string() {
719        let dt = DataType::from_str("hello").unwrap();
720        assert_eq!(dt, DataType::String("\"hello\"".to_string()));
721    }
722
723    #[test]
724    fn from_str_returns_error_for_invalid_data_type() {
725        let dt = DataType::from_str("");
726        assert!(dt.is_err());
727    }
728}