Skip to main content

atypical_commit/
config.rs

1// The `[commit]` section of atypical.toml: an owned mirror of
2// `Tokens`, defaulting field by field to the standard preset.
3
4use serde::Deserialize;
5
6use crate::{DelimitedBy, EnclosureToken, Sequence, Tokens};
7
8pub const SECTION: &str = "commit";
9
10#[derive(Debug, Clone, PartialEq)]
11#[derive(Deserialize)]
12#[serde(deny_unknown_fields, rename_all = "kebab-case", default)]
13pub struct CommitConfig {
14    pub keywords: Vec<String>,
15    pub modifiers: Vec<String>,
16    pub enclosures: Vec<EnclosureConfig>,
17    pub separator: char,
18    pub modifier_sequence: Sequence,
19    /// Skip machine-generated headers (merges, reverts, version
20    /// bumps...); not part of the grammar, so absent from `Tokens`.
21    pub default_ignores: bool,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25#[derive(Deserialize)]
26#[serde(deny_unknown_fields, rename_all = "kebab-case")]
27pub struct EnclosureConfig {
28    pub delimiters: DelimitedBy,
29    /// Restricts the contents to these values; anything goes when omitted.
30    pub allowed: Option<Vec<String>>,
31}
32
33impl Default for CommitConfig {
34    fn default() -> Self {
35        (&Tokens::preset_standard()).into()
36    }
37}
38
39impl From<&Tokens<'_>> for CommitConfig {
40    fn from(tokens: &Tokens<'_>) -> Self {
41        fn owned(v: &[&str]) -> Vec<String> {
42            v.iter().map(ToString::to_string).collect()
43        }
44
45        Self {
46            keywords: owned(&tokens.keywords),
47            modifiers: owned(&tokens.modifiers),
48            enclosures: tokens
49                .enclosures
50                .iter()
51                .map(|enclosure| EnclosureConfig {
52                    delimiters: enclosure.delimiters(),
53                    allowed: match enclosure {
54                        EnclosureToken::Flexible(_) => None,
55                        EnclosureToken::Strict(_, allowed) => {
56                            Some(owned(allowed))
57                        }
58                    },
59                })
60                .collect(),
61            separator: tokens.separator,
62            modifier_sequence: tokens.modifier_sequence,
63            default_ignores: true,
64        }
65    }
66}
67
68impl<'i> From<&'i CommitConfig> for Tokens<'i> {
69    fn from(config: &'i CommitConfig) -> Self {
70        fn borrowed(v: &[String]) -> Vec<&str> {
71            v.iter().map(String::as_str).collect()
72        }
73
74        Self {
75            keywords: borrowed(&config.keywords),
76            modifiers: borrowed(&config.modifiers),
77            enclosures: config
78                .enclosures
79                .iter()
80                .map(|enclosure| match &enclosure.allowed {
81                    None => EnclosureToken::Flexible(enclosure.delimiters),
82                    Some(allowed) => EnclosureToken::Strict(
83                        enclosure.delimiters,
84                        borrowed(allowed),
85                    ),
86                })
87                .collect(),
88            separator: config.separator,
89            modifier_sequence: config.modifier_sequence,
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_default_mirrors_the_standard_preset() {
100        let config = CommitConfig::default();
101
102        assert_eq!(Tokens::from(&config), Tokens::preset_standard());
103    }
104
105    #[test]
106    fn test_partial_section_keeps_preset_defaults() {
107        let config: CommitConfig =
108            toml::from_str(r#"keywords = ["feat", "fix"]"#).unwrap();
109
110        assert_eq!(config.keywords, vec!["feat", "fix"]);
111        assert_eq!(config.modifiers, CommitConfig::default().modifiers);
112        assert_eq!(config.separator, ':');
113    }
114
115    #[test]
116    fn test_enclosures_map_to_strict_and_flexible() {
117        let config: CommitConfig = toml::from_str(indoc::indoc! {r#"
118            [[enclosures]]
119            delimiters = ["(", ")"]
120            allowed = ["core"]
121
122            [[enclosures]]
123            delimiters = ["{", "}"]
124        "#})
125        .unwrap();
126
127        assert_eq!(
128            Tokens::from(&config).enclosures,
129            vec![
130                EnclosureToken::Strict(['(', ')'], vec!["core"]),
131                EnclosureToken::Flexible(['{', '}']),
132            ]
133        );
134    }
135
136    #[test]
137    fn test_default_ignores_is_on_unless_disabled() {
138        assert!(CommitConfig::default().default_ignores);
139
140        let config: CommitConfig =
141            toml::from_str("default-ignores = false").unwrap();
142
143        assert!(!config.default_ignores);
144    }
145
146    #[test]
147    fn test_modifier_sequence_names() {
148        let config: CommitConfig =
149            toml::from_str(r#"modifier-sequence = "post""#).unwrap();
150
151        assert_eq!(config.modifier_sequence, Sequence::Post);
152
153        assert!(
154            toml::from_str::<CommitConfig>(r#"modifier-sequence = "sideways""#)
155                .is_err()
156        );
157    }
158
159    #[test]
160    fn test_unknown_fields_are_rejected() {
161        assert!(
162            toml::from_str::<CommitConfig>(r#"keyword = ["typo"]"#).is_err()
163        );
164    }
165}