Skip to main content

citum_engine/api/
warnings.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Input-compatibility warning scanners.
7//!
8//! Each function inspects already-loaded inputs (style, bibliography) for
9//! constructs the engine tolerated but cannot act on — unknown reference
10//! classes, fields captured by the forward-compat `unknown_fields`
11//! catch-all, and unknown enum variants — and reports them as structured
12//! [`Warning`]s. Adapters (CLI, WASM, FFI) present these; they must never
13//! re-derive their own checks.
14
15use crate::processor::Processor;
16use crate::reference::Bibliography;
17use citum_schema::locale::{GeneralTerm, TermForm};
18use citum_schema::reference::{
19    ClassExtension, CollectionType, ContributorRole as ReferenceRole, MonographComponentType,
20    MonographType, ReferenceClass, SerialComponentType,
21};
22use citum_schema::template::ContributorRole as TemplateRole;
23
24use super::{Warning, WarningLevel};
25
26/// Scan the bibliography for unknown reference classes and return compatibility warnings.
27pub fn unknown_reference_class_warnings(bibliography: &Bibliography) -> Vec<Warning> {
28    bibliography
29        .iter()
30        .filter_map(|(ref_id, reference)| {
31            let ReferenceClass::Unknown(class) = reference.class() else {
32                return None;
33            };
34            Some(Warning {
35                level: WarningLevel::Warning,
36                code: "unknown_reference_class".to_string(),
37                citation_id: None,
38                ref_id: Some(ref_id.clone()),
39                message: format!(
40                    "Reference '{ref_id}' uses unknown class '{class}'; rendering will use only fields this engine understands."
41                ),
42            })
43        })
44        .collect()
45}
46
47/// Scan the bibliography for fields captured by the forward-compat
48/// `unknown_fields` catch-all and return per-reference warnings.
49///
50/// Unknown-class references are skipped here; they are already reported by
51/// [`unknown_reference_class_warnings`].
52pub fn unknown_reference_field_warnings(bibliography: &Bibliography) -> Vec<Warning> {
53    bibliography
54        .iter()
55        .filter_map(|(ref_id, reference)| {
56            let unknown = reference.unknown_fields()?;
57            if unknown.is_empty() {
58                return None;
59            }
60            let keys: Vec<&str> = unknown.keys().map(String::as_str).collect();
61            Some(Warning {
62                level: WarningLevel::Warning,
63                code: "unknown_reference_field".to_string(),
64                citation_id: None,
65                ref_id: Some(ref_id.clone()),
66                message: format!(
67                    "Reference '{ref_id}' has unknown field(s): {}; these fields are ignored during rendering.",
68                    keys.join(", ")
69                ),
70            })
71        })
72        .collect()
73}
74
75/// Scan the style and bibliography for unknown enum variants and term keys.
76///
77/// Returns a list of structured compatibility warnings for encounter of
78/// unknown variants that were captured via the tolerant-enum mechanism.
79pub fn unknown_enum_warnings(processor: &Processor) -> Vec<Warning> {
80    let mut warnings = Vec::new();
81
82    // 1. Scan bibliography
83    for (ref_id, reference) in &processor.bibliography {
84        match reference.extension() {
85            ClassExtension::Monograph(r) => {
86                if let MonographType::Unknown(s) = &r.r#type {
87                    warnings.push(Warning {
88                        level: WarningLevel::Warning,
89                        code: "unknown_enum_variant".to_string(),
90                        citation_id: None,
91                        ref_id: Some(ref_id.clone()),
92                        message: format!("Reference '{ref_id}' uses unknown monograph type '{s}'; rendering will use default monograph formatting."),
93                    });
94                }
95            }
96            ClassExtension::Collection(r) => {
97                if let CollectionType::Unknown(s) = &r.r#type {
98                    warnings.push(Warning {
99                        level: WarningLevel::Warning,
100                        code: "unknown_enum_variant".to_string(),
101                        citation_id: None,
102                        ref_id: Some(ref_id.clone()),
103                        message: format!("Reference '{ref_id}' uses unknown collection type '{s}'; rendering will use default collection formatting."),
104                    });
105                }
106            }
107            ClassExtension::CollectionComponent(r) => {
108                if let MonographComponentType::Unknown(s) = &r.r#type {
109                    warnings.push(Warning {
110                        level: WarningLevel::Warning,
111                        code: "unknown_enum_variant".to_string(),
112                        citation_id: None,
113                        ref_id: Some(ref_id.clone()),
114                        message: format!("Reference '{ref_id}' uses unknown monograph component type '{s}'; rendering will use default chapter formatting."),
115                    });
116                }
117            }
118            ClassExtension::SerialComponent(r) => {
119                if let SerialComponentType::Unknown(s) = &r.r#type {
120                    warnings.push(Warning {
121                        level: WarningLevel::Warning,
122                        code: "unknown_enum_variant".to_string(),
123                        citation_id: None,
124                        ref_id: Some(ref_id.clone()),
125                        message: format!("Reference '{ref_id}' uses unknown serial component type '{s}'; rendering will use default article formatting."),
126                    });
127                }
128            }
129            _ => {}
130        }
131
132        for contributor in reference.all_contributor_entries() {
133            if let ReferenceRole::Unknown(s) = &contributor.role {
134                warnings.push(Warning {
135                    level: WarningLevel::Warning,
136                    code: "unknown_enum_variant".to_string(),
137                    citation_id: None,
138                    ref_id: Some(ref_id.clone()),
139                    message: format!("Reference '{ref_id}' uses unknown contributor role '{s}'; this role may be ignored during rendering."),
140                });
141            }
142        }
143    }
144
145    // 2. Scan Style
146    if let Some(templates) = &processor.style.templates {
147        for (name, template) in templates {
148            scan_template_for_unknowns(template, &format!("template '{name}'"), &mut warnings);
149        }
150    }
151    if let Some(citation) = &processor.style.citation
152        && let Some(template) = &citation.template
153    {
154        scan_template_for_unknowns(template, "citation layout", &mut warnings);
155    }
156    if let Some(bib) = &processor.style.bibliography
157        && let Some(template) = &bib.template
158    {
159        scan_template_for_unknowns(template, "bibliography layout", &mut warnings);
160    }
161
162    warnings
163}
164
165fn scan_template_for_unknowns(
166    components: &[citum_schema::template::TemplateComponent],
167    location: &str,
168    warnings: &mut Vec<Warning>,
169) {
170    use citum_schema::template::TemplateComponent;
171    for component in components {
172        match component {
173            TemplateComponent::Term(t) => {
174                if let GeneralTerm::Unknown(s) = &t.term {
175                    warnings.push(Warning {
176                        level: WarningLevel::Warning,
177                        code: "unknown_enum_variant".to_string(),
178                        citation_id: None,
179                        ref_id: None,
180                        message: format!("Style {location} uses unknown locale term key '{s}'; this term may render as empty."),
181                    });
182                }
183                if let Some(TermForm::Unknown(s)) = &t.form {
184                    warnings.push(Warning {
185                        level: WarningLevel::Warning,
186                        code: "unknown_enum_variant".to_string(),
187                        citation_id: None,
188                        ref_id: None,
189                        message: format!("Style {location} uses unknown term form '{s}'; falling back to long form."),
190                    });
191                }
192            }
193            TemplateComponent::Contributor(c) => {
194                if let TemplateRole::Unknown(s) = &c.contributor {
195                    warnings.push(Warning {
196                        level: WarningLevel::Warning,
197                        code: "unknown_enum_variant".to_string(),
198                        citation_id: None,
199                        ref_id: None,
200                        message: format!("Style {location} uses unknown contributor role '{s}'; this role may be ignored."),
201                    });
202                }
203            }
204            TemplateComponent::Date(d) => {
205                if let citum_schema::template::DateForm::Unknown(s) = &d.form {
206                    warnings.push(Warning {
207                        level: WarningLevel::Warning,
208                        code: "unknown_enum_variant".to_string(),
209                        citation_id: None,
210                        ref_id: None,
211                        message: format!("Style {location} uses unknown date form '{s}'; falling back to year only."),
212                    });
213                }
214            }
215            TemplateComponent::Group(g) => {
216                scan_template_for_unknowns(&g.group, location, warnings);
217            }
218            _ => {}
219        }
220    }
221}