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 for role in contributor.roles.as_slice() {
134 if let ReferenceRole::Unknown(s) = role {
135 warnings.push(Warning {
136 level: WarningLevel::Warning,
137 code: "unknown_enum_variant".to_string(),
138 citation_id: None,
139 ref_id: Some(ref_id.clone()),
140 message: format!("Reference '{ref_id}' uses unknown contributor role '{s}'; this role may be ignored during rendering."),
141 });
142 }
143 }
144 }
145 }
146
147 if let Some(templates) = &processor.style.templates {
149 for (name, template) in templates {
150 scan_template_for_unknowns(template, &format!("template '{name}'"), &mut warnings);
151 }
152 }
153 if let Some(citation) = &processor.style.citation {
154 scan_citation_spec_for_unknowns(citation, "citation layout", &mut warnings);
155 }
156 if let Some(bib) = &processor.style.bibliography {
157 if let Some(template) = &bib.template {
158 scan_template_for_unknowns(template, "bibliography layout", &mut warnings);
159 }
160 if let Some(type_variants) = &bib.type_variants {
161 for variant in type_variants.values() {
162 if let Some(template) = variant.as_template() {
163 scan_template_for_unknowns(template, "bibliography layout", &mut warnings);
164 }
165 }
166 }
167 if let Some(locales) = &bib.locales {
168 for locale_spec in locales {
169 scan_template_for_unknowns(
170 &locale_spec.template,
171 "bibliography layout",
172 &mut warnings,
173 );
174 }
175 }
176 }
177 scan_bibliography_config_sort_for_citation_number(processor, &mut warnings);
178
179 warnings
180}
181
182fn scan_bibliography_config_sort_for_citation_number(
189 processor: &Processor,
190 warnings: &mut Vec<Warning>,
191) {
192 let Some(citum_schema::options::SortEntry::Explicit(sort)) = processor
193 .get_bibliography_config()
194 .processing
195 .as_ref()
196 .map(citum_schema::options::Processing::config)
197 .and_then(|config| config.sort)
198 else {
199 return;
200 };
201
202 let uses_citation_number = sort
203 .template
204 .iter()
205 .any(|spec| matches!(spec.key, citum_schema::options::SortKey::CitationNumber));
206
207 if uses_citation_number {
208 warnings.push(Warning {
209 level: WarningLevel::Warning,
210 code: "citation_number_sort_not_supported".to_string(),
211 citation_id: None,
212 ref_id: None,
213 message: "Style bibliography configuration lists 'citation-number' as an explicit \
214 sort key; it is not supported and is ignored for bibliography ordering."
215 .to_string(),
216 });
217 }
218}
219
220fn scan_citation_spec_for_unknowns(
228 spec: &citum_schema::CitationSpec,
229 location: &str,
230 warnings: &mut Vec<Warning>,
231) {
232 if let Some(template) = &spec.template {
233 scan_template_for_unknowns(template, location, warnings);
234 }
235 if let Some(type_variants) = &spec.type_variants {
236 for variant in type_variants.values() {
237 if let Some(template) = variant.as_template() {
238 scan_template_for_unknowns(template, location, warnings);
239 }
240 }
241 }
242 if let Some(locales) = &spec.locales {
243 for locale_spec in locales {
244 scan_template_for_unknowns(&locale_spec.template, location, warnings);
245 }
246 }
247
248 if let Some(child) = &spec.integral {
249 scan_citation_spec_for_unknowns(child, &format!("{location} (integral)"), warnings);
250 }
251 if let Some(child) = &spec.non_integral {
252 scan_citation_spec_for_unknowns(child, &format!("{location} (non-integral)"), warnings);
253 }
254 if let Some(child) = &spec.subsequent {
255 scan_citation_spec_for_unknowns(child, &format!("{location} (subsequent)"), warnings);
256 }
257 if let Some(child) = &spec.ibid {
258 scan_citation_spec_for_unknowns(child, &format!("{location} (ibid)"), warnings);
259 }
260}
261
262fn scan_template_for_unknowns(
263 components: &[citum_schema::template::TemplateComponent],
264 location: &str,
265 warnings: &mut Vec<Warning>,
266) {
267 use citum_schema::template::TemplateComponent;
268 for component in components {
269 match component {
270 TemplateComponent::Term(t) => {
271 if let GeneralTerm::Unknown(s) = &t.term {
272 warnings.push(Warning {
273 level: WarningLevel::Warning,
274 code: "unknown_enum_variant".to_string(),
275 citation_id: None,
276 ref_id: None,
277 message: format!("Style {location} uses unknown locale term key '{s}'; this term may render as empty."),
278 });
279 }
280 if let Some(TermForm::Unknown(s)) = &t.form {
281 warnings.push(Warning {
282 level: WarningLevel::Warning,
283 code: "unknown_enum_variant".to_string(),
284 citation_id: None,
285 ref_id: None,
286 message: format!("Style {location} uses unknown term form '{s}'; falling back to long form."),
287 });
288 }
289 }
290 TemplateComponent::Contributor(c) => {
291 for role in c.contributor.as_slice() {
292 if let TemplateRole::Unknown(s) = role {
293 warnings.push(Warning {
294 level: WarningLevel::Warning,
295 code: "unknown_enum_variant".to_string(),
296 citation_id: None,
297 ref_id: None,
298 message: format!("Style {location} uses unknown contributor role '{s}'; this role may be ignored."),
299 });
300 }
301 }
302 if let Some(label) = &c.label {
303 let term = label.term.as_str();
304 if !crate::values::contributor::labels::RECOGNIZED_LABEL_TERMS.contains(&term) {
305 warnings.push(Warning {
306 level: WarningLevel::Warning,
307 code: "unknown_role_label_term".to_string(),
308 citation_id: None,
309 ref_id: None,
310 message: format!("Style {location} uses unrecognized role-label term '{term}'; falling back to the contributor's own role term instead of the requested one."),
311 });
312 }
313 }
314 }
315 TemplateComponent::Date(d) => {
316 if let citum_schema::template::DateForm::Unknown(s) = &d.form {
317 warnings.push(Warning {
318 level: WarningLevel::Warning,
319 code: "unknown_enum_variant".to_string(),
320 citation_id: None,
321 ref_id: None,
322 message: format!("Style {location} uses unknown date form '{s}'; falling back to year only."),
323 });
324 }
325 }
326 TemplateComponent::Group(g) => {
327 scan_template_for_unknowns(&g.group, location, warnings);
328 }
329 _ => {}
330 }
331 }
332}
333
334#[cfg(test)]
335#[allow(clippy::unwrap_used, reason = "tests")]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn unknown_enum_warnings_reports_unknown_term_in_integral_sub_spec() {
341 let yaml = "info:\n title: Test\ncitation:\n integral:\n template:\n - term: not-a-real-term\n";
342 let style = citum_schema::Style::from_yaml_str(yaml).unwrap();
343 let processor = Processor::new(style, Bibliography::new());
344
345 let warnings = unknown_enum_warnings(&processor);
346 assert!(
347 warnings
348 .iter()
349 .any(|w| w.message.contains("not-a-real-term") && w.message.contains("(integral)")),
350 "expected a warning for the unknown term in citation.integral.template, got: {warnings:?}"
351 );
352 }
353
354 #[test]
355 fn unknown_enum_warnings_reports_unknown_role_label_term() {
356 let yaml = "info:\n title: Test\nbibliography:\n template:\n - contributor: editor\n form: long\n label: {term: not-a-real-role}\n";
357 let style = citum_schema::Style::from_yaml_str(yaml).unwrap();
358 let processor = Processor::new(style, Bibliography::new());
359
360 let warnings = unknown_enum_warnings(&processor);
361 assert!(
362 warnings
363 .iter()
364 .any(|w| w.code == "unknown_role_label_term"
365 && w.message.contains("not-a-real-role")),
366 "expected a warning for the unrecognized role-label term, got: {warnings:?}"
367 );
368 }
369
370 #[test]
371 fn unknown_enum_warnings_does_not_flag_recognized_role_label_terms() {
372 let yaml = "info:\n title: Test\nbibliography:\n template:\n - contributor: editor\n form: long\n label: {term: editor}\n";
373 let style = citum_schema::Style::from_yaml_str(yaml).unwrap();
374 let processor = Processor::new(style, Bibliography::new());
375
376 let warnings = unknown_enum_warnings(&processor);
377 assert!(
378 !warnings.iter().any(|w| w.code == "unknown_role_label_term"),
379 "did not expect a warning for a recognized role-label term, got: {warnings:?}"
380 );
381 }
382
383 #[test]
384 fn unknown_enum_warnings_reports_unknown_term_in_type_variants() {
385 let yaml = "info:\n title: Test\ncitation:\n type-variants:\n book:\n - term: not-a-real-term-2\n";
386 let style = citum_schema::Style::from_yaml_str(yaml).unwrap();
387 let processor = Processor::new(style, Bibliography::new());
388
389 let warnings = unknown_enum_warnings(&processor);
390 assert!(
391 warnings
392 .iter()
393 .any(|w| w.message.contains("not-a-real-term-2")),
394 "expected a warning for the unknown term in citation.type-variants, got: {warnings:?}"
395 );
396 }
397}