Skip to main content

mdmodels_core/
object.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::{attribute::Attribute, markdown::position::Position};
25use convert_case::{Case, Casing};
26use serde::{Deserialize, Serialize};
27use serde_with::skip_serializing_none;
28use std::collections::BTreeMap;
29use std::hash::{Hash, Hasher};
30
31#[cfg(feature = "python")]
32use pyo3::pyclass;
33
34#[cfg(feature = "wasm")]
35use tsify_next::Tsify;
36
37#[skip_serializing_none]
38#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
39#[cfg_attr(feature = "python", pyclass(get_all, from_py_object))]
40#[cfg_attr(feature = "wasm", derive(Tsify))]
41#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
42/// Represents an object with a name, attributes, docstring, and an optional term.
43pub struct Object {
44    /// Name of the object.
45    pub name: String,
46    /// List of attributes associated with the object.
47    pub attributes: Vec<Attribute>,
48    /// Documentation string for the object.
49    pub docstring: String,
50    /// Optional term associated with the object.
51    pub term: Option<String>,
52    /// Other objects that this object gets mixed in with.
53    #[serde(default, skip_serializing_if = "Vec::is_empty")]
54    pub mixins: Vec<String>,
55    /// The line number of the object
56    pub position: Option<Position>,
57}
58
59impl Object {
60    /// Creates a new `Object` with the given name and optional term.
61    ///
62    /// # Arguments
63    ///
64    /// * `name` - A string representing the name of the object.
65    /// * `term` - An optional string representing a term associated with the object.
66    ///
67    /// # Returns
68    ///
69    /// * `Object` - A new instance of the `Object` struct.
70    pub fn new(name: String, term: Option<String>) -> Self {
71        let name = name.replace(" ", "_").to_case(Case::Pascal);
72        Object {
73            name,
74            attributes: Vec::new(),
75            docstring: String::new(),
76            term,
77            mixins: Vec::new(),
78            position: None,
79        }
80    }
81
82    /// Adds an attribute to the object.
83    ///
84    /// # Arguments
85    ///
86    /// * `attribute` - An instance of `Attribute` to be added to the object's attributes.
87    pub fn add_attribute(&mut self, attribute: Attribute) {
88        self.attributes.push(attribute);
89    }
90
91    /// Sets the docstring for the object.
92    ///
93    /// # Arguments
94    ///
95    /// * `docstring` - A string representing the documentation string for the object.
96    pub fn set_docstring(&mut self, docstring: String) {
97        self.docstring = docstring;
98    }
99
100    /// Sets the line number of the object.
101    ///
102    /// # Arguments
103    ///
104    /// * `position` - The position to set.
105    pub fn set_position(&mut self, position: Position) {
106        self.position = Some(position);
107    }
108
109    /// Retrieves the last attribute added to the object.
110    ///
111    /// # Returns
112    ///
113    /// * `&mut Attribute` - A mutable reference to the last attribute.
114    ///
115    /// # Panics
116    ///
117    /// This function will panic if there are no attributes in the object.
118    pub fn get_last_attribute(&mut self) -> Option<&mut Attribute> {
119        self.attributes.last_mut()
120    }
121
122    /// Creates and adds a new attribute to the object.
123    ///
124    /// # Arguments
125    ///
126    /// * `name` - A string representing the name of the attribute.
127    /// * `required` - A boolean indicating whether the attribute is required.
128    pub fn create_new_attribute(&mut self, name: String, required: bool) {
129        let attribute = Attribute::new(name, required);
130        self.attributes.push(attribute);
131    }
132
133    /// Checks if the object has any attributes.
134    ///
135    /// # Returns
136    ///
137    /// * `bool` - `true` if the object has attributes, `false` otherwise.
138    pub fn has_attributes(&self) -> bool {
139        !self.attributes.is_empty()
140    }
141
142    /// Sets the name of the object.
143    ///
144    /// # Arguments
145    ///
146    /// * `name` - A string representing the new name of the object.
147    pub fn set_name(&mut self, name: String) {
148        self.name = name;
149    }
150
151    /// Checks if any attribute of the object has a term.
152    ///
153    /// # Returns
154    ///
155    /// * `bool` - `true` if any attribute has a term, `false` otherwise.
156    pub fn has_any_terms(&self) -> bool {
157        self.attributes.iter().any(|attr| attr.has_term())
158    }
159
160    /// Sorts the attributes of the object by their `required` field in descending order.
161    pub fn sort_attrs_by_required(&mut self) {
162        let mut top_elements: Vec<Attribute> = vec![];
163        let mut bottom_elements: Vec<Attribute> = vec![];
164
165        for attr in self.attributes.iter() {
166            if attr.required && attr.default.is_none() && !attr.is_array {
167                top_elements.push(attr.clone());
168            } else {
169                bottom_elements.push(attr.clone());
170            }
171        }
172
173        self.attributes = top_elements;
174        self.attributes.append(&mut bottom_elements);
175    }
176
177    /// Checks if this object has the same hash as another object.
178    ///
179    /// # Arguments
180    ///
181    /// * `other` - Another `Object` to compare hashes with.
182    ///
183    /// # Returns
184    ///
185    /// * `bool` - `true` if both objects have the same hash, `false` otherwise.
186    pub(crate) fn same_hash(&self, other: &Object) -> bool {
187        use std::collections::hash_map::DefaultHasher;
188        use std::hash::{Hash, Hasher};
189
190        let mut hasher1 = DefaultHasher::new();
191        let mut hasher2 = DefaultHasher::new();
192        self.hash(&mut hasher1);
193        other.hash(&mut hasher2);
194        hasher1.finish() == hasher2.finish()
195    }
196}
197
198impl Hash for Object {
199    fn hash<H: Hasher>(&self, state: &mut H) {
200        let mut attr_names: Vec<&String> = self.attributes.iter().map(|attr| &attr.name).collect();
201        attr_names.sort();
202        attr_names.hash(state);
203    }
204}
205
206#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
207#[cfg_attr(feature = "python", pyclass(get_all, from_py_object))]
208#[cfg_attr(feature = "wasm", derive(Tsify))]
209#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
210/// Represents an enumeration with a name and mappings.
211pub struct Enumeration {
212    /// Name of the enumeration.
213    pub name: String,
214    /// Mappings associated with the enumeration.
215    pub mappings: BTreeMap<String, String>,
216    /// Documentation string for the enumeration.
217    pub docstring: String,
218    /// The line number of the enumeration
219    pub position: Option<Position>,
220}
221
222impl Enumeration {
223    /// Checks if the enumeration has any values.
224    ///
225    /// # Returns
226    ///
227    /// * `bool` - `true` if the enumeration has values, `false` otherwise.
228    pub fn has_values(&self) -> bool {
229        !self.mappings.is_empty()
230    }
231
232    /// Sets the position of the enumeration.
233    ///
234    /// # Arguments
235    ///
236    /// * `position` - The position to set.
237    pub fn set_position(&mut self, position: Position) {
238        self.position = Some(position);
239    }
240
241    /// Checks if this enumeration has the same hash as another enumeration.
242    ///
243    /// # Arguments
244    ///
245    /// * `other` - Another `Enumeration` to compare hashes with.
246    ///
247    /// # Returns
248    ///
249    /// * `bool` - `true` if both enumerations have the same hash, `false` otherwise.
250    pub(crate) fn same_hash(&self, other: &Enumeration) -> bool {
251        use std::collections::hash_map::DefaultHasher;
252        use std::hash::{Hash, Hasher};
253
254        let mut hasher1 = DefaultHasher::new();
255        let mut hasher2 = DefaultHasher::new();
256        self.hash(&mut hasher1);
257        other.hash(&mut hasher2);
258        hasher1.finish() == hasher2.finish()
259    }
260}
261
262impl Hash for Enumeration {
263    fn hash<H: Hasher>(&self, state: &mut H) {
264        let mut keys: Vec<&String> = self.mappings.keys().collect();
265        keys.sort();
266        keys.hash(state);
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use pretty_assertions::assert_eq;
274    use std::collections::hash_map::DefaultHasher;
275    use std::hash::{Hash, Hasher};
276
277    #[test]
278    fn test_create_new_object() {
279        let object = Object::new("Person".to_string(), None);
280        assert_eq!(object.name, "Person");
281        assert_eq!(object.attributes.len(), 0);
282        assert_eq!(object.docstring, "");
283        assert_eq!(object.term, None);
284    }
285
286    #[test]
287    fn test_add_attribute() {
288        let mut object = Object::new("Person".to_string(), None);
289        let attribute = Attribute::new("name".to_string(), false);
290        object.add_attribute(attribute);
291        assert_eq!(object.attributes.len(), 1);
292        assert_eq!(object.attributes[0].name, "name");
293    }
294
295    #[test]
296    fn test_set_docstring() {
297        let mut object = Object::new("Person".to_string(), None);
298        object.set_docstring("This is a person object".to_string());
299        assert_eq!(object.docstring, "This is a person object");
300    }
301
302    #[test]
303    fn test_get_last_attribute() {
304        let mut object = Object::new("Person".to_string(), None);
305        let attribute = Attribute::new("name".to_string(), false);
306        object.add_attribute(attribute);
307        let last_attribute = object.get_last_attribute();
308        assert_eq!(last_attribute.unwrap().name, "name");
309    }
310
311    #[test]
312    fn test_create_new_attribute() {
313        let mut object = Object::new("Person".to_string(), None);
314        object.create_new_attribute("name".to_string(), false);
315        assert_eq!(object.attributes.len(), 1);
316        assert_eq!(object.attributes[0].name, "name");
317    }
318
319    fn hash<T: Hash>(t: &T) -> u64 {
320        let mut s = DefaultHasher::new();
321        t.hash(&mut s);
322        s.finish()
323    }
324
325    #[test]
326    fn test_object_hash_identical() {
327        let mut object1 = Object::new("Person".to_string(), None);
328        object1.create_new_attribute("name".to_string(), false);
329        object1.create_new_attribute("age".to_string(), false);
330
331        let mut object2 = Object::new("Person".to_string(), None);
332        // Add attributes in different order - should still hash the same
333        object2.create_new_attribute("age".to_string(), false);
334        object2.create_new_attribute("name".to_string(), false);
335
336        assert_eq!(hash(&object1), hash(&object2));
337    }
338
339    #[test]
340    fn test_object_hash_different() {
341        let mut object1 = Object::new("Person".to_string(), None);
342        object1.create_new_attribute("name".to_string(), false);
343        object1.create_new_attribute("age".to_string(), false);
344
345        let mut object2 = Object::new("Person".to_string(), None);
346        object2.create_new_attribute("name".to_string(), false);
347        object2.create_new_attribute("email".to_string(), false);
348
349        assert_ne!(hash(&object1), hash(&object2));
350    }
351
352    #[test]
353    fn test_enumeration_hash_identical() {
354        let mut enum1 = Enumeration::default();
355        enum1
356            .mappings
357            .insert("active".to_string(), "Active".to_string());
358        enum1
359            .mappings
360            .insert("inactive".to_string(), "Inactive".to_string());
361
362        let mut enum2 = Enumeration::default();
363        // Add mappings in different order - should still hash the same
364        enum2
365            .mappings
366            .insert("inactive".to_string(), "Inactive".to_string());
367        enum2
368            .mappings
369            .insert("active".to_string(), "Active".to_string());
370
371        assert_eq!(hash(&enum1), hash(&enum2));
372    }
373
374    #[test]
375    fn test_enumeration_hash_different() {
376        let mut enum1 = Enumeration::default();
377        enum1
378            .mappings
379            .insert("active".to_string(), "Active".to_string());
380        enum1
381            .mappings
382            .insert("inactive".to_string(), "Inactive".to_string());
383
384        let mut enum2 = Enumeration::default();
385        enum2
386            .mappings
387            .insert("pending".to_string(), "Pending".to_string());
388        enum2
389            .mappings
390            .insert("active".to_string(), "Active".to_string());
391
392        assert_ne!(hash(&enum1), hash(&enum2));
393    }
394
395    #[test]
396    fn test_object_hash_reference_identical() {
397        let mut object1 = Object::new("Person".to_string(), None);
398        object1.create_new_attribute("name".to_string(), false);
399        object1.create_new_attribute("age".to_string(), false);
400
401        let mut object2 = Object::new("Person".to_string(), None);
402        object2.create_new_attribute("age".to_string(), false);
403        object2.create_new_attribute("name".to_string(), false);
404
405        let ref1: &Object = &object1;
406        let ref2: &Object = &object2;
407
408        assert_eq!(hash(ref1), hash(ref2));
409        assert_eq!(hash(&object1), hash(ref1));
410    }
411
412    #[test]
413    fn test_object_hash_reference_different() {
414        let mut object1 = Object::new("Person".to_string(), None);
415        object1.create_new_attribute("name".to_string(), false);
416        object1.create_new_attribute("age".to_string(), false);
417
418        let mut object2 = Object::new("Person".to_string(), None);
419        object2.create_new_attribute("name".to_string(), false);
420        object2.create_new_attribute("email".to_string(), false);
421
422        let ref1: &Object = &object1;
423        let ref2: &Object = &object2;
424
425        assert_ne!(hash(ref1), hash(ref2));
426        assert_eq!(hash(&object1), hash(ref1));
427    }
428
429    #[test]
430    fn test_enumeration_hash_reference_identical() {
431        let mut enum1 = Enumeration::default();
432        enum1
433            .mappings
434            .insert("active".to_string(), "Active".to_string());
435        enum1
436            .mappings
437            .insert("inactive".to_string(), "Inactive".to_string());
438
439        let mut enum2 = Enumeration::default();
440        enum2
441            .mappings
442            .insert("inactive".to_string(), "Inactive".to_string());
443        enum2
444            .mappings
445            .insert("active".to_string(), "Active".to_string());
446
447        let ref1: &Enumeration = &enum1;
448        let ref2: &Enumeration = &enum2;
449
450        assert_eq!(hash(ref1), hash(ref2));
451        assert_eq!(hash(&enum1), hash(ref1));
452    }
453
454    #[test]
455    fn test_enumeration_hash_reference_different() {
456        let mut enum1 = Enumeration::default();
457        enum1
458            .mappings
459            .insert("active".to_string(), "Active".to_string());
460        enum1
461            .mappings
462            .insert("inactive".to_string(), "Inactive".to_string());
463
464        let mut enum2 = Enumeration::default();
465        enum2
466            .mappings
467            .insert("pending".to_string(), "Pending".to_string());
468        enum2
469            .mappings
470            .insert("active".to_string(), "Active".to_string());
471
472        let ref1: &Enumeration = &enum1;
473        let ref2: &Enumeration = &enum2;
474
475        assert_ne!(hash(ref1), hash(ref2));
476        assert_eq!(hash(&enum1), hash(ref1));
477    }
478
479    #[test]
480    fn test_object_has_same_hash_identical() {
481        let mut object1 = Object::new("Person".to_string(), None);
482        object1.create_new_attribute("name".to_string(), false);
483        object1.create_new_attribute("age".to_string(), false);
484
485        let mut object2 = Object::new("Person".to_string(), None);
486        // Add attributes in different order - should still hash the same
487        object2.create_new_attribute("age".to_string(), false);
488        object2.create_new_attribute("name".to_string(), false);
489
490        assert!(object1.same_hash(&object2));
491    }
492
493    #[test]
494    fn test_object_has_same_hash_different() {
495        let mut object1 = Object::new("Person".to_string(), None);
496        object1.create_new_attribute("name".to_string(), false);
497        object1.create_new_attribute("age".to_string(), false);
498
499        let mut object2 = Object::new("Person".to_string(), None);
500        object2.create_new_attribute("name".to_string(), false);
501        object2.create_new_attribute("email".to_string(), false);
502
503        assert!(!object1.same_hash(&object2));
504    }
505
506    #[test]
507    fn test_enumeration_has_same_hash_identical() {
508        let mut enum1 = Enumeration::default();
509        enum1
510            .mappings
511            .insert("active".to_string(), "Active".to_string());
512        enum1
513            .mappings
514            .insert("inactive".to_string(), "Inactive".to_string());
515
516        let mut enum2 = Enumeration::default();
517        // Add mappings in different order - should still hash the same
518        enum2
519            .mappings
520            .insert("inactive".to_string(), "Inactive".to_string());
521        enum2
522            .mappings
523            .insert("active".to_string(), "Active".to_string());
524
525        assert!(enum1.same_hash(&enum2));
526    }
527
528    #[test]
529    fn test_enumeration_has_same_hash_different() {
530        let mut enum1 = Enumeration::default();
531        enum1
532            .mappings
533            .insert("active".to_string(), "Active".to_string());
534        enum1
535            .mappings
536            .insert("inactive".to_string(), "Inactive".to_string());
537
538        let mut enum2 = Enumeration::default();
539        enum2
540            .mappings
541            .insert("pending".to_string(), "Pending".to_string());
542        enum2
543            .mappings
544            .insert("active".to_string(), "Active".to_string());
545
546        assert!(!enum1.same_hash(&enum2));
547    }
548}