citum_schema_style/style/metadata.rs
1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Style metadata and classification types.
7
8#[cfg(feature = "schema")]
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12#[allow(unused_imports, reason = "Referenced by intra-doc links.")]
13use crate::ResolutionError;
14
15/// Discipline/field classification for a citation style.
16///
17/// Values correspond to the CSL 1.0 `<category field="..."/>` attribute,
18/// `generic-base` is silently ignored during migration.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[cfg_attr(feature = "schema", derive(JsonSchema))]
21#[serde(rename_all = "kebab-case")]
22pub enum CitationField {
23 /// Anthropology styles.
24 Anthropology,
25 /// Biology styles.
26 Biology,
27 /// Botany styles.
28 Botany,
29 /// Chemistry styles.
30 Chemistry,
31 /// Communications studies styles.
32 Communications,
33 /// Engineering styles.
34 Engineering,
35 /// Geography styles.
36 Geography,
37 /// Geology styles.
38 Geology,
39 /// History styles.
40 History,
41 /// Humanities styles.
42 Humanities,
43 /// Law styles.
44 Law,
45 /// Linguistics styles.
46 Linguistics,
47 /// Literature styles.
48 Literature,
49 /// Mathematics styles.
50 Math,
51 /// Medicine styles.
52 Medicine,
53 /// Philosophy styles.
54 Philosophy,
55 /// Physics styles.
56 Physics,
57 #[serde(rename = "political-science")]
58 /// Political science styles.
59 PoliticalScience,
60 /// Psychology styles.
61 Psychology,
62 /// General science styles.
63 Science,
64 #[serde(rename = "social-science")]
65 /// Social science styles.
66 SocialScience,
67 /// Sociology styles.
68 Sociology,
69 /// Theology styles.
70 Theology,
71 /// Zoology styles.
72 Zoology,
73}
74
75/// A hyperlink associated with a style (documentation, self-link, etc.).
76#[derive(Debug, Default, Clone, Deserialize, Serialize)]
77#[cfg_attr(feature = "schema", derive(JsonSchema))]
78#[serde(rename_all = "kebab-case")]
79pub struct StyleLink {
80 /// Link target for related style metadata.
81 pub href: String,
82 /// Relationship type for the link, such as `self` or `documentation`.
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub rel: Option<String>,
85}
86
87/// A person credit (author or contributor) for a style.
88#[derive(Debug, Default, Clone, Deserialize, Serialize)]
89#[cfg_attr(feature = "schema", derive(JsonSchema))]
90#[serde(rename_all = "kebab-case")]
91pub struct StylePerson {
92 /// Display name for the credited person.
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub name: Option<String>,
95 /// Contact email for the credited person.
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub email: Option<String>,
98 /// URI identifying the credited person.
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub uri: Option<String>,
101}
102
103/// Provenance block for styles adapted from a CSL 1.0 source.
104#[derive(Debug, Default, Clone, Deserialize, Serialize)]
105#[cfg_attr(feature = "schema", derive(JsonSchema))]
106#[serde(rename_all = "kebab-case")]
107pub struct StyleSource {
108 /// The original CSL style ID (URI).
109 pub csl_id: String,
110 /// Who performed the adaptation (e.g., "citum-migrate" or a person's name).
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub adapted_by: Option<String>,
113 /// License URI (e.g., CC BY-SA).
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub license: Option<String>,
116 /// Original CSL style authors.
117 #[serde(skip_serializing_if = "Vec::is_empty", default)]
118 pub original_authors: Vec<StylePerson>,
119 /// Links from the original CSL style (documentation, self, etc.).
120 #[serde(skip_serializing_if = "Vec::is_empty", default)]
121 pub links: Vec<StyleLink>,
122}
123
124/// Style metadata.
125#[derive(Debug, Default, Deserialize, Serialize, Clone)]
126#[cfg_attr(feature = "schema", derive(JsonSchema))]
127#[serde(rename_all = "kebab-case")]
128pub struct StyleInfo {
129 /// Human-readable title of the style.
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub title: Option<String>,
132 /// Stable identifier for the style, usually a URI or slug.
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub id: Option<String>,
135 /// Short summary of the style's intended use or provenance.
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub description: Option<String>,
138 /// Default locale for the style (e.g., "en-US", "de-DE").
139 /// Used for locale-aware term resolution.
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub default_locale: Option<String>,
142 /// Discipline classifications for this style.
143 #[serde(skip_serializing_if = "Vec::is_empty", default)]
144 pub fields: Vec<CitationField>,
145 /// Provenance: set when this style was adapted from a CSL 1.0 source.
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub source: Option<StyleSource>,
148 /// Concise display name for the style family, used by UIs to label
149 /// search results and match banners (e.g. `"APA"`, `"Chicago Notes"`,
150 /// `"MLA"`). Omit for journal-specific styles whose full title is their
151 /// identity. Combine with `edition` to produce labels like `"APA 7th"`.
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub short_name: Option<String>,
154 /// Edition or version qualifier used alongside `short_name` to
155 /// disambiguate multiple editions of the same style family
156 /// (e.g. `"7th"`, `"18th edition"`). Omit when only one edition exists.
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub edition: Option<String>,
159 /// Minimum Citum engine version required to load and render this style.
160 ///
161 /// Accepts a [`semver::VersionReq`]-compatible string (e.g. `">=0.38.0"`,
162 /// `"^0.40"`). When the running engine does not satisfy the requirement,
163 /// the resolver returns
164 /// [`ResolutionError::VersionMismatch`]
165 /// instead of attempting to deserialize fields the engine may not
166 /// understand. Omit for styles that use only stable, long-lived features.
167 #[serde(rename = "citum-version", skip_serializing_if = "Option::is_none")]
168 pub citum_version: Option<String>,
169}
170
171impl StyleInfo {
172 /// Returns `true` when all fields are absent (no content to merge).
173 pub fn is_empty(&self) -> bool {
174 self.title.is_none()
175 && self.id.is_none()
176 && self.description.is_none()
177 && self.default_locale.is_none()
178 && self.fields.is_empty()
179 && self.source.is_none()
180 && self.short_name.is_none()
181 && self.edition.is_none()
182 && self.citum_version.is_none()
183 }
184}