1use 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
26pub 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
47pub 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
75pub fn unknown_enum_warnings(processor: &Processor) -> Vec<Warning> {
80 let mut warnings = Vec::new();
81
82 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 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 scan_citation_spec_for_unknowns(citation, "citation layout", &mut warnings);
153 }
154 if let Some(bib) = &processor.style.bibliography {
155 if let Some(template) = &bib.template {
156 scan_template_for_unknowns(template, "bibliography layout", &mut warnings);
157 }
158 if let Some(type_variants) = &bib.type_variants {
159 for variant in type_variants.values() {
160 if let Some(template) = variant.as_template() {
161 scan_template_for_unknowns(template, "bibliography layout", &mut warnings);
162 }
163 }
164 }
165 if let Some(locales) = &bib.locales {
166 for locale_spec in locales {
167 scan_template_for_unknowns(
168 &locale_spec.template,
169 "bibliography layout",
170 &mut warnings,
171 );
172 }
173 }
174 }
175
176 warnings
177}
178
179fn scan_citation_spec_for_unknowns(
187 spec: &citum_schema::CitationSpec,
188 location: &str,
189 warnings: &mut Vec<Warning>,
190) {
191 if let Some(template) = &spec.template {
192 scan_template_for_unknowns(template, location, warnings);
193 }
194 if let Some(type_variants) = &spec.type_variants {
195 for variant in type_variants.values() {
196 if let Some(template) = variant.as_template() {
197 scan_template_for_unknowns(template, location, warnings);
198 }
199 }
200 }
201 if let Some(locales) = &spec.locales {
202 for locale_spec in locales {
203 scan_template_for_unknowns(&locale_spec.template, location, warnings);
204 }
205 }
206
207 if let Some(child) = &spec.integral {
208 scan_citation_spec_for_unknowns(child, &format!("{location} (integral)"), warnings);
209 }
210 if let Some(child) = &spec.non_integral {
211 scan_citation_spec_for_unknowns(child, &format!("{location} (non-integral)"), warnings);
212 }
213 if let Some(child) = &spec.subsequent {
214 scan_citation_spec_for_unknowns(child, &format!("{location} (subsequent)"), warnings);
215 }
216 if let Some(child) = &spec.ibid {
217 scan_citation_spec_for_unknowns(child, &format!("{location} (ibid)"), warnings);
218 }
219}
220
221fn scan_template_for_unknowns(
222 components: &[citum_schema::template::TemplateComponent],
223 location: &str,
224 warnings: &mut Vec<Warning>,
225) {
226 use citum_schema::template::TemplateComponent;
227 for component in components {
228 match component {
229 TemplateComponent::Term(t) => {
230 if let GeneralTerm::Unknown(s) = &t.term {
231 warnings.push(Warning {
232 level: WarningLevel::Warning,
233 code: "unknown_enum_variant".to_string(),
234 citation_id: None,
235 ref_id: None,
236 message: format!("Style {location} uses unknown locale term key '{s}'; this term may render as empty."),
237 });
238 }
239 if let Some(TermForm::Unknown(s)) = &t.form {
240 warnings.push(Warning {
241 level: WarningLevel::Warning,
242 code: "unknown_enum_variant".to_string(),
243 citation_id: None,
244 ref_id: None,
245 message: format!("Style {location} uses unknown term form '{s}'; falling back to long form."),
246 });
247 }
248 }
249 TemplateComponent::Contributor(c) => {
250 if let TemplateRole::Unknown(s) = &c.contributor {
251 warnings.push(Warning {
252 level: WarningLevel::Warning,
253 code: "unknown_enum_variant".to_string(),
254 citation_id: None,
255 ref_id: None,
256 message: format!("Style {location} uses unknown contributor role '{s}'; this role may be ignored."),
257 });
258 }
259 }
260 TemplateComponent::Date(d) => {
261 if let citum_schema::template::DateForm::Unknown(s) = &d.form {
262 warnings.push(Warning {
263 level: WarningLevel::Warning,
264 code: "unknown_enum_variant".to_string(),
265 citation_id: None,
266 ref_id: None,
267 message: format!("Style {location} uses unknown date form '{s}'; falling back to year only."),
268 });
269 }
270 }
271 TemplateComponent::Group(g) => {
272 scan_template_for_unknowns(&g.group, location, warnings);
273 }
274 _ => {}
275 }
276 }
277}
278
279#[cfg(test)]
280#[allow(clippy::unwrap_used, reason = "tests")]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn unknown_enum_warnings_reports_unknown_term_in_integral_sub_spec() {
286 let yaml = "info:\n title: Test\ncitation:\n integral:\n template:\n - term: not-a-real-term\n";
287 let style = citum_schema::Style::from_yaml_str(yaml).unwrap();
288 let processor = Processor::new(style, Bibliography::new());
289
290 let warnings = unknown_enum_warnings(&processor);
291 assert!(
292 warnings
293 .iter()
294 .any(|w| w.message.contains("not-a-real-term") && w.message.contains("(integral)")),
295 "expected a warning for the unknown term in citation.integral.template, got: {warnings:?}"
296 );
297 }
298
299 #[test]
300 fn unknown_enum_warnings_reports_unknown_term_in_type_variants() {
301 let yaml = "info:\n title: Test\ncitation:\n type-variants:\n book:\n - term: not-a-real-term-2\n";
302 let style = citum_schema::Style::from_yaml_str(yaml).unwrap();
303 let processor = Processor::new(style, Bibliography::new());
304
305 let warnings = unknown_enum_warnings(&processor);
306 assert!(
307 warnings
308 .iter()
309 .any(|w| w.message.contains("not-a-real-term-2")),
310 "expected a warning for the unknown term in citation.type-variants, got: {warnings:?}"
311 );
312 }
313}