{# ============================================================================ #}
{# Global Macros for Type Handling #}
{# ============================================================================ #}
{# Determines if a type should be wrapped in Vec<...> for multiple values #}
{% macro is_multiple(attr) %}
{%- if attr.multiple -%}Vec<{%- endif -%}
{% endmacro %}
{# Closes the Vec<...> wrapper if needed #}
{% macro is_multiple_end(attr) %}
{%- if attr.multiple -%}>{%- endif -%}
{% endmacro %}
{# Converts schema type to Rust type #}
{% macro convert_basic_type(dtype) %}
{%- if dtype == "string" -%}
String
{%- elif dtype == "float" -%}
f64
{%- elif dtype == "integer" -%}
i64
{%- elif dtype == "boolean" -%}
bool
{%- else -%}
{{ dtype }}
{%- endif -%}
{% endmacro %}
{# Determines the appropriate Rust type based on attribute properties #}
{% macro get_type(attr, parent_name, object_names, wrap_index) %}
{%- if attr.xml.wrapped and attr.multiple and attr.dtypes[0] in object_names and wrap_index is not none -%}
{{ attr.xml.wrapped[wrap_index] | cap_first }}
{%- elif attr.dtypes | length > 1 -%}
{{ parent_name }}{{ attr.name | to_identifier | capitalize }}Type
{%- elif attr.dtypes[0] in object_names -%}
{{ attr.dtypes[0] }}
{%- else -%}
{{ convert_basic_type(attr.dtypes[0]) }}
{%- endif -%}
{% endmacro %}
{# Wraps types in Option<...> if they're optional and handles multiple values #}
{% macro wrap_type(attr, parent_name, object_names, wrap_index) %}
{%- if attr.required is false and not attr.multiple -%}
Option<{{ get_type(attr, parent_name, object_names, wrap_index) }}>
{%- else -%}
{{ is_multiple(attr) }}{{ get_type(attr, parent_name, object_names, wrap_index) }}{{ is_multiple_end(attr) }}
{%- endif -%}
{% endmacro %}
{# Adds builder attribute for handling multiple values in derive_builder #}
{% macro array_builder_setter(attr) %}
{%- if attr.multiple -%}
#[builder(setter(into, each(name = "to_{{ attr.name | to_identifier }}")))]
{%- endif -%}
{% endmacro %}
{# to_string for string types #}
{% macro to_string(attr) %}
{%- if attr.dtypes[0] == "string" -%}
.to_string()
{%- endif -%}
{% endmacro %}
{# Determines the appropriate builder attribute for a field #}
{% macro get_builder_attribute(attr) %}
{%- if attr.multiple -%}
#[builder(default, setter(into, each(name = "to_{{ attr.name | to_identifier }}")))]
{%- else -%}
{%- if attr.dtypes | length > 1 -%}
#[builder(default, setter(into))]
{%- elif attr.default -%}
#[builder(default = "{{ attr.default }}{{- to_string(attr) -}}.into()", setter(into))]
{%- elif attr.required is false -%}
#[builder(default, setter(into))]
{%- else -%}
#[builder(setter(into))]
{%- endif -%}
{%- endif -%}
{% endmacro %}
{# Determines the appropriate serde attribute for optional fields. #}
{# When the attribute name is not a valid Rust identifier (e.g. it contains a #}
{# dash like `coupling-scheme`), a `rename` is emitted so the original name is #}
{# used for both serialization and deserialization. #}
{% macro get_serde_attribute(attr) %}
{%- set ident = attr.name | to_identifier -%}
{%- if attr.required is false -%}
{%- if attr.multiple -%}
{%- if ident != attr.name -%}
#[serde(rename = "{{ attr.name }}", skip_serializing_if = "Vec::is_empty")]
{%- else -%}
#[serde(skip_serializing_if = "Vec::is_empty")]
{%- endif -%}
{%- else -%}
{%- if ident != attr.name -%}
#[serde(rename = "{{ attr.name }}", skip_serializing_if = "Option::is_none")]
{%- else -%}
#[serde(skip_serializing_if = "Option::is_none")]
{%- endif -%}
{%- endif -%}
{%- elif ident != attr.name -%}
#[serde(rename = "{{ attr.name }}")]
{%- endif -%}
{% endmacro %}
{# Adds .into() to optional fields #}
{% macro into(attr)%}
{%- if not attr.required -%}
.into()
{%- endif -%}
{% endmacro %}
{# Determines the appropriate derivative attribute for a field #}
{% macro get_derivative_attribute(attr) %}
{%- if attr.default and attr.dtypes | length == 1 -%}
{%- if attr.dtypes[0] == "string" -%}
#[derivative(Default(value = "\"{{ attr.default }}\".to_string()"))]
{%- else -%}
#[derivative(Default(value = "{{ attr.default }}{{- to_string(attr) -}}{{- into(attr) -}}"))]
{%- endif -%}
{%- else -%}
#[derivative(Default)]
{%- endif -%}
{% endmacro %}
{# Defines a default JSON-LD header for an object #}
{% macro get_jsonld_header(object) %}
pub fn default_{{ object.name | lower }}_jsonld_header() -> Option<JsonLdHeader> {
let mut context = SimpleContext::default();
// Add main prefix and repository URL
context.terms.insert("{{ prefix }}".to_string(), TermDef::Simple("{{ repo }}".to_string()));
// Add configured prefixes
{%- for prefix, address in prefixes %}
context.terms.insert("{{ prefix }}".to_string(), TermDef::Simple("{{ address }}".to_string()));
{%- endfor %}
// Add class-scoped prefixes
{%- for obj in objects %}
context.terms.insert("{{ obj.name }}".to_string(), TermDef::Simple("{{ repo }}{%- if repo[-1] != "/" -%}#{%- endif -%}{{ obj.name }}/".to_string()));
{%- endfor %}
{%- for enum in enums %}
context.terms.insert("{{ enum.name }}".to_string(), TermDef::Simple("{{ repo }}{%- if repo[-1] != "/" -%}#{%- endif -%}{{ enum.name }}/".to_string()));
{%- endfor %}
// Add attribute terms
{%- for attr in object.attributes %}
{%- if attr.is_id %}
context.terms.insert("{{ attr.name }}".to_string(), TermDef::Detailed(TermDetail {
{%- if attr.term %}
id: Some("{{ attr.term }}".to_string()),
{%- else %}
id: None,
{%- endif %}
type_: Some("@id".to_string()),
{%- if attr.multiple %}
container: Some("@list".to_string()),
{%- else %}
container: None,
{%- endif %}
context: None,
}));
{%- elif attr.term %}
{%- if attr.multiple %}
context.terms.insert("{{ attr.name }}".to_string(), TermDef::Detailed(TermDetail {
id: Some("{{ attr.term }}".to_string()),
type_: None,
container: Some("@list".to_string()),
context: None,
}));
{%- else %}
context.terms.insert("{{ attr.name }}".to_string(), TermDef::Simple("{{ attr.term }}".to_string()));
{%- endif %}
{%- endif %}
{%- endfor %}
Some(JsonLdHeader {
import: Vec::new(),
context: Some(JsonLdContext::Object(context)),
id: Some(format!("{{ prefix }}:{{ object.name }}/{}", uuid::Uuid::new_v4())),
type_: Some(TypeOrVec::Multi(vec![
"{{ prefix }}:{{ object.name }}".to_string(),
{%- if object.term %}
"{{ object.term }}".to_string(),
{%- endif %}
])),
})
}
{% endmacro %}
//! This file contains Rust struct definitions with serde serialization.
//!
//! WARNING: This is an auto-generated file.
//! Do not edit directly - any changes will be overwritten.
use derive_builder::Builder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use derivative::Derivative;
{%- if "jsonld" in config %}
use std::collections::HashMap;
use serde_json::Value;
use uuid;
{%- endif %}
//
// Type definitions
//
{%- for object in objects %}
{%- if object.name in objects_with_wrapped %}
#[derive(Debug, Default)]
pub struct {{ object.name }} {
{%- if "jsonld" in config %}
/// JSON-LD header
#[builder(default = "default_{{ object.name | lower }}_jsonld_header()")]
#[serde(flatten, default = "default_{{ object.name | lower }}_jsonld_header")]
pub jsonld: Option<JsonLdHeader>,
{% endif %}
{%- for attr in object.attributes %}
pub {{ attr.name | to_identifier }}: {{ wrap_type(attr, object.name, object_names, None) }},
{%- endfor %}
{%- if "jsonld" in config %}
/// Additional properties outside of the schema
#[serde(flatten)]
#[builder(default)]
pub additional_properties: Option<HashMap<String, Value>>,
{%- endif %}
}
impl<'de> Deserialize<'de> for {{ object.name }} {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename="{{ object.name }}")]
struct Helper {
{%- for attr in object.attributes %}
{%- if (attr.name | to_identifier) != attr.name %}
#[serde(rename = "{{ attr.name }}")]
{%- endif %}
pub {{ attr.name | to_identifier }}: {{ wrap_type(attr, object.name, object_names, 0) }},
{%- endfor %}
}
{%- for attr in object.attributes %}
{%- if attr.xml.wrapped %}
{%- for wrapped in attr.xml.wrapped %}
#[derive(Deserialize)]
struct {{ wrapped | cap_first }} {
{%- if loop.last %}
pub value: {{ wrap_type(attr, object.name, object_names, None) }}
{%- else %}
pub value: {{ wrap_type(attr, object.name, object_names, loop.index) }},
{%- endif %}
}
{%- endfor %}
{%- endif %}
{%- endfor %}
Ok(Self {
{%- for attr in object.attributes %}
{{ attr.name | to_identifier }},
{%- endfor %}
})
}
}
{%- else %}
{%- if object.docstring %}
/// {{ wrap(object.docstring, 70, "", "/// ", None) }}
{%- endif %}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Builder, Derivative)]
#[derivative(Default)]
#[serde(default)]
#[allow(non_snake_case)]
pub struct {{ object.name }} {
{%- if "jsonld" in config %}
/// JSON-LD header
#[serde(flatten)]
#[builder(default = "default_{{ object.name | lower }}_jsonld_header()")]
#[derivative(Default(value = "default_{{ object.name | lower }}_jsonld_header()"))]
pub jsonld: Option<JsonLdHeader>,
{% endif %}
{%- for i, attr in object.attributes | enumerate %}
{%- if i == 0 %}
{%- if attr.docstring%}
/// {{ wrap(attr.docstring, 70, "", " /// ", None) }}
{%- else %}
/// {{ attr.name }}
{%- endif %}
{%- else %}
{%- if attr.docstring%}
/// {{ wrap(attr.docstring, 70, "", " /// ", None) }}
{%- else %}
/// {{ attr.name }}
{%- endif %}
{%- endif %}
{{ get_serde_attribute(attr) }}
{{ get_builder_attribute(attr) }}
{{ get_derivative_attribute(attr) }}
pub {{ attr.name | to_identifier }}: {{ wrap_type(attr, object.name, object_names) }},
{%- endfor %}
{%- if "jsonld" in config %}
/// Additional properties outside of the schema
#[serde(flatten)]
#[builder(default)]
pub additional_properties: Option<HashMap<String, Value>>,
{%- endif %}
}
{%- endif %}
{% endfor %}
{%- if enums | length > 0 %}
//
// Enum definitions
//
{%- for enum in enums %}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
pub enum {{ enum.name }} {
{%- for key, value in enum.mappings | dictsort %}
{% if loop.first %}#[default]{% endif %}
#[serde(rename = "{{ trim(value, "_") }}")]
{{ key | pascal_case }},
{%- endfor %}
}
{% endfor %}
{%- endif %}
{%- for object in objects %}
{%- for attr in object.attributes %}
{%- if attr.dtypes | length > 1 %}
/// Union type for {{ object.name }}.{{ attr.name }}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum {{ object.name }}{{ attr.name | to_identifier | capitalize }}Type {
{%- for dtype in attr.dtypes %}
{%- if dtype in object_names %}
Object({{ dtype }}),
{%- else %}
{{ convert_basic_type(dtype) | capitalize }}({{ convert_basic_type(dtype) }}),
{%- endif %}
{%- endfor %}
}
{%- endif %}
{%- endfor %}
{%- endfor %}
{%- if "jsonld" in config %}
// Default JSON-LD header function for each object
{%- for object in objects %}
{{ get_jsonld_header(object) }}
{%- endfor %}
/// JSON-LD Header
///
/// JSON-LD (JavaScript Object Notation for Linked Data) provides a way to express
/// linked data using JSON syntax, enabling semantic web technologies and structured
/// data interchange with context and meaning preservation.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
pub struct JsonLdHeader {
/// JSON-LD context (IRI, object, or array)
#[serde(rename = "@context", skip_serializing_if = "Option::is_none")]
pub context: Option<JsonLdContext>,
/// Node identifier (IRI or blank node)
#[serde(rename = "@id", skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
// Import context from a file
#[serde(rename = "@import", skip_serializing_if = "Vec::is_empty")]
pub import: Vec<String>,
/// Type IRI(s) for the node, e.g. schema:Person
#[serde(rename = "@type", skip_serializing_if = "Option::is_none")]
pub type_: Option<TypeOrVec>,
}
impl Default for JsonLdHeader {
/// Returns the default JSON-LD header.
fn default() -> Self {
Self {
context: None,
id: None,
import: Vec::new(),
type_: None,
}
}
}
impl JsonLdHeader {
/// Adds a new term definition to the JSON-LD context, creating a context object if none exists.
///
/// This method provides a convenient way to extend the JSON-LD context with additional term
/// mappings, allowing for semantic annotation of properties and values within the document.
/// If the header does not already contain a context, a new SimpleContext object will be
/// created automatically to hold the term definition.
///
/// # Arguments
///
/// * `name` - The term name to be defined in the context
/// * `term` - The term definition, either a simple IRI mapping or a detailed definition
///
/// # Example
///
/// ```no_compile
/// let mut header = JsonLdHeader::default();
/// header.add_term("name", TermDef::Simple("https://schema.org/name".to_string()));
/// ```
pub fn add_term(&mut self, name: &str, term: TermDef) {
let context = self
.context
.get_or_insert_with(|| JsonLdContext::Object(SimpleContext::default()));
if let JsonLdContext::Object(object) = context {
object.terms.insert(name.to_string(), term);
}
}
/// Updates an existing term definition in the JSON-LD context or adds it if it doesn't exist.
///
/// This method functions similarly to add_term but provides clearer semantics when the
/// intention is to modify an existing term definition. The behavior is identical to add_term
/// as HashMap::insert will overwrite existing entries with the same key, but this method
/// name makes the intent more explicit in code that is updating rather than initially
/// defining terms.
///
/// # Arguments
///
/// * `name` - The term name to be updated in the context
/// * `term` - The new term definition to replace any existing definition
///
/// # Example
///
/// ```no_compile
/// let mut header = JsonLdHeader::default();
/// header.add_term("name", TermDef::Simple("https://schema.org/name".to_string()));
/// header.update_term("name", TermDef::Simple("https://example.org/fullName".to_string()));
/// ```
pub fn update_term(&mut self, name: &str, term: TermDef) {
let context = self
.context
.get_or_insert_with(|| JsonLdContext::Object(SimpleContext::default()));
if let JsonLdContext::Object(object) = context {
object.terms.insert(name.to_string(), term);
}
}
/// Removes a term definition from the JSON-LD context if it exists.
///
/// This method allows for the removal of previously defined terms from the JSON-LD context,
/// which can be useful when dynamically managing context definitions or when certain terms
/// are no longer needed in the semantic annotation of the document. The method will only
/// attempt removal if the context exists and is an object type; it will silently do nothing
/// if the context is missing or is not an object.
///
/// # Arguments
///
/// * `name` - The term name to be removed from the context
///
/// # Returns
///
/// Returns `true` if the term was found and removed, `false` if the term was not present
/// or if the context is not an object type.
///
/// # Example
///
/// ```no_compile
/// let mut header = JsonLdHeader::default();
/// header.add_term("name", TermDef::Simple("https://schema.org/name".to_string()));
/// let was_removed = header.remove_term("name");
/// assert!(was_removed);
/// ```
pub fn remove_term(&mut self, name: &str) -> bool {
if let Some(JsonLdContext::Object(object)) = &mut self.context {
object.terms.remove(name).is_some()
} else {
false
}
}
/// Adds a new import to the JSON-LD context.
///
/// This method allows for the addition of additional context files to be imported into the
/// JSON-LD context, which can be useful when working with complex or large context definitions.
/// The method will simply append the new import to the existing list of imports, allowing for
/// flexible and dynamic management of context dependencies within the document.
///
/// # Arguments
/// * `import` - The IRI of the context file to be imported
pub fn add_import(&mut self, import: String) {
self.import.push(import);
}
}
/// Accept either a single type IRI or an array of them.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
#[serde(untagged)]
pub enum TypeOrVec {
Single(String),
Multi(Vec<String>),
}
/// JSON-LD Context:
/// - a single IRI (remote context)
/// - an inline context object
/// - or an array of these (merged sequentially)
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
#[serde(untagged)]
pub enum JsonLdContext {
Iri(String),
Object(SimpleContext),
Array(Vec<JsonLdContext>),
}
/// A simple inline @context object with essential global keys and term definitions.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, Eq, PartialEq)]
pub struct SimpleContext {
/// Base IRI used for relative resolution.
#[serde(rename = "@base", skip_serializing_if = "Option::is_none")]
pub base: Option<String>,
/// Default vocabulary IRI for terms without explicit IRIs.
#[serde(rename = "@vocab", skip_serializing_if = "Option::is_none")]
pub vocab: Option<String>,
/// Default language for string literals.
#[serde(rename = "@language", skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
/// Mapping of term → IRI or detailed definition.
#[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
pub terms: HashMap<String, TermDef>,
}
/// Term definition can be a simple mapping (string → IRI) or a detailed object.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
#[serde(untagged)]
pub enum TermDef {
/// Simple alias: `"name": "https://schema.org/name"`
Simple(String),
/// Expanded form with type coercion, container behavior, or nested context.
Detailed(TermDetail),
}
/// Detailed term definition (subset of JSON-LD 1.1 features).
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, Eq, PartialEq)]
pub struct TermDetail {
/// Absolute or relative IRI that the term expands to, or a keyword like "@id".
#[serde(rename = "@id", skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// Type coercion or value type ("@id", "@vocab", or datatype IRI).
#[serde(rename = "@type", skip_serializing_if = "Option::is_none")]
pub type_: Option<String>,
/// Container behavior ("@list", "@set", "@index", etc.).
#[serde(rename = "@container", skip_serializing_if = "Option::is_none")]
pub container: Option<String>,
/// Optional nested (scoped) context.
#[serde(rename = "@context", skip_serializing_if = "Option::is_none")]
pub context: Option<Box<JsonLdContext>>,
}
{%- endif %}