citum_schema_style/style/model.rs
1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
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 super::diagnostics::validate_raw_style(&raw).map_err(serde_yaml::Error::custom)?;
99 let mut style: Style = serde_yaml::from_value(raw.clone())?;
100 style.raw_yaml = Some(raw);
101 style
102 .validate_resource_limits()
103 .map_err(serde_yaml::Error::custom)?;
104 Ok(style)
105 }
106
107 /// Parse a Citum style from YAML bytes, preserving raw YAML for
108 /// null-aware overlay merging during preset resolution.
109 ///
110 /// # Errors
111 ///
112 /// Returns a serde error if YAML parsing or deserialization fails.
113 pub fn from_yaml_bytes(bytes: &[u8]) -> Result<Self, serde_yaml::Error> {
114 let raw: serde_yaml::Value = serde_yaml::from_slice(bytes)?;
115 super::diagnostics::validate_raw_style(&raw).map_err(serde_yaml::Error::custom)?;
116 let mut style: Style = serde_yaml::from_value(raw.clone())?;
117 style.raw_yaml = Some(raw);
118 style
119 .validate_resource_limits()
120 .map_err(serde_yaml::Error::custom)?;
121 Ok(style)
122 }
123}