Skip to main content

mdmodels_core/linkml/
schema.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//! LinkML schema definitions for parsing and representing LinkML schemas in Rust.
25//!
26//! This module provides Rust structs that map directly to the YAML structure of LinkML schemas.
27//! Each struct is annotated with serde derive macros to enable serialization/deserialization.
28
29use indexmap::IndexMap;
30use serde::{Deserialize, Serialize};
31
32/// The root schema object that contains all LinkML schema definitions
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct LinkML {
35    /// Unique identifier for the schema
36    pub id: String,
37    /// Name of the schema
38    pub name: String,
39    /// Title of the schema
40    pub title: String,
41    /// Optional description of the schema's purpose
42    #[serde(
43        default,
44        skip_serializing_if = "Option::is_none",
45        deserialize_with = "remove_newlines"
46    )]
47    pub description: Option<String>,
48    /// Optional license identifier
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub license: Option<String>,
51    /// List of related resources and references
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub see_also: Vec<String>,
54    /// Mapping of prefix strings to their expanded URI forms
55    #[serde(skip_serializing_if = "IndexMap::is_empty")]
56    pub prefixes: IndexMap<String, String>,
57    /// Default prefix to use when none is specified
58    pub default_prefix: String,
59    /// Default range type for slots
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub default_range: Option<String>,
62    /// List of imported schemas
63    #[serde(default, skip_serializing_if = "Vec::is_empty")]
64    pub imports: Vec<String>,
65    /// Map of class definitions
66    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
67    pub classes: IndexMap<String, ClassDefinition>,
68    /// Map of slot definitions
69    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
70    pub slots: IndexMap<String, AttributeDefinition>,
71    /// Map of enum definitions
72    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
73    pub enums: IndexMap<String, EnumDefinition>,
74}
75
76/// Represents a contributor to the schema
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78pub struct Contributor {
79    /// Name of the contributor
80    pub name: String,
81}
82
83/// Defines a subset of schema elements
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85pub struct Subset {
86    /// Optional description of the subset's purpose
87    #[serde(
88        default,
89        skip_serializing_if = "Option::is_none",
90        deserialize_with = "remove_newlines"
91    )]
92    pub description: Option<String>,
93}
94
95/// Defines a custom data type
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
97pub struct TypeDefinition {
98    /// Base type that this type extends
99    pub base: String,
100    /// Optional regex pattern for validation
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub pattern: Option<String>,
103    /// Optional description of the type
104    #[serde(
105        default,
106        skip_serializing_if = "Option::is_none",
107        deserialize_with = "remove_newlines"
108    )]
109    pub description: Option<String>,
110    /// Optional minimum value for numeric types
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub minimum_value: Option<i64>,
113}
114
115/// Defines an enumeration type
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
117pub struct EnumDefinition {
118    /// Optional description of the enum
119    #[serde(
120        default,
121        skip_serializing_if = "is_empty_string_option",
122        deserialize_with = "remove_newlines"
123    )]
124    pub description: Option<String>,
125    /// Map of allowed values and their definitions
126    #[serde(skip_serializing_if = "IndexMap::is_empty")]
127    pub permissible_values: IndexMap<String, PermissibleValue>,
128}
129
130/// Represents a single permissible value in an enum
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
132pub struct PermissibleValue {
133    /// Human-readable text for the value
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub text: Option<String>,
136    /// Optional URI defining the value's meaning
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub meaning: Option<String>,
139    /// Optional description of the value
140    #[serde(
141        default,
142        skip_serializing_if = "Option::is_none",
143        deserialize_with = "remove_newlines"
144    )]
145    pub description: Option<String>,
146}
147
148/// Defines a class in the schema
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
150pub struct ClassDefinition {
151    /// Optional description of the class
152    #[serde(
153        default,
154        skip_serializing_if = "is_empty_string_option",
155        deserialize_with = "remove_newlines"
156    )]
157    pub description: Option<String>,
158    /// Optional URI identifying the class
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub class_uri: Option<String>,
161    /// List of slots that this class can have
162    #[serde(default, skip_serializing_if = "Vec::is_empty")]
163    pub slots: Vec<String>,
164    /// Optional parent class name
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub is_a: Option<String>,
167    /// Whether this class is a tree root
168    #[serde(default, skip_serializing_if = "is_false_option")]
169    pub tree_root: Option<bool>,
170    /// Map of slot usage definitions
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub slot_usage: Option<IndexMap<String, SlotUsage>>,
173    /// Map of attributes
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub attributes: Option<IndexMap<String, AttributeDefinition>>,
176    /// Mixed in class
177    #[serde(default, skip_serializing_if = "Vec::is_empty")]
178    pub mixins: Vec<String>,
179}
180
181/// Represents an annotation on a schema element
182#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
183pub struct Annotation {
184    /// The annotation value
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    pub value: Option<String>,
187}
188
189/// Defines how a slot is used in a specific context
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
191pub struct SlotUsage {
192    /// Optional pattern for validation
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub pattern: Option<String>,
195}
196
197/// Defines a slot (property/field) in the schema
198#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
199pub struct AttributeDefinition {
200    /// Optional description of the slot
201    #[serde(
202        default,
203        skip_serializing_if = "is_empty_string_option",
204        deserialize_with = "remove_newlines"
205    )]
206    pub description: Option<String>,
207    /// Semantic type of the slot
208    #[serde(default, skip_serializing_if = "is_empty_string_option")]
209    pub slot_uri: Option<String>,
210    /// Whether this slot serves as an identifier
211    #[serde(default, skip_serializing_if = "is_false_option")]
212    pub identifier: Option<bool>,
213    /// Whether this slot is required
214    #[serde(default, skip_serializing_if = "is_false_option")]
215    pub required: Option<bool>,
216    /// Optional type range for the slot
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    pub range: Option<String>,
219    /// Whether this slot is read-only
220    #[serde(default, skip_serializing_if = "is_false_option")]
221    pub readonly: Option<bool>,
222    /// Whether this slot can have multiple values
223    #[serde(default, skip_serializing_if = "is_false_option")]
224    pub multivalued: Option<bool>,
225    /// Optional minimum value for numeric slots
226    #[serde(default, skip_serializing_if = "Option::is_none")]
227    pub minimum_value: Option<i64>,
228    /// Optional maximum value for numeric slots
229    #[serde(default, skip_serializing_if = "Option::is_none")]
230    pub maximum_value: Option<i64>,
231    /// Whether this slot is recommended
232    #[serde(default, skip_serializing_if = "is_false_option")]
233    pub recommended: Option<bool>,
234    /// Optional map of example values
235    #[serde(default, skip_serializing_if = "Vec::is_empty")]
236    pub examples: Vec<Example>,
237    /// Optional map of annotations
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    pub annotations: Option<IndexMap<String, Annotation>>,
240}
241
242/// Represents an example value for a slot
243#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
244pub struct Example {
245    /// The example value
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    pub value: Option<String>,
248    /// Optional description of the example
249    #[serde(
250        default,
251        skip_serializing_if = "is_empty_string_option",
252        deserialize_with = "remove_newlines"
253    )]
254    pub description: Option<String>,
255}
256
257// Helper functions
258fn remove_newlines<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
259where
260    D: serde::Deserializer<'de>,
261{
262    let opt = Option::<String>::deserialize(deserializer)?;
263    Ok(opt.map(|s| {
264        s.lines()
265            .map(|line| line.trim())
266            .collect::<Vec<&str>>()
267            .join(" ")
268    }))
269}
270
271fn is_empty_string_option(s: &Option<String>) -> bool {
272    s.is_none() || s.as_ref().unwrap().is_empty()
273}
274
275fn is_false_option(s: &Option<bool>) -> bool {
276    s.is_none() || !s.as_ref().unwrap()
277}