mdmodels_core/jsonld/schema.rs
1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3use serde_json::Value as JsonValue;
4
5/// Represents the JSON-LD document header, containing context, id, and type definitions.
6/// This structure is used as the envelope for JSON-LD documents and nodes.
7///
8/// The fields correspond to the standard JSON-LD keywords:
9/// - `@context`: Describes the term definitions and mapping information; can be an IRI, an object, or an array of contexts.
10/// - `@id`: The node identifier, usually as an IRI string.
11/// - `@type`: The semantic type(s) of the node as a string or array of strings.
12#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Default)]
13pub struct JsonLdHeader {
14 #[serde(rename = "@context", skip_serializing_if = "Option::is_none")]
15 pub context: Option<JsonLdContext>,
16
17 #[serde(rename = "@id", skip_serializing_if = "Option::is_none")]
18 pub id: Option<String>,
19
20 #[serde(rename = "@type", skip_serializing_if = "Option::is_none")]
21 pub type_: Option<TypeOrVec>,
22}
23
24impl JsonLdHeader {
25 /// Add a new term definition to the context, creating the context object if not present.
26 ///
27 /// # Arguments
28 ///
29 /// * `name` - The key/term to be added to the `@context`
30 /// * `term` - The associated mapping (simple IRI, detailed mapping, or null)
31 pub fn add_term(&mut self, name: &str, term: TermDef) {
32 let ctx = self
33 .context
34 .get_or_insert_with(|| JsonLdContext::Object(SimpleContext::default()));
35 if let JsonLdContext::Object(object) = ctx {
36 object.terms.insert(name.to_string(), term);
37 }
38 }
39
40 /// Insert or replace a term definition in the context.
41 pub fn update_term(&mut self, name: &str, term: TermDef) {
42 self.add_term(name, term);
43 }
44
45 /// Remove a term from the context. Returns true if the term existed and was removed.
46 pub fn remove_term(&mut self, name: &str) -> bool {
47 if let Some(JsonLdContext::Object(object)) = &mut self.context {
48 object.terms.shift_remove(name).is_some()
49 } else {
50 false
51 }
52 }
53
54 /// Add an `@import` field as required by JSON-LD 1.1.
55 /// If `@import` already exists, merges multiple contexts into an array.
56 ///
57 /// # Arguments
58 ///
59 /// * `import_url` - The URL (IRI) to include with `@import`.
60 pub fn add_import(&mut self, import_url: impl Into<String>) {
61 let import_url = import_url.into();
62 match self.context.take() {
63 None => {
64 let obj = SimpleContext {
65 import: Some(import_url),
66 ..Default::default()
67 };
68 self.context = Some(JsonLdContext::Object(obj));
69 }
70 Some(JsonLdContext::Object(mut obj)) => {
71 if obj.import.is_none() {
72 obj.import = Some(import_url);
73 self.context = Some(JsonLdContext::Object(obj));
74 } else {
75 let arr = vec![
76 JsonLdContext::Object(obj),
77 JsonLdContext::Object(SimpleContext {
78 import: Some(import_url),
79 ..Default::default()
80 }),
81 ];
82 self.context = Some(JsonLdContext::Array(arr));
83 }
84 }
85 Some(JsonLdContext::Iri(iri)) => {
86 let arr = vec![
87 JsonLdContext::Iri(iri),
88 JsonLdContext::Object(SimpleContext {
89 import: Some(import_url),
90 ..Default::default()
91 }),
92 ];
93 self.context = Some(JsonLdContext::Array(arr));
94 }
95 Some(JsonLdContext::Array(mut arr)) => {
96 arr.push(JsonLdContext::Object(SimpleContext {
97 import: Some(import_url),
98 ..Default::default()
99 }));
100 self.context = Some(JsonLdContext::Array(arr));
101 }
102 }
103 }
104}
105
106/// Represents the possible values for the `@type` field in JSON-LD nodes.
107/// Accepts a single type string or an array of type strings.
108#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
109#[serde(untagged)]
110pub enum TypeOrVec {
111 /// A single type annotation as a string
112 Single(String),
113 /// Multiple types as a vector of strings
114 Multi(Vec<String>),
115}
116
117/// Describes how a JSON-LD `@context` may be represented:
118/// as a remote IRI, an inline object, or a list of multiple contexts.
119///
120/// This flexibility enables referencing remote contexts, inlining ad-hoc mappings,
121/// or combining several contexts in a single document.
122#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
123#[serde(untagged)]
124#[allow(clippy::large_enum_variant)]
125pub enum JsonLdContext {
126 /// The context is a remote document specified by an IRI.
127 Iri(String),
128 /// The context is a locally inlined object containing mappings and settings.
129 Object(SimpleContext),
130 /// The context is a list of IRIs and/or objects, processed in order.
131 Array(Vec<JsonLdContext>),
132}
133
134/// Defines the structure of an inline JSON-LD `@context` object, supporting:
135/// - reserved context keywords (e.g., `@base`, `@vocab`, `@protected`)
136/// - term mappings for expanding compact keys to IRIs or more detailed definitions
137///
138/// Most reserved keywords are optional.
139/// Doubly-Option fields (e.g., `Option<Option<String>>`) allow distinguishing between
140/// omitted, null, or non-null values when serializing.
141#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)]
142pub struct SimpleContext {
143 /// `@import`: Import external context definition (JSON-LD 1.1 feature).
144 #[serde(rename = "@import", skip_serializing_if = "Option::is_none")]
145 pub import: Option<String>,
146
147 /// `@type`: The type of the context object.
148 #[serde(rename = "@type", skip_serializing_if = "Option::is_none")]
149 pub type_: Option<TypeOrVec>,
150
151 /// `@version`: Indicates the JSON-LD version this context targets.
152 #[serde(rename = "@version", skip_serializing_if = "Option::is_none")]
153 pub version: Option<String>,
154
155 /// `@base`: The default base IRI; Some(None) serializes to null (explicitly clear).
156 #[serde(rename = "@base", skip_serializing_if = "Option::is_none")]
157 pub base: Option<Option<String>>,
158
159 /// `@vocab`: The default vocabulary IRI; Some(None) outputs null for explicit clearing.
160 #[serde(rename = "@vocab", skip_serializing_if = "Option::is_none")]
161 pub vocab: Option<Option<String>>,
162
163 /// `@language`: Default language code. Some(None) serializes as null.
164 #[serde(rename = "@language", skip_serializing_if = "Option::is_none")]
165 pub language: Option<Option<String>>,
166
167 /// `@direction`: Base direction for string values ("ltr" or "rtl").
168 #[serde(rename = "@direction", skip_serializing_if = "Option::is_none")]
169 pub direction: Option<String>,
170
171 /// `@protected`: When true, marks all term definitions in this context as protected.
172 #[serde(rename = "@protected", skip_serializing_if = "Option::is_none")]
173 pub protected: Option<bool>,
174
175 /// Key-value term definitions (mappings or detailed definitions for context terms).
176 #[serde(flatten, skip_serializing_if = "IndexMap::is_empty", default)]
177 pub terms: IndexMap<String, TermDef>,
178}
179
180/// Represents a context term mapping:
181/// - as a simple IRI string,
182/// - as a detailed object (`TermDetail`) with advanced options,
183/// - or explicitly `null` to remove or overwrite a term definition.
184///
185/// The null variant uses `serde_json::Value::Null` under the hood to allow proper null emission in serialization.
186#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
187#[serde(untagged)]
188pub enum TermDef {
189 /// Direct mapping to an IRI (string).
190 Simple(String),
191 /// Full-featured definition using advanced JSON-LD keyword fields.
192 Detailed(TermDetail),
193 /// Explicit null value for term removal or overriding.
194 Null(JsonValue), // Must be JsonValue::Null in use.
195}
196
197/// Provides a full mapping for a term in a JSON-LD context, supporting advanced features such as:
198/// type coercion, container types, nested context definition, language, protection, prefixing, reverse properties, and data nesting.
199///
200/// Each field is optional and maps to the appropriate JSON-LD 1.1 feature.
201#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)]
202pub struct TermDetail {
203 /// The IRI or keyword that this term maps to (`@id`).
204 #[serde(rename = "@id", skip_serializing_if = "Option::is_none")]
205 pub id: Option<String>,
206
207 /// Type coercion target for values (`@type`). E.g., "@id", "@vocab", or custom IRI.
208 #[serde(rename = "@type", skip_serializing_if = "Option::is_none")]
209 pub type_: Option<String>,
210
211 /// Specifies container type(s) (e.g., ["@set"], "@list"). Single value or array.
212 #[serde(rename = "@container", skip_serializing_if = "Option::is_none")]
213 pub container: Option<OneOrMany<String>>,
214
215 /// Nested context (`@context`) to use for this term, if any.
216 #[serde(rename = "@context", skip_serializing_if = "Option::is_none")]
217 pub context: Option<Box<JsonLdContext>>,
218
219 /// Marks this context term as protected (`@protected`).
220 #[serde(rename = "@protected", skip_serializing_if = "Option::is_none")]
221 pub protected: Option<bool>,
222
223 /// Whether this term provides prefix expansion (`@prefix`).
224 #[serde(rename = "@prefix", skip_serializing_if = "Option::is_none")]
225 pub prefix: Option<bool>,
226
227 /// Defines a reverse property mapping (`@reverse`).
228 #[serde(rename = "@reverse", skip_serializing_if = "Option::is_none")]
229 pub reverse: Option<String>,
230
231 /// Directs compacted output to nest data at this property (`@nest`).
232 #[serde(rename = "@nest", skip_serializing_if = "Option::is_none")]
233 pub nest: Option<String>,
234
235 /// Assigns the property to a named index (`@index`).
236 #[serde(rename = "@index", skip_serializing_if = "Option::is_none")]
237 pub index: Option<String>,
238
239 /// Sets the default language for string values in this term. If Some(None), serializes as explicit null.
240 #[serde(rename = "@language", skip_serializing_if = "Option::is_none")]
241 pub language: Option<Option<String>>,
242}
243
244/// Utility type that accepts either a single element or a list, used for keywords like
245/// `@container` which can take a string or array of strings in JSON-LD.
246#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
247#[serde(untagged)]
248pub enum OneOrMany<T> {
249 /// Single value.
250 One(T),
251 /// Multiple values.
252 Many(Vec<T>),
253}
254
255impl TermDef {
256 /// Convenience constructor for a null-valued term, meaning term removal or explicit undefinition.
257 pub fn null() -> Self {
258 TermDef::Null(JsonValue::Null)
259 }
260}
261
262impl SimpleContext {
263 /// Ensures the inline context explicitly contains `@version: 1.1`.
264 /// Returns self for fluent usage.
265 pub fn ensure_v11(mut self) -> Self {
266 self.version = Some("1.1".to_string());
267 self
268 }
269}