Skip to main content

mdmodels_core/linkml/
import.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
24//! Provides functionality to import LinkML schemas into internal data model format.
25//!
26//! This module contains implementations for converting LinkML schema format into internal
27//! data model representations. It handles the conversion of classes, slots, and enumerations
28//! from their LinkML representations.
29//!
30//! # Key Components
31//!
32//! - `deserialize_linkml`: Main entry point for importing LinkML YAML files
33//! - `From<LinkML> for DataModel`: Core conversion from LinkML schema to internal model
34//! - `From` implementations for converting individual LinkML components:
35//!   - `ClassDefinition` -> `Object`
36//!   - `AttributeDefinition` -> `Attribute`
37//!   - `EnumDefinition` -> `Enumeration`
38use std::{collections::BTreeMap, error::Error, path::PathBuf};
39
40use crate::{
41    attribute::Attribute,
42    markdown::frontmatter::FrontMatter,
43    object::{Enumeration, Object},
44    option::AttrOption,
45    prelude::DataModel,
46};
47
48use super::schema::{AttributeDefinition, ClassDefinition, EnumDefinition, LinkML};
49
50/// Deserializes a LinkML YAML file into a DataModel.
51///
52/// This function reads a LinkML schema from a YAML file and converts it into the internal
53/// DataModel representation. The conversion preserves all relevant schema information including
54/// classes, attributes, enumerations, and metadata.
55///
56/// # Arguments
57///
58/// * `path` - Path to the LinkML YAML file to import
59///
60/// # Returns
61///
62/// * `Ok(DataModel)` - Successfully parsed and converted data model
63/// * `Err(Box<dyn Error>)` - Error during file reading or YAML parsing
64pub fn deserialize_linkml(path: &PathBuf) -> Result<DataModel, Box<dyn Error>> {
65    let yaml = std::fs::read_to_string(path)?;
66    let linkml: LinkML = serde_yaml::from_str(&yaml)?;
67    Ok(DataModel::from(linkml))
68}
69
70/// Implements conversion from LinkML schema to DataModel.
71impl From<LinkML> for DataModel {
72    /// Converts a LinkML schema into the internal DataModel format.
73    ///
74    /// This conversion handles:
75    /// - Schema metadata through FrontMatter configuration
76    /// - Classes and their attributes
77    /// - Global slots/attributes
78    /// - Enumerations
79    ///
80    /// The conversion preserves:
81    /// - Prefixes and namespace information
82    /// - Class hierarchies and relationships
83    /// - Attribute definitions and constraints
84    /// - Enumeration values and meanings
85    fn from(linkml: LinkML) -> Self {
86        // Create config from LinkML metadata
87        let config = FrontMatter {
88            prefix: linkml.id,
89            prefixes: Some(linkml.prefixes.into_iter().collect()),
90            ..Default::default()
91        };
92
93        // Convert classes to objects, merging in global slots
94        let mut objects = Vec::new();
95        for (name, class) in linkml.classes {
96            let mut obj = Object::from(class.clone());
97            obj.name = name;
98
99            // Add global slots to object attributes
100            for slot_name in class.slots {
101                if let Some(slot_def) = linkml.slots.get(&slot_name) {
102                    let mut attr = Attribute::from(slot_def.clone());
103                    attr.name = slot_name;
104                    obj.attributes.push(attr);
105                }
106            }
107            objects.push(obj);
108        }
109
110        // Convert enums
111        let enums = linkml
112            .enums
113            .into_iter()
114            .map(|(name, def)| {
115                let mut enum_ = Enumeration::from(def);
116                enum_.name = name;
117                enum_
118            })
119            .collect();
120
121        DataModel {
122            name: Some(linkml.name),
123            config: Some(config),
124            objects,
125            enums,
126        }
127    }
128}
129
130/// Implements conversion from LinkML ClassDefinition to Object.
131impl From<ClassDefinition> for Object {
132    /// Converts a LinkML ClassDefinition into an internal Object representation.
133    ///
134    /// This conversion handles:
135    /// - Class metadata (name, description, URI)
136    /// - Local attribute definitions
137    /// - Slot usage patterns and constraints
138    ///
139    /// # Arguments
140    ///
141    /// * `class` - The LinkML ClassDefinition to convert
142    ///
143    /// # Returns
144    ///
145    /// An Object representing the class in the internal model format
146    fn from(class: ClassDefinition) -> Self {
147        let mut attributes = Vec::new();
148
149        // Convert local attributes
150        if let Some(attrs) = class.attributes {
151            for (name, def) in attrs {
152                let mut attr = Attribute::from(def);
153                attr.name = name;
154                attributes.push(attr);
155            }
156        }
157
158        // Add pattern constraints from slot usage
159        if let Some(slot_usage) = class.slot_usage {
160            for (name, usage) in slot_usage {
161                if let Some(pattern) = usage.pattern {
162                    if let Some(attr) = attributes.iter_mut().find(|a| a.name == name) {
163                        attr.options.push(AttrOption::Pattern(pattern));
164                    }
165                }
166            }
167        }
168
169        Object {
170            name: class.is_a.unwrap_or_default(),
171            docstring: class.description.unwrap_or_default(),
172            term: class.class_uri,
173            attributes,
174            mixins: Vec::new(),
175            position: None,
176        }
177    }
178}
179
180/// Implements conversion from LinkML AttributeDefinition to Attribute.
181impl From<AttributeDefinition> for Attribute {
182    /// Converts a LinkML AttributeDefinition into an internal Attribute representation.
183    ///
184    /// This conversion preserves:
185    /// - Documentation
186    /// - Data type/range
187    /// - Cardinality (multivalued status)
188    /// - Identifier status
189    /// - Required status
190    /// - URI/term mapping
191    ///
192    /// # Arguments
193    ///
194    /// * `attr` - The LinkML AttributeDefinition to convert
195    ///
196    /// # Returns
197    ///
198    /// An Attribute representing the slot in the internal model format
199    fn from(attr: AttributeDefinition) -> Self {
200        Attribute {
201            name: String::new(), // Set later when context is available
202            docstring: attr.description.unwrap_or_default(),
203            dtypes: vec![attr.range.unwrap_or_else(|| "string".to_string())],
204            term: attr.slot_uri,
205            is_array: attr.multivalued.unwrap_or(false),
206            is_id: attr.identifier.unwrap_or(false),
207            required: attr.required.unwrap_or(false),
208            options: Vec::new(), // Patterns added later from slot_usage
209            default: None,
210            is_enum: false,
211            position: None,
212            xml: None,
213            import_prefix: None,
214        }
215    }
216}
217
218/// Implements conversion from LinkML EnumDefinition to Enumeration.
219impl From<EnumDefinition> for Enumeration {
220    /// Converts a LinkML EnumDefinition into an internal Enumeration representation.
221    ///
222    /// This conversion preserves:
223    /// - Documentation
224    /// - Enumeration values and their meanings
225    /// - Value mappings
226    ///
227    /// # Arguments
228    ///
229    /// * `enum_def` - The LinkML EnumDefinition to convert
230    ///
231    /// # Returns
232    ///
233    /// An Enumeration representing the enum in the internal model format
234    fn from(enum_def: EnumDefinition) -> Self {
235        let mappings = enum_def
236            .permissible_values
237            .into_iter()
238            .map(|(key, value)| (key, value.meaning.unwrap_or_default()))
239            .collect::<BTreeMap<String, String>>();
240
241        Enumeration {
242            name: String::new(), // Set later when context is available
243            docstring: enum_def.description.unwrap_or_default(),
244            mappings,
245            position: None,
246        }
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use pretty_assertions::assert_eq;
254
255    #[test]
256    fn deserialize_linkml_test() {
257        let model = deserialize_linkml(&PathBuf::from("tests/data/expected_linkml.yml")).unwrap();
258        let expected_model =
259            DataModel::from_markdown(&PathBuf::from("tests/data/model.md")).unwrap();
260
261        assert_eq!(
262            model.objects.len(),
263            expected_model.objects.len(),
264            "Objects length mismatch"
265        );
266        assert_eq!(
267            model.enums.len(),
268            expected_model.enums.len(),
269            "Enums length mismatch"
270        );
271
272        for obj in model.objects.iter() {
273            let other_obj = expected_model
274                .objects
275                .iter()
276                .find(|o| o.name == obj.name)
277                .unwrap_or_else(|| panic!("Object {} not found", obj.name));
278            assert_eq!(obj.name, other_obj.name, "Object name mismatch");
279            assert_eq!(
280                obj.docstring, other_obj.docstring,
281                "Object docstring mismatch"
282            );
283            assert_eq!(obj.term, other_obj.term, "Object term mismatch");
284            assert_eq!(
285                obj.attributes.len(),
286                other_obj.attributes.len(),
287                "Attributes length mismatch"
288            );
289
290            for attr in obj.attributes.iter() {
291                let other_attr = other_obj
292                    .attributes
293                    .iter()
294                    .find(|a| a.name == attr.name)
295                    .unwrap_or_else(|| panic!("Attribute {} not found", attr.name));
296                assert_eq!(attr.name, other_attr.name, "Attribute name mismatch");
297            }
298        }
299
300        for enum_ in model.enums.iter() {
301            let other_enum = expected_model
302                .enums
303                .iter()
304                .find(|e| e.name == enum_.name)
305                .unwrap_or_else(|| panic!("Enum {} not found", enum_.name));
306            assert_eq!(enum_.name, other_enum.name, "Enum name mismatch");
307            assert_eq!(
308                enum_.docstring, other_enum.docstring,
309                "Enum docstring mismatch"
310            );
311            assert_eq!(
312                enum_.mappings, other_enum.mappings,
313                "Enum mappings mismatch"
314            );
315        }
316    }
317
318    // Add more specific tests for each conversion implementation
319}