Skip to main content

citum_schema_style/
version.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Style schema version and resource-limit constants.
7
8#[cfg(doc)]
9use crate::Style;
10#[cfg(feature = "schema")]
11use schemars::JsonSchema;
12
13/// Canonical Citum style schema version used when `Style.version` is omitted.
14pub const STYLE_SCHEMA_VERSION: &str = "0.64.0";
15
16/// Maximum accepted nesting depth for authored template groups and fallbacks.
17pub const MAX_TEMPLATE_NESTING_DEPTH: usize = 64;
18
19/// Maximum accepted authored template components in one style.
20pub const MAX_TEMPLATE_COMPONENTS: usize = 16_384;
21
22/// A schema version (major.minor).
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[cfg_attr(feature = "schema", derive(JsonSchema), schemars(with = "String"))]
25pub struct SchemaVersion {
26    /// Major version number.
27    pub major: u32,
28    /// Minor version number (None if not provided in string).
29    pub minor: Option<u32>,
30    /// Patch version number (None if not provided in string).
31    pub patch: Option<u32>,
32}
33
34impl SchemaVersion {
35    /// Parse a version string into a `SchemaVersion`.
36    ///
37    /// Requires at least "X.Y". Supports "X.Y.Z".
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if the string is not a valid version format
42    /// or lacks the required minor version.
43    pub fn parse(s: &str) -> Result<Self, String> {
44        let parts: Vec<&str> = s.split('.').collect();
45        if parts.len() < 2 || parts.len() > 3 {
46            return Err(format!(
47                "invalid version format (expected X.Y or X.Y.Z): \"{}\"",
48                s
49            ));
50        }
51
52        let major_str = parts
53            .first()
54            .ok_or_else(|| "missing major version".to_string())?;
55        let major = major_str
56            .parse::<u32>()
57            .map_err(|_| format!("invalid major version: \"{}\"", major_str))?;
58
59        let minor_str = parts
60            .get(1)
61            .ok_or_else(|| "missing minor version".to_string())?;
62        let minor = Some(
63            minor_str
64                .parse::<u32>()
65                .map_err(|_| format!("invalid minor version: \"{}\"", minor_str))?,
66        );
67
68        let patch = if let Some(patch_str) = parts.get(2) {
69            Some(
70                patch_str
71                    .parse::<u32>()
72                    .map_err(|_| format!("invalid patch version: \"{}\"", patch_str))?,
73            )
74        } else {
75            None
76        };
77
78        Ok(SchemaVersion {
79            major,
80            minor,
81            patch,
82        })
83    }
84}
85
86impl PartialOrd for SchemaVersion {
87    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
88        Some(self.cmp(other))
89    }
90}
91
92impl Ord for SchemaVersion {
93    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
94        match self.major.cmp(&other.major) {
95            std::cmp::Ordering::Equal => {
96                let self_minor = self.minor.unwrap_or(0);
97                let other_minor = other.minor.unwrap_or(0);
98                match self_minor.cmp(&other_minor) {
99                    std::cmp::Ordering::Equal => {
100                        let self_patch = self.patch.unwrap_or(0);
101                        let other_patch = other.patch.unwrap_or(0);
102                        self_patch.cmp(&other_patch)
103                    }
104                    ord => ord,
105                }
106            }
107            ord => ord,
108        }
109    }
110}
111
112impl std::fmt::Display for SchemaVersion {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        write!(f, "{}", self.major)?;
115        if let Some(minor) = self.minor {
116            write!(f, ".{}", minor)?;
117            if let Some(patch) = self.patch {
118                write!(f, ".{}", patch)?;
119            }
120        }
121        Ok(())
122    }
123}
124
125impl Default for SchemaVersion {
126    #[allow(
127        clippy::expect_used,
128        reason = "STYLE_SCHEMA_VERSION is a canonical constant"
129    )]
130    fn default() -> Self {
131        SchemaVersion::parse(STYLE_SCHEMA_VERSION).expect("STYLE_SCHEMA_VERSION is valid")
132    }
133}
134
135impl<'de> serde::Deserialize<'de> for SchemaVersion {
136    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
137    where
138        D: serde::Deserializer<'de>,
139    {
140        let s = <String as serde::Deserialize>::deserialize(deserializer)?;
141        SchemaVersion::parse(&s).map_err(serde::de::Error::custom)
142    }
143}
144
145impl serde::Serialize for SchemaVersion {
146    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
147    where
148        S: serde::Serializer,
149    {
150        serializer.serialize_str(&self.to_string())
151    }
152}