1#[cfg(feature = "schema")]
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11use crate::Template;
12use crate::locale::{GeneralTerm, TermForm};
13use crate::presets::SortPreset;
14use crate::template::TypeSelector;
15
16#[derive(Debug, Clone, Deserialize, Serialize, Default)]
40#[cfg_attr(feature = "schema", derive(JsonSchema))]
41#[serde(rename_all = "kebab-case")]
42pub struct BibliographyGroup {
43 pub id: String,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
49 pub heading: Option<GroupHeading>,
50
51 pub selector: GroupSelector,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
57 pub sort: Option<GroupSortEntry>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
62 pub template: Option<Template>,
63
64 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub disambiguate: Option<DisambiguationScope>,
69}
70
71#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
73#[cfg_attr(feature = "schema", derive(JsonSchema))]
74#[serde(rename_all = "kebab-case", untagged)]
75pub enum GroupHeading {
76 Literal {
78 literal: String,
80 },
81 Term {
83 term: GeneralTerm,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 form: Option<TermForm>,
88 },
89 Localized {
91 localized: HashMap<String, String>,
93 },
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
98#[cfg_attr(feature = "schema", derive(JsonSchema))]
99#[serde(rename_all = "kebab-case")]
100pub enum DisambiguationScope {
101 #[default]
103 Globally,
104 Locally,
106}
107
108#[derive(Debug, Clone, Deserialize, Serialize, Default)]
113#[cfg_attr(feature = "schema", derive(JsonSchema))]
114#[serde(rename_all = "kebab-case")]
115pub struct GroupSelector {
116 #[serde(skip_serializing_if = "Option::is_none")]
118 #[serde(rename = "type")]
119 pub ref_type: Option<TypeSelector>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub cited: Option<CitedStatus>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub field: Option<HashMap<String, FieldMatcher>>,
128
129 #[serde(skip_serializing_if = "Option::is_none")]
132 pub not: Option<Box<GroupSelector>>,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
137#[cfg_attr(feature = "schema", derive(JsonSchema))]
138#[serde(rename_all = "kebab-case")]
139pub enum CitedStatus {
140 Visible,
142 Any,
144}
145
146#[derive(Debug, Clone, Deserialize, Serialize)]
148#[cfg_attr(feature = "schema", derive(JsonSchema))]
149#[serde(untagged)]
150pub enum FieldMatcher {
151 Exact(String),
153 Multiple(Vec<String>),
155 }
157
158#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
160#[cfg_attr(feature = "schema", derive(JsonSchema))]
161#[serde(untagged)]
162pub enum GroupSortEntry {
163 Preset(SortPreset),
165 Explicit(GroupSort),
167}
168
169impl GroupSortEntry {
170 pub fn resolve(&self) -> GroupSort {
175 match self {
176 GroupSortEntry::Preset(preset) => preset.group_sort(),
177 GroupSortEntry::Explicit(sort) => sort.clone(),
178 }
179 }
180}
181
182#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
187#[cfg_attr(feature = "schema", derive(JsonSchema))]
188#[serde(rename_all = "kebab-case")]
189pub struct GroupSort {
190 pub template: Vec<GroupSortKey>,
192}
193
194#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
196#[cfg_attr(feature = "schema", derive(JsonSchema))]
197#[serde(rename_all = "kebab-case")]
198pub struct GroupSortKey {
199 pub key: SortKey,
201
202 #[serde(default = "default_true")]
204 pub ascending: bool,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
211 pub order: Option<Vec<String>>,
212
213 #[serde(skip_serializing_if = "Option::is_none")]
217 pub sort_order: Option<NameSortOrder>,
218}
219
220fn default_true() -> bool {
221 true
222}
223
224#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
226#[cfg_attr(feature = "schema", derive(JsonSchema))]
227#[serde(rename_all = "kebab-case")]
228pub enum SortKey {
229 #[serde(rename = "type")]
231 RefType,
232 Author,
234 Title,
236 Issued,
238 Field(String),
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
244#[cfg_attr(feature = "schema", derive(JsonSchema))]
245#[serde(rename_all = "kebab-case")]
246pub enum NameSortOrder {
247 FamilyGiven,
250 GivenFamily,
253}
254
255#[cfg(test)]
256#[allow(
257 clippy::unwrap_used,
258 clippy::expect_used,
259 clippy::panic,
260 clippy::indexing_slicing,
261 clippy::todo,
262 clippy::unimplemented,
263 clippy::unreachable,
264 clippy::get_unwrap,
265 reason = "Panicking is acceptable and often desired in tests."
266)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_group_selector_type_single() {
272 let yaml = r#"
273type: legal-case
274"#;
275 let selector: GroupSelector = serde_yaml::from_str(yaml).unwrap();
276 assert!(selector.ref_type.is_some());
277 match selector.ref_type.unwrap() {
278 TypeSelector::Single(t) => assert_eq!(t, "legal-case"),
279 _ => panic!("Expected Single"),
280 }
281 }
282
283 #[test]
284 fn test_group_selector_type_multiple() {
285 let yaml = r#"
286type: [legal-case, statute, treaty]
287"#;
288 let selector: GroupSelector = serde_yaml::from_str(yaml).unwrap();
289 match selector.ref_type.unwrap() {
290 TypeSelector::Multiple(types) => {
291 assert_eq!(types, vec!["legal-case", "statute", "treaty"]);
292 }
293 _ => panic!("Expected Multiple"),
294 }
295 }
296
297 #[test]
298 fn test_group_selector_field_exact() {
299 let yaml = r#"
300field:
301 language: vi
302"#;
303 let selector: GroupSelector = serde_yaml::from_str(yaml).unwrap();
304 let fields = selector.field.unwrap();
305 match fields.get("language").unwrap() {
306 FieldMatcher::Exact(lang) => assert_eq!(lang, "vi"),
307 _ => panic!("Expected Exact"),
308 }
309 }
310
311 #[test]
312 fn test_group_selector_negation() {
313 let yaml = r#"
314not:
315 type: legal-case
316"#;
317 let selector: GroupSelector = serde_yaml::from_str(yaml).unwrap();
318 let negated = selector.not.unwrap();
319 assert!(negated.ref_type.is_some());
320 }
321
322 #[test]
323 fn test_bibliography_group_minimal() {
324 let yaml = r#"
325id: cases
326selector:
327 type: legal-case
328"#;
329 let group: BibliographyGroup = serde_yaml::from_str(yaml).unwrap();
330 assert_eq!(group.id, "cases");
331 assert!(group.heading.is_none());
332 assert!(group.sort.is_none());
333 }
334
335 #[test]
336 fn test_bibliography_group_full() {
337 let yaml = r#"
338id: vietnamese
339heading:
340 localized:
341 vi: "Tài liệu tiếng Việt"
342 en-US: "Vietnamese Sources"
343selector:
344 field:
345 language: vi
346sort:
347 template:
348 - key: author
349 sort-order: given-family
350 - key: issued
351 ascending: false
352"#;
353 let group: BibliographyGroup = serde_yaml::from_str(yaml).unwrap();
354 assert_eq!(group.id, "vietnamese");
355 match group.heading.unwrap() {
356 GroupHeading::Localized { localized } => {
357 assert_eq!(localized.get("vi").unwrap(), "Tài liệu tiếng Việt");
358 assert_eq!(localized.get("en-US").unwrap(), "Vietnamese Sources");
359 }
360 _ => panic!("Expected localized heading"),
361 }
362
363 let sort = group.sort.unwrap().resolve();
364 assert_eq!(sort.template.len(), 2);
365
366 match &sort.template[0].key {
367 SortKey::Author => {}
368 _ => panic!("Expected Author"),
369 }
370 assert_eq!(
371 sort.template[0].sort_order,
372 Some(NameSortOrder::GivenFamily)
373 );
374
375 match &sort.template[1].key {
376 SortKey::Issued => {}
377 _ => panic!("Expected Issued"),
378 }
379 assert!(!sort.template[1].ascending);
380 }
381
382 #[test]
383 fn test_type_order_sorting() {
384 let yaml = r#"
385template:
386 - key: type
387 order: [legal-case, statute, treaty]
388"#;
389 let sort: GroupSort = serde_yaml::from_str(yaml).unwrap();
390 assert_eq!(sort.template.len(), 1);
391
392 let order = sort.template[0].order.as_ref().unwrap();
393 assert_eq!(order, &vec!["legal-case", "statute", "treaty"]);
394 }
395
396 #[test]
397 fn test_group_heading_literal() {
398 let yaml = r#"
399literal: "Primary Sources"
400"#;
401 let heading: GroupHeading = serde_yaml::from_str(yaml).unwrap();
402 match heading {
403 GroupHeading::Literal { literal } => assert_eq!(literal, "Primary Sources"),
404 _ => panic!("Expected literal heading"),
405 }
406 }
407
408 #[test]
409 fn test_group_heading_term() {
410 let yaml = r#"
411term: no-date
412form: short
413"#;
414 let heading: GroupHeading = serde_yaml::from_str(yaml).unwrap();
415 match heading {
416 GroupHeading::Term { term, form } => {
417 assert_eq!(term, GeneralTerm::NoDate);
418 assert_eq!(form, Some(TermForm::Short));
419 }
420 _ => panic!("Expected term heading"),
421 }
422 }
423}