Skip to main content

citum_schema_style/style/
model.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus
4*/
5
6//! The Citum style model.
7
8use std::collections::HashMap;
9
10#[cfg(feature = "schema")]
11use schemars::JsonSchema;
12use serde::de::Error as _;
13use serde::{Deserialize, Serialize};
14
15#[allow(unused_imports, reason = "Referenced by intra-doc links.")]
16use crate::ResolutionError;
17use crate::style_base;
18use crate::{BibliographySpec, CitationSpec, Config, SchemaVersion, StyleInfo, Template};
19
20/// The new Citum Style model.
21///
22/// This is the target schema for Citum, featuring declarative options
23/// and simple template components instead of procedural conditionals.
24#[derive(Debug, Default, Deserialize, Serialize, Clone)]
25#[cfg_attr(feature = "schema", derive(JsonSchema))]
26#[serde(rename_all = "kebab-case")]
27pub struct Style {
28    /// Style schema version.
29    #[serde(default)]
30    pub version: SchemaVersion,
31    /// Style metadata.
32    #[serde(default)]
33    pub info: StyleInfo,
34    /// Named reusable templates.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub templates: Option<HashMap<String, Template>>,
37    /// Global style options.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub options: Option<Config>,
40    /// Citation specification.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub citation: Option<CitationSpec>,
43    /// Bibliography specification.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub bibliography: Option<BibliographySpec>,
46    /// Custom user-defined fields for extensions.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub custom: Option<HashMap<String, serde_json::Value>>,
49    /// Extends a base style, with optional local overrides.
50    ///
51    /// When present, the base [`StyleReference`](style_base::StyleReference) is resolved and the local
52    /// overrides are merged before any further processing. Explicit `options`,
53    /// `citation`, and `bibliography` keys at the same document level take
54    /// precedence over the resolved base.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub extends: Option<style_base::StyleReference>,
57    /// Optional content-addressed integrity pin for the parent style referenced
58    /// by [`extends`](Self::extends).
59    ///
60    /// When present, the resolver verifies that the SHA-256 of the fetched
61    /// parent matches this CIDv1 string before merging. Mismatches abort
62    /// resolution with [`ResolutionError::IntegrityFailure`]. Absent means
63    /// "no integrity check" — appropriate for `file://` parents under user
64    /// control or trusted local registries.
65    #[serde(rename = "extends-pin", skip_serializing_if = "Option::is_none")]
66    pub extends_pin: Option<String>,
67    /// Raw YAML captured when the style was loaded via [`Style::from_yaml_str`]
68    /// or [`Style::from_yaml_bytes`]. Used during style resolution for
69    /// null-aware overlay merging (e.g., `ibid: ~` correctly clears an
70    /// inherited preset value). Absent in programmatically-constructed styles.
71    #[cfg_attr(feature = "schema", schemars(skip))]
72    #[serde(skip, default)]
73    pub raw_yaml: Option<serde_yaml::Value>,
74    /// Forward-compat: captures unknown keys when an older engine reads a
75    /// style produced by a newer schema. Empty by default; treated as a
76    /// SoftDegrade signal. See `docs/specs/FORWARD_COMPATIBILITY.md`.
77    #[serde(
78        flatten,
79        default,
80        skip_serializing_if = "std::collections::BTreeMap::is_empty"
81    )]
82    #[cfg_attr(feature = "schema", schemars(skip))]
83    pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
84}
85
86impl Style {
87    /// Parse a Citum style from a YAML string, preserving raw YAML for
88    /// null-aware overlay merging during base resolution.
89    ///
90    /// Preferred over `serde_yaml::from_str` when the style extends a base,
91    /// so that `ibid: ~` and similar null overrides correctly clear inherited values.
92    ///
93    /// # Errors
94    ///
95    /// Returns a serde error if YAML parsing or deserialization fails.
96    pub fn from_yaml_str(s: &str) -> Result<Self, serde_yaml::Error> {
97        let raw: serde_yaml::Value = serde_yaml::from_str(s)?;
98        let mut style: Style = serde_yaml::from_value(raw.clone())?;
99        style.raw_yaml = Some(raw);
100        style
101            .validate_resource_limits()
102            .map_err(serde_yaml::Error::custom)?;
103        Ok(style)
104    }
105
106    /// Parse a Citum style from YAML bytes, preserving raw YAML for
107    /// null-aware overlay merging during preset resolution.
108    ///
109    /// # Errors
110    ///
111    /// Returns a serde error if YAML parsing or deserialization fails.
112    pub fn from_yaml_bytes(bytes: &[u8]) -> Result<Self, serde_yaml::Error> {
113        let raw: serde_yaml::Value = serde_yaml::from_slice(bytes)?;
114        let mut style: Style = serde_yaml::from_value(raw.clone())?;
115        style.raw_yaml = Some(raw);
116        style
117            .validate_resource_limits()
118            .map_err(serde_yaml::Error::custom)?;
119        Ok(style)
120    }
121}