From 2150309c98c5333409b72796d16366434191ba8e Mon Sep 17 00:00:00 2001 From: Jan Range <30547301+JR-1991@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:43:42 +0100 Subject: [PATCH 1/6] add linkml import --- src/lib.rs | 1 + src/linkml/import.rs | 318 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+) create mode 100644 src/linkml/import.rs diff --git a/src/lib.rs b/src/lib.rs index 4c40066..6a275e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,6 +73,7 @@ pub mod bindings { pub mod linkml { pub mod export; + pub mod import; pub mod schema; } diff --git a/src/linkml/import.rs b/src/linkml/import.rs new file mode 100644 index 0000000..4cec92c --- /dev/null +++ b/src/linkml/import.rs @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2024 Jan Range + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +//! Provides functionality to import LinkML schemas into internal data model format. +//! +//! This module contains implementations for converting LinkML schema format into internal +//! data model representations. It handles the conversion of classes, slots, and enumerations +//! from their LinkML representations. +//! +//! # Key Components +//! +//! - `deserialize_linkml`: Main entry point for importing LinkML YAML files +//! - `From for DataModel`: Core conversion from LinkML schema to internal model +//! - `From` implementations for converting individual LinkML components: +//! - `ClassDefinition` -> `Object` +//! - `AttributeDefinition` -> `Attribute` +//! - `EnumDefinition` -> `Enumeration` +use std::{collections::BTreeMap, error::Error, path::PathBuf}; + +use crate::{ + attribute::Attribute, + markdown::frontmatter::FrontMatter, + object::{Enumeration, Object}, + option::AttrOption, + prelude::DataModel, +}; + +use super::schema::{AttributeDefinition, ClassDefinition, EnumDefinition, LinkML}; + +/// Deserializes a LinkML YAML file into a DataModel. +/// +/// This function reads a LinkML schema from a YAML file and converts it into the internal +/// DataModel representation. The conversion preserves all relevant schema information including +/// classes, attributes, enumerations, and metadata. +/// +/// # Arguments +/// +/// * `path` - Path to the LinkML YAML file to import +/// +/// # Returns +/// +/// * `Ok(DataModel)` - Successfully parsed and converted data model +/// * `Err(Box)` - Error during file reading or YAML parsing +pub fn deserialize_linkml(path: &PathBuf) -> Result> { + let yaml = std::fs::read_to_string(path)?; + let linkml: LinkML = serde_yaml::from_str(&yaml)?; + Ok(DataModel::from(linkml)) +} + +/// Implements conversion from LinkML schema to DataModel. +impl From for DataModel { + /// Converts a LinkML schema into the internal DataModel format. + /// + /// This conversion handles: + /// - Schema metadata through FrontMatter configuration + /// - Classes and their attributes + /// - Global slots/attributes + /// - Enumerations + /// + /// The conversion preserves: + /// - Prefixes and namespace information + /// - Class hierarchies and relationships + /// - Attribute definitions and constraints + /// - Enumeration values and meanings + fn from(linkml: LinkML) -> Self { + // Create config from LinkML metadata + let config = FrontMatter { + prefix: linkml.id, + prefixes: Some(linkml.prefixes), + ..Default::default() + }; + + // Convert classes to objects, merging in global slots + let mut objects = Vec::new(); + for (name, class) in linkml.classes { + let mut obj = Object::from(class.clone()); + obj.name = name; + + // Add global slots to object attributes + for slot_name in class.slots { + if let Some(slot_def) = linkml.slots.get(&slot_name) { + let mut attr = Attribute::from(slot_def.clone()); + attr.name = slot_name; + obj.attributes.push(attr); + } + } + objects.push(obj); + } + + // Convert enums + let enums = linkml + .enums + .into_iter() + .map(|(name, def)| { + let mut enum_ = Enumeration::from(def); + enum_.name = name; + enum_ + }) + .collect(); + + DataModel { + name: Some(linkml.name), + config: Some(config), + objects, + enums, + } + } +} + +/// Implements conversion from LinkML ClassDefinition to Object. +impl From for Object { + /// Converts a LinkML ClassDefinition into an internal Object representation. + /// + /// This conversion handles: + /// - Class metadata (name, description, URI) + /// - Local attribute definitions + /// - Slot usage patterns and constraints + /// + /// # Arguments + /// + /// * `class` - The LinkML ClassDefinition to convert + /// + /// # Returns + /// + /// An Object representing the class in the internal model format + fn from(class: ClassDefinition) -> Self { + let mut attributes = Vec::new(); + + // Convert local attributes + if let Some(attrs) = class.attributes { + for (name, def) in attrs { + let mut attr = Attribute::from(def); + attr.name = name; + attributes.push(attr); + } + } + + // Add pattern constraints from slot usage + if let Some(slot_usage) = class.slot_usage { + for (name, usage) in slot_usage { + if let Some(pattern) = usage.pattern { + if let Some(attr) = attributes.iter_mut().find(|a| a.name == name) { + attr.options.push(AttrOption::Pattern(pattern)); + } + } + } + } + + Object { + name: class.is_a.unwrap_or_default(), + docstring: class.description.unwrap_or_default(), + term: class.class_uri, + attributes, + parent: None, + position: None, + } + } +} + +/// Implements conversion from LinkML AttributeDefinition to Attribute. +impl From for Attribute { + /// Converts a LinkML AttributeDefinition into an internal Attribute representation. + /// + /// This conversion preserves: + /// - Documentation + /// - Data type/range + /// - Cardinality (multivalued status) + /// - Identifier status + /// - Required status + /// - URI/term mapping + /// + /// # Arguments + /// + /// * `attr` - The LinkML AttributeDefinition to convert + /// + /// # Returns + /// + /// An Attribute representing the slot in the internal model format + fn from(attr: AttributeDefinition) -> Self { + Attribute { + name: String::new(), // Set later when context is available + docstring: attr.description.unwrap_or_default(), + dtypes: vec![attr.range.unwrap_or_else(|| "string".to_string())], + term: attr.slot_uri, + is_array: attr.multivalued.unwrap_or(false), + is_id: attr.identifier.unwrap_or(false), + required: attr.required.unwrap_or(false), + options: Vec::new(), // Patterns added later from slot_usage + default: None, + is_enum: false, + position: None, + xml: None, + } + } +} + +/// Implements conversion from LinkML EnumDefinition to Enumeration. +impl From for Enumeration { + /// Converts a LinkML EnumDefinition into an internal Enumeration representation. + /// + /// This conversion preserves: + /// - Documentation + /// - Enumeration values and their meanings + /// - Value mappings + /// + /// # Arguments + /// + /// * `enum_def` - The LinkML EnumDefinition to convert + /// + /// # Returns + /// + /// An Enumeration representing the enum in the internal model format + fn from(enum_def: EnumDefinition) -> Self { + let mappings = enum_def + .permissible_values + .into_iter() + .map(|(key, value)| (key, value.meaning.unwrap_or_default())) + .collect::>(); + + Enumeration { + name: String::new(), // Set later when context is available + docstring: enum_def.description.unwrap_or_default(), + mappings, + position: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn deserialize_linkml_test() { + let model = deserialize_linkml(&PathBuf::from("tests/data/expected_linkml.yml")).unwrap(); + let expected_model = + DataModel::from_markdown(&PathBuf::from("tests/data/model.md")).unwrap(); + + assert_eq!( + model.objects.len(), + expected_model.objects.len(), + "Objects length mismatch" + ); + assert_eq!( + model.enums.len(), + expected_model.enums.len(), + "Enums length mismatch" + ); + + for obj in model.objects.iter() { + let other_obj = expected_model + .objects + .iter() + .find(|o| o.name == obj.name) + .expect(&format!("Object {} not found", obj.name)); + assert_eq!(obj.name, other_obj.name, "Object name mismatch"); + assert_eq!( + obj.docstring, other_obj.docstring, + "Object docstring mismatch" + ); + assert_eq!(obj.term, other_obj.term, "Object term mismatch"); + assert_eq!( + obj.attributes.len(), + other_obj.attributes.len(), + "Attributes length mismatch" + ); + + for attr in obj.attributes.iter() { + let other_attr = other_obj + .attributes + .iter() + .find(|a| a.name == attr.name) + .expect(&format!("Attribute {} not found", attr.name)); + assert_eq!(attr.name, other_attr.name, "Attribute name mismatch"); + } + } + + for enum_ in model.enums.iter() { + let other_enum = expected_model + .enums + .iter() + .find(|e| e.name == enum_.name) + .expect(&format!("Enum {} not found", enum_.name)); + assert_eq!(enum_.name, other_enum.name, "Enum name mismatch"); + assert_eq!( + enum_.docstring, other_enum.docstring, + "Enum docstring mismatch" + ); + assert_eq!( + enum_.mappings, other_enum.mappings, + "Enum mappings mismatch" + ); + } + } + + // Add more specific tests for each conversion implementation +} From d2d96f0b90e034edfc2d339f8fc935b9e9ce04e8 Mon Sep 17 00:00:00 2001 From: Jan Range <30547301+JR-1991@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:43:55 +0100 Subject: [PATCH 2/6] update expected linkml --- tests/data/expected_linkml.yml | 42 +++++++++++++--------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/tests/data/expected_linkml.yml b/tests/data/expected_linkml.yml index 74e2ac7..01abc33 100644 --- a/tests/data/expected_linkml.yml +++ b/tests/data/expected_linkml.yml @@ -9,50 +9,40 @@ imports: - linkml:types classes: Test2: - is_a: Test2 attributes: names: - identifier: false - required: false + slot_uri: schema:hello multivalued: true number: - identifier: false - required: false + slot_uri: schema:one range: float - multivalued: false + minimum_value: 0 Test: description: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. - is_a: Test attributes: + name: + description: The name of the test. This is a unique identifier that helps track individual test cases across the system. It should be descriptive and follow the standard naming conventions. + slot_uri: schema:hello + identifier: true + required: true + number: + slot_uri: schema:one + range: float test2: - identifier: false - required: false + slot_uri: schema:something range: Test2 multivalued: true ontology: - identifier: false - required: false range: Ontology - multivalued: false - number: - identifier: false - required: false - range: float - multivalued: false - name: - description: The name of the test. This is a unique identifier that helps track individual test cases across the system. It should be descriptive and follow the standard naming conventions. - identifier: true - required: true - multivalued: false enums: Ontology: permissible_values: - GO: - meaning: https://amigo.geneontology.org/amigo/term/ - description: https://amigo.geneontology.org/amigo/term/ SIO: meaning: http://semanticscience.org/resource/ description: http://semanticscience.org/resource/ ECO: meaning: https://www.evidenceontology.org/term/ - description: https://www.evidenceontology.org/term/ \ No newline at end of file + description: https://www.evidenceontology.org/term/ + GO: + meaning: https://amigo.geneontology.org/amigo/term/ + description: https://amigo.geneontology.org/amigo/term/ \ No newline at end of file From 42151a936f2ce1d0c11afc4d2008ed448455be11 Mon Sep 17 00:00:00 2001 From: Jan Range <30547301+JR-1991@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:45:08 +0100 Subject: [PATCH 3/6] change `examples` type and serialisation --- src/linkml/schema.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/linkml/schema.rs b/src/linkml/schema.rs index 9575136..8a42fce 100644 --- a/src/linkml/schema.rs +++ b/src/linkml/schema.rs @@ -165,7 +165,7 @@ pub struct ClassDefinition { #[serde(default, skip_serializing_if = "Option::is_none")] pub is_a: Option, /// Whether this class is a tree root - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "is_false_option")] pub tree_root: Option, /// Map of slot usage definitions #[serde(default, skip_serializing_if = "Option::is_none")] @@ -173,6 +173,9 @@ pub struct ClassDefinition { /// Map of attributes #[serde(default, skip_serializing_if = "Option::is_none")] pub attributes: Option>, + /// Mixed in class + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub mixins: Vec, } /// Represents an annotation on a schema element @@ -201,20 +204,23 @@ pub struct AttributeDefinition { deserialize_with = "remove_newlines" )] pub description: Option, + /// Semantic type of the slot + #[serde(default, skip_serializing_if = "is_empty_string_option")] + pub slot_uri: Option, /// Whether this slot serves as an identifier - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "is_false_option")] pub identifier: Option, /// Whether this slot is required - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "is_false_option")] pub required: Option, /// Optional type range for the slot #[serde(default, skip_serializing_if = "Option::is_none")] pub range: Option, /// Whether this slot is read-only - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "is_false_option")] pub readonly: Option, /// Whether this slot can have multiple values - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "is_false_option")] pub multivalued: Option, /// Optional minimum value for numeric slots #[serde(default, skip_serializing_if = "Option::is_none")] @@ -223,11 +229,11 @@ pub struct AttributeDefinition { #[serde(default, skip_serializing_if = "Option::is_none")] pub maximum_value: Option, /// Whether this slot is recommended - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "is_false_option")] pub recommended: Option, /// Optional map of example values - #[serde(default, skip_serializing_if = "Option::is_none")] - pub examples: Option>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub examples: Vec, /// Optional map of annotations #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option>, @@ -265,3 +271,7 @@ where fn is_empty_string_option(s: &Option) -> bool { s.is_none() || s.as_ref().unwrap().is_empty() } + +fn is_false_option(s: &Option) -> bool { + s.is_none() || !s.as_ref().unwrap() +} From f123d173e3328ef4b9a36d0e559f04478b26cf2b Mon Sep 17 00:00:00 2001 From: Jan Range <30547301+JR-1991@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:45:16 +0100 Subject: [PATCH 4/6] wrap descriptions --- templates/markdown.jinja | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/templates/markdown.jinja b/templates/markdown.jinja index 7c81657..80a23a8 100644 --- a/templates/markdown.jinja +++ b/templates/markdown.jinja @@ -4,7 +4,7 @@ {% for object in objects %} ### {{ object.name }} {% if object.docstring %} -{{ object.docstring }} +{{ wrap(object.docstring, 70, "", "", None) }} {% endif %} {%- for attribute in object.attributes %} - {{attribute.name}} @@ -12,6 +12,9 @@ {%- if attribute.term %} - Term: {{ attribute.term }} {%- endif %} + {%- if attribute.docstring %} + - Description: {{ wrap(attribute.docstring, 60, "", " ", None) }} + {%- endif %} {%- for option in attribute.options %} - {{ option.key }}: {{ option.value }} {%- endfor -%} @@ -25,7 +28,7 @@ {%- for enum in enums %} ### {{ enum.name }} {% if enum.docstring %} -{{ enum.docstring }} +{{ wrap(enum.docstring, 70, "", "", None) }} {% endif %} ``` {%- for key, value in enum.mappings | dictsort %} From e9d991655204ac1cd21463857c62382b6e9a6982 Mon Sep 17 00:00:00 2001 From: Jan Range <30547301+JR-1991@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:45:32 +0100 Subject: [PATCH 5/6] add explicit options to linkml --- src/linkml/export.rs | 164 ++++++++++++++++++++++++++++--------------- 1 file changed, 106 insertions(+), 58 deletions(-) diff --git a/src/linkml/export.rs b/src/linkml/export.rs index de1a85c..91b7c0e 100644 --- a/src/linkml/export.rs +++ b/src/linkml/export.rs @@ -1,8 +1,48 @@ +/* + * Copyright (c) 2024 Jan Range + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + //! Provides functionality to export data models to LinkML format. //! //! This module contains implementations for converting internal data model representations //! to LinkML schema format. It handles the conversion of objects, attributes, and enumerations //! to their corresponding LinkML representations. +//! +//! The module provides several key components: +//! - Serialization of DataModel instances to LinkML YAML format +//! - Conversion implementations between internal model types and LinkML schema types +//! - Utilities for handling global slots and attribute sharing between classes +//! - Pattern constraint management through slot usage +//! +//! The conversion process preserves: +//! - Documentation and descriptions +//! - Data types and ranges +//! - Cardinality constraints +//! - Identifier flags +//! - Required/optional status +//! - URI/term mappings +//! - Enumeration values and meanings +//! - Minimum/maximum value constraints +//! - Pattern validation rules use std::{collections::HashMap, error::Error, path::PathBuf}; @@ -13,23 +53,26 @@ use crate::{ }; use super::schema::{ - AttributeDefinition, ClassDefinition, EnumDefinition, LinkML, PermissibleValue, SlotUsage, + AttributeDefinition, ClassDefinition, EnumDefinition, Example, LinkML, PermissibleValue, + SlotUsage, }; /// Serializes a DataModel to LinkML YAML format and writes it to a file. /// /// This function takes a DataModel instance and converts it to LinkML schema format, -/// then serializes it to YAML and writes the output to the specified file path. +/// then serializes it to YAML. If an output path is provided, the YAML will be written +/// to that file. The function returns the serialized YAML string regardless of whether +/// it was written to a file. /// /// # Arguments /// /// * `model` - The DataModel to serialize -/// * `out` - The output path to write the YAML to, if provided. +/// * `out` - Optional output path to write the YAML to /// /// # Returns /// -/// * `Ok(yaml)` if serialization and file writing succeed -/// * `Err(Box)` if YAML serialization fails or if file writing fails +/// * `Ok(String)` - The serialized YAML string +/// * `Err(Box)` - If serialization or file writing fails pub fn serialize_linkml(model: DataModel, out: Option<&PathBuf>) -> Result> { let linkml = LinkML::from(model); let yaml = serde_yaml::to_string(&linkml)?; @@ -44,19 +87,16 @@ pub fn serialize_linkml(model: DataModel, out: Option<&PathBuf>) -> Result for LinkML { /// Converts a DataModel instance into a LinkML schema. /// - /// This conversion handles: - /// - Basic configuration (id, prefixes, name) - /// - Classes and their attributes - /// - Global slots (shared attributes) - /// - Enumerations - /// - /// # Arguments - /// - /// * `model` - The DataModel to convert - /// - /// # Returns + /// This conversion process handles: + /// - Basic schema configuration including ID, prefixes, and name + /// - Class definitions and their attributes + /// - Global slots (shared attributes across classes) + /// - Enumeration definitions + /// - Import declarations + /// - Default type configurations /// - /// A LinkML schema representing the data model + /// The conversion maintains the hierarchical structure of the data model while + /// adapting it to LinkML's schema format requirements. fn from(model: DataModel) -> Self { // Basic configuration let config = model.clone().config.unwrap_or_default(); @@ -108,7 +148,14 @@ impl From for LinkML { /// Extracts global slots (shared attributes) from a data model. /// -/// Global slots are attributes that appear in multiple classes with the same definition. +/// Global slots are attributes that appear in multiple classes with identical definitions. +/// This function identifies such attributes and extracts them to be defined at the schema level +/// rather than within individual classes. +/// +/// The extraction process: +/// 1. Collects all attributes from all classes +/// 2. Identifies attributes that appear multiple times with identical definitions +/// 3. Returns these as global slots /// /// # Arguments /// @@ -145,15 +192,18 @@ fn extract_slots(model: &DataModel) -> HashMap { /// Updates a class definition to use global slots where appropriate. /// -/// This function: -/// 1. Identifies which of the class's attributes are global slots +/// This function modifies a class definition to reference global slots instead of +/// duplicating attribute definitions. It performs the following steps: +/// 1. Identifies which of the class's attributes match global slot definitions /// 2. Adds references to those slots in the class's slots list -/// 3. Removes those attributes from the class's local attributes +/// 3. Removes the matching attributes from the class's local attributes +/// +/// This process helps reduce redundancy and maintain consistency across the schema. /// /// # Arguments /// /// * `class` - The class definition to update -/// * `slots` - The map of global slots +/// * `slots` - The map of global slots to reference fn remove_global_slots(class: &mut ClassDefinition, slots: &HashMap) { // Get the class's attributes let class_attrs = class.attributes.clone().unwrap_or_default(); @@ -179,18 +229,12 @@ fn remove_global_slots(class: &mut ClassDefinition, slots: &HashMap for ClassDefinition { /// Converts an Object into a LinkML ClassDefinition. /// - /// This handles: + /// This conversion process handles: /// - Converting attributes to LinkML format /// - Setting up slot usage for pattern constraints /// - Preserving documentation and URI terms - /// - /// # Arguments - /// - /// * `obj` - The Object to convert - /// - /// # Returns - /// - /// A LinkML ClassDefinition + /// - Maintaining inheritance relationships + /// - Managing attribute constraints and validations fn from(obj: Object) -> Self { // Create a map of attributes let attrib = obj @@ -217,7 +261,8 @@ impl From for ClassDefinition { description: Some(obj.docstring), class_uri: obj.term, slots: Vec::new(), - is_a: Some(obj.name), + is_a: obj.parent, + mixins: vec![], tree_root: None, attributes: Some(attrib), slot_usage: if slot_usage.is_empty() { @@ -233,22 +278,30 @@ impl From for ClassDefinition { impl From for AttributeDefinition { /// Converts an Attribute into a LinkML AttributeDefinition. /// - /// This preserves: + /// This conversion preserves: /// - Array/multivalued status /// - Data type (range) /// - Documentation /// - ID status /// - Required status - /// - /// # Arguments - /// - /// * `attribute` - The Attribute to convert - /// - /// # Returns - /// - /// A LinkML AttributeDefinition + /// - Minimum and maximum values + /// - Examples + /// - Term mappings fn from(attribute: Attribute) -> Self { + let minimum_value = attribute.options.iter().find(|o| o.key() == "minimum"); + let maximum_value = attribute.options.iter().find(|o| o.key() == "maximum"); + let example = attribute + .options + .iter() + .filter(|o| o.key() == "example") + .map(|o| Example { + value: Some(o.value()), + description: None, + }) + .collect::>(); + AttributeDefinition { + slot_uri: attribute.term, multivalued: Some(attribute.is_array), range: if attribute.dtypes[0] == "string" { None @@ -259,10 +312,10 @@ impl From for AttributeDefinition { identifier: Some(attribute.is_id), required: Some(attribute.required), readonly: None, - minimum_value: None, - maximum_value: None, + minimum_value: minimum_value.map(|v| v.value().parse::().unwrap()), + maximum_value: maximum_value.map(|v| v.value().parse::().unwrap()), recommended: None, - examples: None, + examples: example, annotations: None, } } @@ -272,17 +325,11 @@ impl From for AttributeDefinition { impl From for EnumDefinition { /// Converts an Enumeration into a LinkML EnumDefinition. /// - /// This preserves: - /// - Documentation + /// This conversion process handles: + /// - Documentation preservation /// - Enumeration values and their meanings - /// - /// # Arguments - /// - /// * `enum_` - The Enumeration to convert - /// - /// # Returns - /// - /// A LinkML EnumDefinition + /// - Value descriptions + /// - Semantic mappings fn from(enum_: Enumeration) -> Self { let mut values = HashMap::new(); for (key, value) in enum_.mappings.iter() { @@ -304,6 +351,7 @@ impl From for EnumDefinition { #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use std::{collections::BTreeMap, path::PathBuf}; use crate::option::AttrOption; @@ -313,14 +361,14 @@ mod tests { #[test] fn serialize_linkml_test() { let model = DataModel::from_markdown(&PathBuf::from("tests/data/model.md")).unwrap(); - let yaml = serialize_linkml(model, None).unwrap(); + let yaml = serde_yaml::from_str::(&serialize_linkml(model, None).unwrap()).unwrap(); let expected_yaml = serde_yaml::from_str::( &std::fs::read_to_string("tests/data/expected_linkml.yml").unwrap(), ) .unwrap(); - let yaml_yaml = serde_yaml::from_str::(&yaml).unwrap(); - assert_eq!(yaml_yaml, expected_yaml); + + assert_eq!(yaml, expected_yaml); } #[test] @@ -343,7 +391,7 @@ mod tests { class_def.class_uri, Some("http://example.org/TestClass".to_string()) ); - assert_eq!(class_def.is_a, Some("TestClass".to_string())); + assert!(class_def.is_a.is_none()); assert!(class_def.slot_usage.is_some()); } From b84f902a92d29846fa16825414ad2cfb1412d727 Mon Sep 17 00:00:00 2001 From: Jan Range <30547301+JR-1991@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:46:26 +0100 Subject: [PATCH 6/6] fix clippy issues --- src/linkml/import.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/linkml/import.rs b/src/linkml/import.rs index 4cec92c..679f309 100644 --- a/src/linkml/import.rs +++ b/src/linkml/import.rs @@ -273,7 +273,7 @@ mod tests { .objects .iter() .find(|o| o.name == obj.name) - .expect(&format!("Object {} not found", obj.name)); + .unwrap_or_else(|| panic!("Object {} not found", obj.name)); assert_eq!(obj.name, other_obj.name, "Object name mismatch"); assert_eq!( obj.docstring, other_obj.docstring, @@ -291,7 +291,7 @@ mod tests { .attributes .iter() .find(|a| a.name == attr.name) - .expect(&format!("Attribute {} not found", attr.name)); + .unwrap_or_else(|| panic!("Attribute {} not found", attr.name)); assert_eq!(attr.name, other_attr.name, "Attribute name mismatch"); } } @@ -301,7 +301,7 @@ mod tests { .enums .iter() .find(|e| e.name == enum_.name) - .expect(&format!("Enum {} not found", enum_.name)); + .unwrap_or_else(|| panic!("Enum {} not found", enum_.name)); assert_eq!(enum_.name, other_enum.name, "Enum name mismatch"); assert_eq!( enum_.docstring, other_enum.docstring,