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}