Skip to main content

mdmodels_core/json/
export.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 std::{
25    collections::{BTreeMap, HashMap, HashSet},
26    str::FromStr,
27};
28
29use crate::{
30    attribute::{self, Attribute},
31    datamodel::DataModel,
32    json::schema::{AnyOfItemType, DataType, DataTypeItemType, Item, ReferenceItemType},
33    markdown::frontmatter::FrontMatter,
34    object::{Enumeration, Object},
35    option::AttrOption,
36    validation::BASIC_TYPES,
37};
38
39use super::schema::{self, PrimitiveType};
40
41const SCHEMA: &str = "https://json-schema.org/draft/2020-12/schema";
42
43/// Converts a `DataModel` into a JSON schema representation.
44///
45/// # Arguments
46///
47/// * `model` - A reference to the `DataModel` to be converted.
48/// * `root` - The root object name in the model.
49/// * `openai` - A boolean flag indicating whether to use the OpenAI schema.
50///
51/// # Returns
52///
53/// A `Result` containing the `SchemaObject` or an error message.
54pub fn to_json_schema(
55    model: &DataModel,
56    root: &str,
57    openai: bool,
58) -> Result<schema::SchemaObject, String> {
59    let root_object = retrieve_object(model, root)?;
60
61    let mut schema_object = schema::SchemaObject::try_from(root_object)?;
62    let mut used_types = HashSet::new();
63    let mut used_enums = HashSet::new();
64
65    collect_definitions(root_object, model, &mut used_types, &mut used_enums)?;
66
67    let definitions = collect_definitions_from_model(model, &used_types, &used_enums)?;
68
69    schema_object.schema = Some(SCHEMA.to_string());
70    schema_object.definitions = definitions;
71
72    if let Some(config) = model.config.clone() {
73        post_process_schema(&mut schema_object, &config, openai, &used_enums)?;
74    }
75
76    Ok(schema_object)
77}
78
79/// Retrieves an object from the `DataModel` by name.
80///
81/// # Arguments
82///
83/// * `model` - A reference to the `DataModel`.
84/// * `name` - The name of the object to retrieve.
85///
86/// # Returns
87///
88/// A `Result` containing a reference to the `Object` or an error message.
89fn retrieve_object<'a>(model: &'a DataModel, name: &'a str) -> Result<&'a Object, String> {
90    model
91        .objects
92        .iter()
93        .find(|obj| obj.name == name)
94        .ok_or(format!("Object {name} not found"))
95}
96
97/// Retrieves an enumeration from the `DataModel` by name.
98///
99/// # Arguments
100///
101/// * `model` - A reference to the `DataModel`.
102/// * `name` - The name of the enumeration to retrieve.
103///
104/// # Returns
105///
106/// A `Result` containing a reference to the `EnumObject` or an error message.
107fn retrieve_enum<'a>(model: &'a DataModel, name: &'a str) -> Result<&'a Enumeration, String> {
108    model
109        .enums
110        .iter()
111        .find(|e| e.name == name)
112        .ok_or(format!("Enum {name} not found"))
113}
114
115/// Collects definitions from the `DataModel` based on used types and enums.
116///
117/// # Arguments
118///
119/// * `model` - A reference to the `DataModel`.
120/// * `used_types` - A reference to a set of used type names.
121/// * `used_enums` - A reference to a set of used enum names.
122///
123/// # Returns
124///
125/// A `Result` containing a `BTreeMap` of schema definitions or an error message.
126fn collect_definitions_from_model(
127    model: &DataModel,
128    used_types: &HashSet<String>,
129    used_enums: &HashSet<String>,
130) -> Result<BTreeMap<String, schema::SchemaType>, String> {
131    let mut definitions = BTreeMap::new();
132
133    for obj_name in used_types {
134        let obj = retrieve_object(model, obj_name)?;
135        definitions.insert(obj_name.clone(), schema::SchemaType::try_from(obj)?);
136    }
137
138    for enum_name in used_enums {
139        let enum_object = retrieve_enum(model, enum_name)?;
140        definitions.insert(
141            enum_name.clone(),
142            schema::SchemaType::try_from(enum_object)?,
143        );
144    }
145
146    Ok(definitions)
147}
148
149/// Collects definitions from an object and updates the used types and enums sets.
150///
151/// # Arguments
152///
153/// * `object` - A reference to the `Object`.
154/// * `model` - A reference to the `DataModel`.
155/// * `used_types` - A mutable reference to a set of used type names.
156/// * `used_enums` - A mutable reference to a set of used enum names.
157///
158/// # Returns
159///
160/// A `Result` indicating success or an error message.
161fn collect_definitions(
162    object: &Object,
163    model: &DataModel,
164    used_types: &mut HashSet<String>,
165    used_enums: &mut HashSet<String>,
166) -> Result<(), String> {
167    for attr in object.attributes.iter() {
168        for dtype in attr.dtypes.iter() {
169            if BASIC_TYPES.contains(&dtype.as_str()) || used_types.contains(dtype) {
170                continue;
171            }
172
173            let object = model.objects.iter().find(|obj| obj.name == *dtype);
174            let enumeration = model.enums.iter().find(|e| e.name == *dtype);
175
176            if let Some(object) = object {
177                used_types.insert(dtype.clone());
178                collect_definitions(object, model, used_types, used_enums)?;
179            } else if let Some(enumeration) = enumeration {
180                used_enums.insert(enumeration.name.clone());
181            } else {
182                return Err(format!("Object or enumeration {dtype} not found"));
183            }
184        }
185    }
186
187    Ok(())
188}
189
190/// Resolves prefixes in the schema properties using the provided prefixes map.
191///
192/// # Arguments
193///
194/// * `schema` - A mutable reference to the `SchemaObject`.
195/// * `prefixes` - A reference to a map containing prefix-to-URI mappings.
196fn resolve_prefixes(schema: &mut schema::SchemaObject, prefixes: &HashMap<String, String>) {
197    for (_, property) in schema.properties.iter_mut() {
198        if let Some(reference) = property.term.clone() {
199            let (prefix, term) = reference.split_once(":").unwrap_or(("", ""));
200            if let Some(prefix) = prefixes.get(prefix) {
201                property.term = Some(format!("{prefix}{term}"));
202            }
203        }
204    }
205}
206
207/// Post-processes the schema object by setting its ID, resolving prefixes, and optionally removing options.
208///
209/// # Arguments
210///
211/// * `schema_object` - A mutable reference to the `SchemaObject` to be post-processed.
212/// * `config` - A reference to the `FrontMatter` configuration containing repository and prefix information.
213/// * `no_options` - A boolean flag indicating whether to remove options from the schema properties.
214fn post_process_schema(
215    schema_object: &mut schema::SchemaObject,
216    config: &FrontMatter,
217    openai: bool,
218    used_enums: &HashSet<String>,
219) -> Result<(), String> {
220    schema_object.id = Some(config.repo.clone());
221    post_process_object(schema_object, config, openai, used_enums)?;
222
223    for (_, definition) in schema_object.definitions.iter_mut() {
224        if let schema::SchemaType::Object(definition) = definition {
225            post_process_object(definition, config, openai, used_enums)?;
226        }
227    }
228
229    Ok(())
230}
231
232/// Post-processes an object by resolving prefixes and removing options.
233///
234/// # Arguments
235///
236/// * `object` - A mutable reference to the `SchemaObject`.
237/// * `config` - A reference to the `FrontMatter` configuration containing repository and prefix information.
238/// * `openai` - A boolean flag indicating whether to remove options from the schema properties.
239/// * `used_enums` - A reference to a set of used enum names.
240fn post_process_object(
241    object: &mut schema::SchemaObject,
242    config: &FrontMatter,
243    openai: bool,
244    used_enums: &HashSet<String>,
245) -> Result<(), String> {
246    if let Some(prefixes) = &config.prefixes {
247        resolve_prefixes(object, prefixes);
248    }
249    if openai {
250        object.schema = None;
251        object.id = None;
252        remove_options(object);
253        set_required_and_nullable(object);
254    }
255
256    for (_, property) in object.properties.iter_mut() {
257        if let Some(reference) = &property.reference {
258            if used_enums.contains(
259                reference
260                    .split("/")
261                    .last()
262                    .ok_or(format!("Failed to split reference: {reference}"))?,
263            ) {
264                if openai {
265                    property.dtype = None;
266                } else {
267                    property.dtype = Some(schema::DataType::String);
268                }
269            }
270        }
271    }
272
273    Ok(())
274}
275
276/// Removes options from the schema properties.
277///
278/// # Arguments
279///
280/// * `schema` - A mutable reference to the `SchemaObject`.
281fn remove_options(schema: &mut schema::SchemaObject) {
282    for (_, property) in schema.properties.iter_mut() {
283        property.options = HashMap::new();
284    }
285}
286
287/// Sets the required and nullable fields in the schema object.
288///
289/// # Arguments
290///
291/// * `schema` - A mutable reference to the `SchemaObject`.
292fn set_required_and_nullable(schema: &mut schema::SchemaObject) {
293    let mut new_required = Vec::new();
294
295    for (name, property) in &mut schema.properties {
296        clean_reference_property(property);
297        convert_one_of_to_any_of(property);
298
299        if !schema.required.contains(name) {
300            new_required.push(name.clone());
301            make_property_nullable(property);
302        }
303    }
304
305    finalize_schema_requirements(schema, new_required);
306}
307
308/// Cleans up properties that have references by removing unnecessary fields.
309///
310/// # Arguments
311///
312/// * `property` - A mutable reference to the property to clean.
313fn clean_reference_property(property: &mut schema::Property) {
314    if property.reference.is_some() {
315        property.description = None;
316        property.title = None;
317        property.dtype = None;
318    }
319}
320
321/// Converts oneOf items to anyOf items in the property.
322///
323/// # Arguments
324///
325/// * `property` - A mutable reference to the property to convert.
326fn convert_one_of_to_any_of(property: &mut schema::Property) {
327    if let Some(Item::OneOfItem(one_of)) = &mut property.items {
328        property.items = Some(Item::AnyOfItem(AnyOfItemType {
329            any_of: one_of.one_of.clone(),
330        }));
331    }
332}
333
334/// Makes a property nullable by creating an anyOf structure with null as an option.
335///
336/// # Arguments
337///
338/// * `property` - A mutable reference to the property to make nullable.
339fn make_property_nullable(property: &mut schema::Property) {
340    let mut any_of = vec![Item::DataTypeItem(DataTypeItemType {
341        dtype: DataType::Null,
342    })];
343
344    handle_property_data_type(property, &mut any_of);
345    handle_property_reference(property, &mut any_of);
346    handle_property_one_of(property, &mut any_of);
347
348    if !matches!(property.dtype, Some(DataType::Array)) {
349        property.any_of = Some(any_of);
350    }
351}
352
353/// Handles the data type of a property when making it nullable.
354///
355/// # Arguments
356///
357/// * `property` - A mutable reference to the property.
358/// * `any_of` - A mutable reference to the anyOf vector to populate.
359fn handle_property_data_type(property: &mut schema::Property, any_of: &mut Vec<Item>) {
360    if let Some(dtype) = &property.dtype {
361        let is_array = matches!(dtype, DataType::Array);
362
363        match dtype {
364            DataType::Array => {
365                any_of.push(Item::DataTypeItem(DataTypeItemType {
366                    dtype: DataType::Null,
367                }));
368            }
369            DataType::Object => {
370                property.dtype = None;
371            }
372            DataType::Multiple(data_types) => {
373                add_multiple_data_types(any_of, data_types);
374            }
375            _ => {
376                any_of.push(Item::DataTypeItem(DataTypeItemType {
377                    dtype: dtype.clone(),
378                }));
379            }
380        }
381
382        if !is_array {
383            property.dtype = None;
384        }
385    }
386}
387
388/// Adds multiple data types to the anyOf vector, filtering out objects.
389///
390/// # Arguments
391///
392/// * `any_of` - A mutable reference to the anyOf vector.
393/// * `data_types` - A reference to the vector of data types to add.
394fn add_multiple_data_types(any_of: &mut Vec<Item>, data_types: &[DataType]) {
395    for dtype in data_types.iter() {
396        if dtype.is_not_object() || dtype.is_array() {
397            any_of.push(Item::DataTypeItem(DataTypeItemType {
398                dtype: dtype.clone(),
399            }));
400        }
401    }
402}
403
404/// Handles the reference of a property when making it nullable.
405///
406/// # Arguments
407///
408/// * `property` - A mutable reference to the property.
409/// * `any_of` - A mutable reference to the anyOf vector to populate.
410fn handle_property_reference(property: &mut schema::Property, any_of: &mut Vec<Item>) {
411    if let Some(reference) = &property.reference {
412        any_of.push(Item::ReferenceItem(ReferenceItemType {
413            reference: reference.clone(),
414        }));
415        property.reference = None;
416        property.dtype = None;
417        property.title = None;
418        property.description = None;
419    }
420}
421
422/// Handles the oneOf property when making it nullable.
423///
424/// # Arguments
425///
426/// * `property` - A mutable reference to the property.
427/// * `any_of` - A mutable reference to the anyOf vector to populate.
428fn handle_property_one_of(property: &mut schema::Property, any_of: &mut Vec<Item>) {
429    if let Some(one_of) = &property.one_of {
430        any_of.extend(one_of.clone());
431        property.one_of = None;
432    }
433}
434
435/// Finalizes the schema requirements by setting additional properties and sorting required fields.
436///
437/// # Arguments
438///
439/// * `schema` - A mutable reference to the schema object.
440/// * `new_required` - A vector of newly required field names.
441fn finalize_schema_requirements(schema: &mut schema::SchemaObject, new_required: Vec<String>) {
442    schema.additional_properties = false;
443    schema.required.extend(new_required);
444    schema.required.sort();
445}
446
447impl TryFrom<&Enumeration> for schema::SchemaType {
448    type Error = String;
449
450    /// Attempts to convert an `Enumeration` into a `SchemaType`.
451    ///
452    /// # Arguments
453    ///
454    /// * `enumeration` - A reference to the `Enumeration`.
455    ///
456    /// # Returns
457    ///
458    /// A `Result` containing the `SchemaType` or an error message.
459    fn try_from(enumeration: &Enumeration) -> Result<Self, Self::Error> {
460        Ok(schema::SchemaType::Enum(schema::EnumObject::try_from(
461            enumeration,
462        )?))
463    }
464}
465
466impl TryFrom<&Object> for schema::SchemaType {
467    type Error = String;
468
469    /// Attempts to convert an `Object` into a `SchemaType`.
470    ///
471    /// # Arguments
472    ///
473    /// * `obj` - A reference to the `Object`.
474    ///
475    /// # Returns
476    ///
477    /// A `Result` containing the `SchemaType` or an error message.
478    fn try_from(obj: &Object) -> Result<Self, Self::Error> {
479        Ok(schema::SchemaType::Object(schema::SchemaObject::try_from(
480            obj,
481        )?))
482    }
483}
484
485impl TryFrom<&Object> for schema::SchemaObject {
486    type Error = String;
487
488    /// Attempts to convert an `Object` into a `SchemaObject`.
489    ///
490    /// # Arguments
491    ///
492    /// * `obj` - A reference to the `Object`.
493    ///
494    /// # Returns
495    ///
496    /// A `Result` containing the `SchemaObject` or an error message.
497    fn try_from(obj: &Object) -> Result<Self, Self::Error> {
498        let properties: Result<BTreeMap<String, schema::Property>, String> = obj
499            .attributes
500            .iter()
501            .map(|attr| -> Result<(String, schema::Property), String> {
502                Ok((attr.name.clone(), schema::Property::try_from(attr)?))
503            })
504            .collect();
505
506        let required: Vec<String> = obj
507            .attributes
508            .iter()
509            .filter(|attr| attr.required)
510            .map(|attr| attr.name.clone())
511            .collect();
512
513        Ok(schema::SchemaObject {
514            title: obj.name.clone(),
515            dtype: Some(schema::DataType::Object),
516            description: Some(obj.docstring.clone()),
517            properties: properties?,
518            definitions: BTreeMap::new(),
519            required,
520            schema: None,
521            id: None,
522            additional_properties: false,
523        })
524    }
525}
526
527impl TryFrom<&Enumeration> for schema::EnumObject {
528    type Error = String;
529
530    /// Attempts to convert an `Enumeration` into an `EnumObject`.
531    ///
532    /// # Arguments
533    ///
534    /// * `enumeration` - A reference to the `Enumeration`.
535    ///
536    /// # Returns
537    ///
538    /// A `Result` containing the `EnumObject` or an error message.
539    fn try_from(enumeration: &Enumeration) -> Result<Self, Self::Error> {
540        let values = enumeration
541            .mappings
542            .values()
543            .cloned()
544            .collect::<Vec<String>>();
545
546        Ok(schema::EnumObject {
547            title: enumeration.name.clone(),
548            dtype: Some(schema::DataType::String),
549            description: Some(enumeration.docstring.clone()),
550            enum_values: values,
551        })
552    }
553}
554
555impl TryFrom<&Attribute> for schema::Property {
556    type Error = String;
557
558    /// Attempts to convert an `Attribute` into a `Property`.
559    ///
560    /// # Arguments
561    ///
562    /// * `attr` - A reference to the `Attribute`.
563    ///
564    /// # Returns
565    ///
566    /// A `Result` containing the `Property` or an error message.
567    fn try_from(attr: &Attribute) -> Result<Self, Self::Error> {
568        let mut dtype = (!attr.is_enum)
569            .then(|| schema::DataType::try_from(attr))
570            .transpose()?;
571
572        let options: HashMap<String, PrimitiveType> = attr
573            .options
574            .iter()
575            .map(|o| -> Result<(String, PrimitiveType), String> {
576                Ok((o.key().to_string(), o.try_into()?))
577            })
578            .collect::<Result<HashMap<String, PrimitiveType>, String>>()?;
579
580        let reference: Option<String> = if (attr.is_enum
581            || matches!(dtype, Some(schema::DataType::Object)))
582            && attr.dtypes.len() == 1
583        {
584            Some(format!("#/$defs/{}", attr.dtypes[0]))
585        } else {
586            None
587        };
588
589        let items: Option<schema::Item> = attr.into();
590        let one_of = (!attr.is_array).then(|| attr.into());
591        let description = (!attr.docstring.is_empty()).then(|| attr.docstring.clone());
592        let enum_values = if attr.is_enum { Some(Vec::new()) } else { None };
593
594        if attr.dtypes.len() > 1 && !attr.is_array {
595            // If there are multiple types, we need to use the AnyOf case
596            dtype = None;
597        }
598
599        // Make sure that the default matches the datatype
600        let default: Option<PrimitiveType> = if let Some(default) = attr.default.clone() {
601            process_default(default, &dtype)
602        } else {
603            None
604        };
605
606        Ok(schema::Property {
607            title: Some(attr.name.clone()),
608            dtype,
609            default,
610            description,
611            term: attr.term.clone(),
612            reference,
613            options,
614            one_of,
615            items,
616            enum_values,
617            any_of: None,
618            all_of: None,
619            examples: Vec::new(),
620        })
621    }
622}
623
624/// Processes the default value of an attribute.
625///
626/// # Arguments
627///
628/// * `default` - A reference to the default value of the attribute.
629/// * `dtype` - A reference to the data type of the attribute.
630///
631/// # Returns
632///
633/// A `Result` containing the processed default value or an error message.
634fn process_default(
635    default: attribute::DataType,
636    dtype: &Option<schema::DataType>,
637) -> Option<PrimitiveType> {
638    if matches!(dtype, Some(schema::DataType::String)) {
639        default
640            .as_string()
641            .map(|d| PrimitiveType::String(d.trim_matches('"').to_string()))
642    } else {
643        Some(default.into())
644    }
645}
646
647impl TryFrom<&Attribute> for schema::DataType {
648    type Error = String;
649
650    /// Attempts to convert an `Attribute` into a `DataType`.
651    ///
652    /// # Arguments
653    ///
654    /// * `attr` - A reference to the `Attribute`.
655    ///
656    /// # Returns
657    ///
658    /// A `Result` containing the `DataType` or an error message.
659    ///
660    /// # Errors
661    ///
662    /// Returns an error if the `dtypes` vector in the attribute is empty.
663    fn try_from(attr: &Attribute) -> Result<Self, Self::Error> {
664        if attr.is_array {
665            return Ok(schema::DataType::Array);
666        }
667
668        schema::DataType::try_from(
669            attr.dtypes
670                .first()
671                .ok_or(format!("No data types found for attribute: {}", attr.name))?,
672        )
673    }
674}
675
676/// Specific case for the `items` field in the JSON schema.
677impl From<&Attribute> for Option<schema::Item> {
678    /// Converts an `Attribute` into an `Option<Item>`.
679    ///
680    /// # Arguments
681    ///
682    /// * `attr` - A reference to the `Attribute`.
683    ///
684    /// # Returns
685    ///
686    /// An `Option<Item>` representing the attribute's items.
687    fn from(attr: &Attribute) -> Self {
688        if !attr.is_array {
689            // No need for 'items' when the attr is not
690            // an array type
691            return None;
692        }
693
694        // Check if it is an OneOf case
695        let one_of: Vec<schema::Item> = attr.into();
696
697        if one_of.is_empty() {
698            // There is just a single type
699            Some(process_dtype(&attr.dtypes[0]))
700        } else {
701            Some(schema::Item::OneOfItem(schema::OneOfItemType { one_of }))
702        }
703    }
704}
705
706impl From<&Attribute> for Vec<schema::Item> {
707    /// Converts an `Attribute` into a `Vec<Item>`.
708    ///
709    /// # Arguments
710    ///
711    /// * `attr` - A reference to the `Attribute`.
712    ///
713    /// # Returns
714    ///
715    /// A `Vec<Item>` representing the attribute's items.
716    fn from(attr: &Attribute) -> Self {
717        if attr.dtypes.len() == 1 {
718            return Vec::new();
719        }
720
721        let mut items = Vec::new();
722        for dtype in attr.dtypes.iter() {
723            items.push(process_dtype(dtype));
724        }
725
726        items
727    }
728}
729
730/// Processes a data type string and returns an `Item`.
731///
732/// # Arguments
733///
734/// * `dtype` - A reference to the data type string.
735///
736/// # Returns
737///
738/// An `Item` representing the data type.
739fn process_dtype(dtype: &str) -> schema::Item {
740    match schema::DataType::from_str(dtype) {
741        Ok(basic_type) => {
742            schema::Item::DataTypeItem(schema::DataTypeItemType { dtype: basic_type })
743        }
744        Err(_) => schema::Item::ReferenceItem(schema::ReferenceItemType {
745            reference: format!("#/$defs/{dtype}"),
746        }),
747    }
748}
749
750impl TryFrom<&AttrOption> for PrimitiveType {
751    type Error = String;
752
753    fn try_from(option: &AttrOption) -> Result<Self, Self::Error> {
754        let value = option.value();
755
756        // Try parsing in order: f64, boolean, i64, string
757        if let Ok(float_val) = value.parse::<f64>() {
758            return Ok(PrimitiveType::Number(float_val));
759        }
760
761        if let Ok(bool_val) = value.parse::<bool>() {
762            return Ok(PrimitiveType::Boolean(bool_val));
763        }
764
765        if let Ok(int_val) = value.parse::<i64>() {
766            return Ok(PrimitiveType::Integer(int_val));
767        }
768
769        // If all other parses fail, treat as string
770        Ok(PrimitiveType::String(value))
771    }
772}
773
774#[cfg(test)]
775mod tests {
776    use serde_json::{json, Value};
777
778    use super::*;
779    use crate::attribute::Attribute;
780
781    #[test]
782    fn test_attribute_with_multiple_types() {
783        let attr = Attribute {
784            name: "test_attribute".to_string(),
785            is_array: false,
786            is_id: false,
787            dtypes: vec!["string".to_string(), "RefType".to_string()],
788            docstring: "".to_string(),
789            options: vec![],
790            term: None,
791            required: false,
792            default: None,
793            xml: None,
794            is_enum: false,
795            position: None,
796            import_prefix: None,
797        };
798
799        let property: schema::Property =
800            schema::Property::try_from(&attr).expect("Failed to convert Attribute to Property");
801
802        let serialized_property =
803            serde_json::to_value(&property).expect("Failed to serialize Property to JSON");
804
805        let expected_json = json!({
806            "title": "test_attribute",
807            "oneOf": [
808                {"type": "string"},
809                {"$ref": "#/$defs/RefType"},
810            ]
811        });
812
813        assert_eq!(serialized_property, expected_json);
814    }
815
816    #[test]
817    fn test_array_attribute() {
818        let attr = Attribute {
819            name: "test_attribute".to_string(),
820            is_array: true,
821            is_id: false,
822            dtypes: vec!["string".to_string(), "RefType".to_string()],
823            docstring: "".to_string(),
824            options: vec![],
825            term: None,
826            required: false,
827            default: None,
828            xml: None,
829            is_enum: false,
830            position: None,
831            import_prefix: None,
832        };
833
834        let property: schema::Property =
835            schema::Property::try_from(&attr).expect("Failed to convert Attribute to Property");
836        let serialized_property: Value =
837            serde_json::to_value(&property).expect("Failed to serialize Property to JSON");
838
839        let expected_json = json!({
840            "title": "test_attribute",
841            "type": "array",
842            "items": {
843                "oneOf": [
844                    {"type": "string"},
845                    {"$ref": "#/$defs/RefType"}
846                ]
847            }
848        });
849
850        assert_eq!(serialized_property, expected_json);
851    }
852}