1#[cfg(feature = "schema")]
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20
21use crate::presets::SortPreset;
22
23const PROCESSING_STRING_VARIANTS: &[&str] = &[
24 "author-date",
25 "author-date-givenname",
26 "author-date-names",
27 "author-date-full",
28 "numeric",
29 "note",
30 "label",
31];
32
33#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
35#[cfg_attr(feature = "schema", derive(JsonSchema))]
36#[serde(rename_all = "kebab-case")]
37#[non_exhaustive]
38pub enum LabelPreset {
39 #[default]
41 Alpha,
42 Din,
44 Ams,
46}
47
48#[derive(Debug, Clone)]
53pub struct LabelParams {
54 pub single_author_chars: u8,
56 pub multi_author_chars: u8,
58 pub et_al_min: u8,
60 pub et_al_marker: String,
62 pub et_al_names: u8,
64 pub year_digits: u8,
66}
67
68#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
70#[cfg_attr(feature = "schema", derive(JsonSchema))]
71#[serde(rename_all = "kebab-case")]
72pub struct LabelConfig {
73 #[serde(default)]
75 pub preset: LabelPreset,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub single_author_chars: Option<u8>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub multi_author_chars: Option<u8>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub et_al_min: Option<u8>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub et_al_marker: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub et_al_names: Option<u8>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub year_digits: Option<u8>,
94}
95
96impl LabelConfig {
97 pub fn effective_params(&self) -> LabelParams {
107 let (
108 default_single_author_chars,
109 default_multi_author_chars,
110 default_et_al_min,
111 default_marker,
112 default_et_al_names,
113 ) = match self.preset {
114 LabelPreset::Alpha => (3u8, 1u8, 4u8, "+".to_string(), 3u8),
115 LabelPreset::Ams => (4u8, 1u8, 5u8, String::new(), 4u8),
116 LabelPreset::Din => (4u8, 1u8, 3u8, String::new(), 3u8),
117 };
118 LabelParams {
119 single_author_chars: self
120 .single_author_chars
121 .unwrap_or(default_single_author_chars),
122 multi_author_chars: self
123 .multi_author_chars
124 .unwrap_or(default_multi_author_chars),
125 et_al_min: self.et_al_min.unwrap_or(default_et_al_min),
126 et_al_marker: self.et_al_marker.clone().unwrap_or(default_marker),
127 et_al_names: self.et_al_names.unwrap_or(default_et_al_names),
128 year_digits: self.year_digits.unwrap_or(2),
129 }
130 }
131}
132
133#[derive(Debug, Default, PartialEq, Clone)]
143#[cfg_attr(feature = "schema", derive(JsonSchema))]
144#[cfg_attr(feature = "schema", schemars(rename_all = "kebab-case"))]
145#[non_exhaustive]
146pub enum Processing {
147 #[default]
150 AuthorDate,
151 AuthorDateGivenname,
153 AuthorDateNames,
155 AuthorDateFull,
157 Numeric,
160 Note,
163 Label(LabelConfig),
166 Custom(ProcessingCustom),
169}
170
171#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
176#[cfg_attr(feature = "schema", derive(JsonSchema))]
177#[serde(rename_all = "kebab-case")]
178pub enum CitationSortPolicy {
179 ExplicitOnly,
181}
182
183#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
189#[cfg_attr(feature = "schema", derive(JsonSchema))]
190#[serde(rename_all = "kebab-case")]
191#[non_exhaustive]
192pub enum ProcessingBase {
193 AuthorDate,
195 AuthorDateGivenname,
197 AuthorDateNames,
199 AuthorDateFull,
201 Numeric,
203 Note,
205 Label,
207}
208
209impl ProcessingBase {
210 pub fn processing(&self) -> Processing {
215 match self {
216 Self::AuthorDate => Processing::AuthorDate,
217 Self::AuthorDateGivenname => Processing::AuthorDateGivenname,
218 Self::AuthorDateNames => Processing::AuthorDateNames,
219 Self::AuthorDateFull => Processing::AuthorDateFull,
220 Self::Numeric => Processing::Numeric,
221 Self::Note => Processing::Note,
222 Self::Label => Processing::Label(LabelConfig::default()),
223 }
224 }
225}
226
227#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
234#[cfg_attr(feature = "schema", derive(JsonSchema))]
235#[serde(rename_all = "kebab-case")]
236pub struct ProcessingCustom {
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub base: Option<ProcessingBase>,
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub sort: Option<SortEntry>,
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub group: Option<Group>,
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub disambiguate: Option<Disambiguation>,
249}
250
251impl ProcessingCustom {
252 #[must_use]
260 pub fn resolved(&self) -> ProcessingCustom {
261 let mut config = match self.base {
262 Some(base) => base.processing().config(),
263 None => ProcessingCustom::default(),
264 };
265 config.base = None;
266 if self.sort.is_some() {
267 config.sort = self.sort.clone();
268 }
269 if self.group.is_some() {
270 config.group = self.group.clone();
271 }
272 if self.disambiguate.is_some() {
273 config.disambiguate = self.disambiguate.clone();
274 }
275 config
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub enum RegimeFamily {
290 AuthorDate,
292 Numeric,
294 Note,
296 Label,
298 Custom,
300}
301
302fn author_date_config(
303 names: bool,
304 add_givenname: bool,
305 givenname_rule: GivennameRule,
306) -> ProcessingCustom {
307 ProcessingCustom {
308 base: None,
309 sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
310 group: Some(Group {
311 template: vec![SortKey::Author, SortKey::Year],
312 }),
313 disambiguate: Some(Disambiguation {
314 names,
315 add_givenname,
316 givenname_rule,
317 year_suffix: true,
318 }),
319 }
320}
321
322impl Processing {
323 pub fn default_bibliography_sort(&self) -> Option<SortPreset> {
333 match self {
334 Processing::AuthorDate
335 | Processing::AuthorDateGivenname
336 | Processing::AuthorDateNames
337 | Processing::AuthorDateFull => Some(SortPreset::AuthorDateTitle),
338 Processing::Numeric => None,
339 Processing::Note => Some(SortPreset::AuthorTitleDate),
340 Processing::Label(_) => Some(SortPreset::AuthorDateTitle),
341 Processing::Custom(custom) => match (custom.base, custom.sort.as_ref()) {
342 (Some(base), None) => base.processing().default_bibliography_sort(),
343 _ => None,
344 },
345 }
346 }
347
348 pub fn is_author_date_family(&self) -> bool {
355 self.regime_family() == RegimeFamily::AuthorDate
356 }
357
358 pub fn regime_family(&self) -> RegimeFamily {
371 match self {
372 Self::AuthorDate
373 | Self::AuthorDateGivenname
374 | Self::AuthorDateNames
375 | Self::AuthorDateFull => RegimeFamily::AuthorDate,
376 Self::Numeric => RegimeFamily::Numeric,
377 Self::Note => RegimeFamily::Note,
378 Self::Label(_) => RegimeFamily::Label,
379 Self::Custom(custom) => match custom.base {
380 Some(base) => base.processing().regime_family(),
381 None => RegimeFamily::Custom,
382 },
383 }
384 }
385
386 pub fn default_citation_sort_policy(&self) -> CitationSortPolicy {
391 CitationSortPolicy::ExplicitOnly
392 }
393
394 pub fn config(&self) -> ProcessingCustom {
399 match self {
400 Processing::AuthorDate => author_date_config(false, false, GivennameRule::ByCite),
401 Processing::AuthorDateGivenname => {
402 author_date_config(false, true, GivennameRule::ByCite)
403 }
404 Processing::AuthorDateNames => author_date_config(true, false, GivennameRule::ByCite),
405 Processing::AuthorDateFull => {
412 author_date_config(true, true, GivennameRule::PrimaryName)
413 }
414 Processing::Numeric => ProcessingCustom::default(),
415 Processing::Note => ProcessingCustom {
416 base: None,
417 sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
418 group: None,
419 disambiguate: Some(Disambiguation {
420 names: true,
421 add_givenname: false,
422 givenname_rule: GivennameRule::default(),
423 year_suffix: false,
424 }),
425 },
426 Processing::Label(_) => ProcessingCustom {
427 base: None,
428 sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
429 group: None,
430 disambiguate: Some(Disambiguation {
431 names: false,
432 add_givenname: false,
433 givenname_rule: GivennameRule::default(),
434 year_suffix: true,
435 }),
436 },
437 Processing::Custom(custom) => custom.resolved(),
438 }
439 }
440}
441
442impl Serialize for Processing {
443 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
444 where
445 S: serde::Serializer,
446 {
447 match self {
448 Processing::AuthorDate => serializer.serialize_str("author-date"),
449 Processing::AuthorDateGivenname => serializer.serialize_str("author-date-givenname"),
450 Processing::AuthorDateNames => serializer.serialize_str("author-date-names"),
451 Processing::AuthorDateFull => serializer.serialize_str("author-date-full"),
452 Processing::Numeric => serializer.serialize_str("numeric"),
453 Processing::Note => serializer.serialize_str("note"),
454 Processing::Label(config) => {
455 use serde::ser::SerializeMap;
456 let mut map = serializer.serialize_map(Some(1))?;
457 map.serialize_entry("label", config)?;
458 map.end()
459 }
460 Processing::Custom(custom) => custom.serialize(serializer),
464 }
465 }
466}
467
468impl<'de> Deserialize<'de> for Processing {
469 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
470 where
471 D: serde::Deserializer<'de>,
472 {
473 use serde::de::{self, MapAccess, Visitor};
474
475 struct ProcessingVisitor;
476
477 impl<'de> Visitor<'de> for ProcessingVisitor {
478 type Value = Processing;
479
480 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
481 f.write_str("a processing mode string or map")
482 }
483
484 fn visit_str<E: de::Error>(self, v: &str) -> Result<Processing, E> {
485 match v {
486 "author-date" => Ok(Processing::AuthorDate),
487 "author-date-givenname" => Ok(Processing::AuthorDateGivenname),
488 "author-date-names" => Ok(Processing::AuthorDateNames),
489 "author-date-full" => Ok(Processing::AuthorDateFull),
490 "numeric" => Ok(Processing::Numeric),
491 "note" => Ok(Processing::Note),
492 "label" => Ok(Processing::Label(LabelConfig::default())),
493 other => Err(E::unknown_variant(other, PROCESSING_STRING_VARIANTS)),
494 }
495 }
496
497 fn visit_enum<A: de::EnumAccess<'de>>(self, data: A) -> Result<Processing, A::Error> {
498 use serde::de::VariantAccess;
499 let (variant, access) = data.variant::<String>()?;
500 match variant.as_str() {
501 "custom" => {
502 let custom: ProcessingCustom = access.newtype_variant()?;
503 Ok(Processing::Custom(custom))
504 }
505 other => Err(de::Error::unknown_variant(other, &["custom"])),
508 }
509 }
510
511 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Processing, A::Error> {
512 let key: String = map
513 .next_key()?
514 .ok_or_else(|| de::Error::invalid_length(0, &"1"))?;
515 match key.as_str() {
516 "label" => {
517 let config: LabelConfig = map.next_value()?;
518 Ok(Processing::Label(config))
519 }
520 "base" | "sort" | "group" | "disambiguate" => {
521 let mut base = None;
526 let mut sort = None;
527 let mut group = None;
528 let mut disambiguate = None;
529
530 match key.as_str() {
536 "base" => base = map.next_value()?,
537 "sort" => sort = map.next_value()?,
538 "group" => group = map.next_value()?,
539 "disambiguate" => disambiguate = map.next_value()?,
540 _ => {
541 return Err(de::Error::unknown_field(
542 &key,
543 &["base", "sort", "group", "disambiguate"],
544 ));
545 }
546 }
547
548 while let Some(k) = map.next_key::<String>()? {
550 match k.as_str() {
551 "base" => base = map.next_value()?,
552 "sort" => sort = map.next_value()?,
553 "group" => group = map.next_value()?,
554 "disambiguate" => disambiguate = map.next_value()?,
555 other => {
556 return Err(de::Error::unknown_field(
557 other,
558 &["base", "sort", "group", "disambiguate"],
559 ));
560 }
561 }
562 }
563
564 Ok(Processing::Custom(ProcessingCustom {
565 base,
566 sort,
567 group,
568 disambiguate,
569 }))
570 }
571 other => Err(de::Error::unknown_field(
572 other,
573 &["label", "base", "sort", "group", "disambiguate"],
574 )),
575 }
576 }
577 }
578
579 deserializer.deserialize_any(ProcessingVisitor)
580 }
581}
582
583#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
591#[cfg_attr(feature = "schema", derive(JsonSchema))]
592#[serde(rename_all = "kebab-case")]
593#[non_exhaustive]
594pub enum GivennameRule {
595 #[default]
598 ByCite,
599 AllNames,
601 AllNamesWithInitials,
603 PrimaryName,
605 PrimaryNameWithInitials,
607}
608
609#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
613#[cfg_attr(feature = "schema", derive(JsonSchema))]
614#[serde(rename_all = "kebab-case")]
615pub struct Disambiguation {
616 pub names: bool,
618 #[serde(default)]
620 pub add_givenname: bool,
621 #[serde(default)]
623 pub givenname_rule: GivennameRule,
624 pub year_suffix: bool,
626}
627
628impl Default for Disambiguation {
629 fn default() -> Self {
630 Self {
631 names: true,
632 add_givenname: false,
633 givenname_rule: GivennameRule::default(),
634 year_suffix: false,
635 }
636 }
637}
638
639#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
643#[cfg_attr(feature = "schema", derive(JsonSchema))]
644#[serde(rename_all = "kebab-case")]
645pub struct Sort {
646 #[serde(default)]
648 pub shorten_names: bool,
649 #[serde(default)]
651 pub render_substitutions: bool,
652 pub template: Vec<SortSpec>,
654}
655
656#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
660#[cfg_attr(feature = "schema", derive(JsonSchema))]
661#[serde(untagged)]
662pub enum SortEntry {
663 Preset(crate::presets::SortPreset),
665 Explicit(Sort),
667}
668
669impl SortEntry {
670 pub fn resolve(&self) -> Sort {
674 match self {
675 SortEntry::Preset(preset) => preset.sort(),
676 SortEntry::Explicit(sort) => sort.clone(),
677 }
678 }
679}
680
681impl Sort {
682 pub fn group_sort(&self) -> crate::grouping::GroupSort {
691 let template = self
692 .template
693 .iter()
694 .filter_map(|sort| {
695 let key = match sort.key {
696 SortKey::Author => crate::grouping::SortKey::Author,
697 SortKey::Year => crate::grouping::SortKey::Issued,
698 SortKey::Title => crate::grouping::SortKey::Title,
699 SortKey::CitationNumber => return None,
703 };
704 Some(crate::grouping::GroupSortKey {
705 key,
706 ascending: sort.ascending,
707 order: None,
708 sort_order: None,
709 })
710 })
711 .collect();
712
713 crate::grouping::GroupSort { template }
714 }
715}
716
717#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
721#[cfg_attr(feature = "schema", derive(JsonSchema))]
722#[serde(rename_all = "kebab-case")]
723pub struct SortSpec {
724 pub key: SortKey,
726 #[serde(default = "default_ascending")]
728 pub ascending: bool,
729}
730
731fn default_ascending() -> bool {
732 true
733}
734
735#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
739#[cfg_attr(feature = "schema", derive(JsonSchema))]
740#[serde(rename_all = "kebab-case")]
741#[non_exhaustive]
742pub enum SortKey {
743 #[default]
745 Author,
746 Year,
748 Title,
750 CitationNumber,
752}
753
754#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
758#[cfg_attr(feature = "schema", derive(JsonSchema))]
759#[serde(rename_all = "kebab-case")]
760pub struct Group {
761 pub template: Vec<SortKey>,
763}
764
765#[cfg(test)]
766#[allow(
767 clippy::unwrap_used,
768 clippy::expect_used,
769 clippy::panic,
770 clippy::indexing_slicing,
771 clippy::todo,
772 clippy::unimplemented,
773 clippy::unreachable,
774 clippy::get_unwrap,
775 reason = "Panicking is acceptable and often desired in tests."
776)]
777mod tests {
778 use super::*;
779
780 #[test]
782 fn test_label_config_alpha_preset_defaults() {
783 let config = LabelConfig {
784 preset: LabelPreset::Alpha,
785 single_author_chars: None,
786 multi_author_chars: None,
787 et_al_min: None,
788 et_al_marker: None,
789 et_al_names: None,
790 year_digits: None,
791 };
792
793 let params = config.effective_params();
794 assert_eq!(params.single_author_chars, 3);
795 assert_eq!(params.multi_author_chars, 1);
796 assert_eq!(params.et_al_min, 4);
797 assert_eq!(params.et_al_marker, "+");
798 assert_eq!(params.et_al_names, 3);
799 assert_eq!(params.year_digits, 2);
800 }
801
802 #[test]
804 fn test_label_config_alpha_with_overrides() {
805 let config = LabelConfig {
806 preset: LabelPreset::Alpha,
807 single_author_chars: Some(5),
808 multi_author_chars: Some(2),
809 et_al_min: Some(5),
810 et_al_marker: Some("*".to_string()),
811 et_al_names: Some(4),
812 year_digits: Some(4),
813 };
814
815 let params = config.effective_params();
816 assert_eq!(params.single_author_chars, 5);
817 assert_eq!(params.multi_author_chars, 2);
818 assert_eq!(params.et_al_min, 5);
819 assert_eq!(params.et_al_marker, "*");
820 assert_eq!(params.et_al_names, 4);
821 assert_eq!(params.year_digits, 4);
822 }
823
824 #[test]
826 fn test_label_config_din_preset_defaults() {
827 let config = LabelConfig {
828 preset: LabelPreset::Din,
829 single_author_chars: None,
830 multi_author_chars: None,
831 et_al_min: None,
832 et_al_marker: None,
833 et_al_names: None,
834 year_digits: None,
835 };
836
837 let params = config.effective_params();
838 assert_eq!(params.single_author_chars, 4);
839 assert_eq!(params.multi_author_chars, 1);
840 assert_eq!(params.et_al_min, 3);
841 assert_eq!(params.et_al_marker, "");
842 assert_eq!(params.et_al_names, 3);
843 assert_eq!(params.year_digits, 2);
844 }
845
846 #[test]
848 fn test_label_config_ams_preset_defaults() {
849 let config = LabelConfig {
850 preset: LabelPreset::Ams,
851 single_author_chars: None,
852 multi_author_chars: None,
853 et_al_min: None,
854 et_al_marker: None,
855 et_al_names: None,
856 year_digits: None,
857 };
858
859 let params = config.effective_params();
860 assert_eq!(params.single_author_chars, 4);
861 assert_eq!(params.multi_author_chars, 1);
862 assert_eq!(params.et_al_min, 5);
863 assert_eq!(params.et_al_marker, "");
864 assert_eq!(params.et_al_names, 4);
865 assert_eq!(params.year_digits, 2);
866 }
867
868 #[test]
870 fn test_processing_author_date_default_bibliography_sort() {
871 let processing = Processing::AuthorDate;
872 let sort = processing.default_bibliography_sort();
873 assert_eq!(sort, Some(SortPreset::AuthorDateTitle));
874 }
875
876 #[test]
878 fn test_processing_numeric_default_bibliography_sort() {
879 let processing = Processing::Numeric;
880 let sort = processing.default_bibliography_sort();
881 assert_eq!(sort, None);
882 }
883
884 #[test]
886 fn test_processing_note_default_bibliography_sort() {
887 let processing = Processing::Note;
888 let sort = processing.default_bibliography_sort();
889 assert_eq!(sort, Some(SortPreset::AuthorTitleDate));
890 }
891
892 #[test]
894 fn test_processing_citation_sort_policy() {
895 let modes = vec![
896 Processing::AuthorDate,
897 Processing::AuthorDateGivenname,
898 Processing::AuthorDateNames,
899 Processing::AuthorDateFull,
900 Processing::Numeric,
901 Processing::Note,
902 Processing::Label(LabelConfig::default()),
903 Processing::Custom(ProcessingCustom::default()),
904 ];
905
906 for mode in modes {
907 assert_eq!(
908 mode.default_citation_sort_policy(),
909 CitationSortPolicy::ExplicitOnly
910 );
911 }
912 }
913
914 #[test]
916 fn test_processing_author_date_variant_configs() {
917 let cases = [
918 (Processing::AuthorDate, false, false, GivennameRule::ByCite),
919 (
920 Processing::AuthorDateGivenname,
921 false,
922 true,
923 GivennameRule::ByCite,
924 ),
925 (
926 Processing::AuthorDateNames,
927 true,
928 false,
929 GivennameRule::ByCite,
930 ),
931 (
933 Processing::AuthorDateFull,
934 true,
935 true,
936 GivennameRule::PrimaryName,
937 ),
938 ];
939
940 for (processing, names, add_givenname, expected_rule) in cases {
941 let config = processing.config();
942
943 assert_eq!(
944 config.sort,
945 Some(SortEntry::Preset(SortPreset::AuthorDateTitle))
946 );
947 assert_eq!(
948 config.group,
949 Some(Group {
950 template: vec![SortKey::Author, SortKey::Year],
951 })
952 );
953
954 let disambig = config.disambiguate.unwrap();
955 assert_eq!(disambig.names, names);
956 assert_eq!(disambig.add_givenname, add_givenname);
957 assert_eq!(disambig.givenname_rule, expected_rule);
958 assert!(disambig.year_suffix);
959 }
960 }
961
962 #[test]
964 fn test_processing_author_date_variant_names() {
965 let cases = [
966 (Processing::AuthorDate, "author-date"),
967 (Processing::AuthorDateGivenname, "author-date-givenname"),
968 (Processing::AuthorDateNames, "author-date-names"),
969 (Processing::AuthorDateFull, "author-date-full"),
970 ];
971
972 for (processing, name) in cases {
973 let serialized = serde_yaml::to_string(&processing).unwrap();
974 assert_eq!(serialized.trim(), name);
975
976 let deserialized: Processing = serde_yaml::from_str(name).unwrap();
977 assert_eq!(deserialized, processing);
978 }
979 }
980
981 #[test]
984 fn test_processing_custom_base_round_trip() {
985 let processing = Processing::Custom(ProcessingCustom {
987 base: Some(ProcessingBase::AuthorDate),
988 sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
989 group: None,
990 disambiguate: None,
991 });
992
993 let yaml = serde_yaml::to_string(&processing).unwrap();
995 let parsed: Processing = serde_yaml::from_str(&yaml).unwrap();
996
997 assert_eq!(yaml.trim(), "base: author-date\nsort: author-title-date");
999 assert_eq!(parsed, processing);
1000 }
1001
1002 #[test]
1005 fn test_processing_custom_base_only_resolves_to_preset_config() {
1006 let parsed: Processing = serde_yaml::from_str("base: author-date-full").unwrap();
1008
1009 assert_eq!(
1011 parsed,
1012 Processing::Custom(ProcessingCustom {
1013 base: Some(ProcessingBase::AuthorDateFull),
1014 sort: None,
1015 group: None,
1016 disambiguate: None,
1017 })
1018 );
1019
1020 assert_eq!(parsed.config(), Processing::AuthorDateFull.config());
1022 }
1023
1024 #[test]
1027 fn test_processing_custom_resolved_overlay_semantics() {
1028 let custom = ProcessingCustom {
1030 base: Some(ProcessingBase::AuthorDate),
1031 sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
1032 group: None,
1033 disambiguate: None,
1034 };
1035
1036 let resolved = custom.resolved();
1038
1039 let base_config = Processing::AuthorDate.config();
1041 assert_eq!(resolved.base, None);
1042 assert_eq!(
1043 resolved.sort,
1044 Some(SortEntry::Preset(SortPreset::AuthorTitleDate))
1045 );
1046 assert_eq!(resolved.group, base_config.group);
1047 assert_eq!(resolved.disambiguate, base_config.disambiguate);
1048 }
1049
1050 #[test]
1052 fn test_processing_custom_resolved_without_base_is_identity() {
1053 let custom = ProcessingCustom {
1054 base: None,
1055 sort: Some(SortEntry::Preset(SortPreset::AuthorDateTitle)),
1056 group: None,
1057 disambiguate: None,
1058 };
1059
1060 assert_eq!(custom.resolved(), custom);
1061 }
1062
1063 #[test]
1066 fn test_processing_custom_base_family_delegation() {
1067 let with_base = Processing::Custom(ProcessingCustom {
1069 base: Some(ProcessingBase::AuthorDate),
1070 ..Default::default()
1071 });
1072 let without_base = Processing::Custom(ProcessingCustom::default());
1073
1074 assert_eq!(with_base.regime_family(), RegimeFamily::AuthorDate);
1076 assert!(with_base.is_author_date_family());
1077 assert_eq!(without_base.regime_family(), RegimeFamily::Custom);
1078 assert!(!without_base.is_author_date_family());
1079
1080 let numeric_base = Processing::Custom(ProcessingCustom {
1082 base: Some(ProcessingBase::Numeric),
1083 ..Default::default()
1084 });
1085 assert_eq!(numeric_base.regime_family(), RegimeFamily::Numeric);
1086 assert!(!numeric_base.is_author_date_family());
1087 }
1088
1089 #[test]
1092 fn test_processing_custom_base_default_bibliography_sort() {
1093 let inherited = Processing::Custom(ProcessingCustom {
1095 base: Some(ProcessingBase::AuthorDate),
1096 ..Default::default()
1097 });
1098 assert_eq!(
1100 inherited.default_bibliography_sort(),
1101 Some(SortPreset::AuthorDateTitle)
1102 );
1103
1104 let overridden = Processing::Custom(ProcessingCustom {
1106 base: Some(ProcessingBase::AuthorDate),
1107 sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
1108 ..Default::default()
1109 });
1110 assert_eq!(overridden.default_bibliography_sort(), None);
1112 }
1113
1114 #[test]
1117 fn test_processing_custom_map_accepts_explicit_nulls() {
1118 let parsed: Processing =
1120 serde_yaml::from_str("base: ~\nsort: author-title-date\ndisambiguate: null").unwrap();
1121
1122 assert_eq!(
1124 parsed,
1125 Processing::Custom(ProcessingCustom {
1126 base: None,
1127 sort: Some(SortEntry::Preset(SortPreset::AuthorTitleDate)),
1128 group: None,
1129 disambiguate: None,
1130 })
1131 );
1132 }
1133
1134 #[test]
1136 fn test_processing_custom_base_rejects_invalid_values() {
1137 let nested = serde_yaml::from_str::<Processing>("base: { sort: author-date-title }");
1139 let unknown = serde_yaml::from_str::<Processing>("base: fancy-date");
1140
1141 assert!(nested.is_err());
1143 assert!(unknown.is_err());
1144 }
1145
1146 #[test]
1148 fn test_disambiguation_defaults() {
1149 let disambig = Disambiguation::default();
1150 assert!(disambig.names);
1151 assert!(!disambig.add_givenname);
1152 assert_eq!(disambig.givenname_rule, GivennameRule::ByCite);
1153 assert!(!disambig.year_suffix);
1154 }
1155
1156 #[test]
1158 fn test_sort_entry_resolve_preset() {
1159 let entry = SortEntry::Preset(SortPreset::AuthorDateTitle);
1160 let sort = entry.resolve();
1161
1162 assert!(!sort.template.is_empty());
1164 }
1165
1166 #[test]
1169 fn test_sort_group_sort_maps_keys_and_skips_citation_number() {
1170 let sort = Sort {
1171 shorten_names: false,
1172 render_substitutions: false,
1173 template: vec![
1174 SortSpec {
1175 key: SortKey::Author,
1176 ascending: true,
1177 },
1178 SortSpec {
1179 key: SortKey::Year,
1180 ascending: false,
1181 },
1182 SortSpec {
1183 key: SortKey::Title,
1184 ascending: true,
1185 },
1186 SortSpec {
1187 key: SortKey::CitationNumber,
1188 ascending: true,
1189 },
1190 ],
1191 };
1192
1193 let group_sort = sort.group_sort();
1194
1195 assert_eq!(group_sort.template.len(), 3);
1196 assert_eq!(group_sort.template[0].key, crate::grouping::SortKey::Author);
1197 assert!(group_sort.template[0].ascending);
1198 assert_eq!(group_sort.template[1].key, crate::grouping::SortKey::Issued);
1199 assert!(!group_sort.template[1].ascending);
1200 assert_eq!(group_sort.template[2].key, crate::grouping::SortKey::Title);
1201 assert!(group_sort.template[2].ascending);
1202 }
1203
1204 #[test]
1206 fn test_sort_entry_resolve_explicit() {
1207 let explicit = Sort {
1208 shorten_names: true,
1209 render_substitutions: false,
1210 template: vec![SortSpec {
1211 key: SortKey::Title,
1212 ascending: false,
1213 }],
1214 };
1215 let entry = SortEntry::Explicit(explicit.clone());
1216 let resolved = entry.resolve();
1217
1218 assert!(resolved.shorten_names);
1219 assert!(!resolved.render_substitutions);
1220 assert_eq!(resolved.template.len(), 1);
1221 assert_eq!(resolved.template[0].key, SortKey::Title);
1222 assert!(!resolved.template[0].ascending);
1223 }
1224}