1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use std::str::FromStr;
5
6#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
8pub struct Config {
9 #[serde(default)]
10 pub settings: Settings,
11
12 #[serde(default)]
16 pub components: Vec<Component>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
21pub struct Settings {
22 #[serde(default = "default_base_branch")]
24 pub base_branch: String,
25
26 #[serde(default = "default_branch_template")]
28 pub branch_template: String,
29
30 #[serde(default)]
32 pub unmatched_files: UnmatchedPolicy,
33
34 pub default_commit_type: Option<String>,
36}
37
38#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
40#[serde(rename_all = "lowercase")]
41pub enum UnmatchedPolicy {
42 #[default]
44 Error,
45 Warn,
47 Ignore,
49}
50
51impl FromStr for UnmatchedPolicy {
52 type Err = String;
53
54 fn from_str(s: &str) -> Result<Self, Self::Err> {
55 match s.to_lowercase().as_str() {
56 "error" => Ok(UnmatchedPolicy::Error),
57 "warn" => Ok(UnmatchedPolicy::Warn),
58 "ignore" => Ok(UnmatchedPolicy::Ignore),
59 other => Err(format!(
60 "invalid unmatched_files policy: {other:?} (expected \"error\", \"warn\", or \"ignore\")"
61 )),
62 }
63 }
64}
65
66impl fmt::Display for UnmatchedPolicy {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 UnmatchedPolicy::Error => f.write_str("error"),
70 UnmatchedPolicy::Warn => f.write_str("warn"),
71 UnmatchedPolicy::Ignore => f.write_str("ignore"),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
78pub struct Component {
79 pub name: String,
81
82 pub globs: Vec<String>,
84
85 pub commit_type: Option<String>,
87
88 pub branch: Option<String>,
90}
91
92fn default_base_branch() -> String {
93 "main".into()
94}
95
96fn default_branch_template() -> String {
97 "atomic/{component}".into()
98}
99
100impl Default for Settings {
101 fn default() -> Self {
102 Self {
103 base_branch: default_base_branch(),
104 branch_template: default_branch_template(),
105 unmatched_files: UnmatchedPolicy::default(),
106 default_commit_type: None,
107 }
108 }
109}
110
111impl Config {
112 pub fn sample() -> Self {
115 Self {
116 settings: Settings::default(),
117 components: vec![
118 Component {
119 name: "frontend".into(),
120 globs: vec!["src/ui/**".into(), "src/components/**".into()],
121 commit_type: None,
122 branch: None,
123 },
124 Component {
125 name: "backend".into(),
126 globs: vec!["src/api/**".into(), "src/db/**".into()],
127 commit_type: Some("fix".into()),
128 branch: None,
129 },
130 ],
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn unmatched_policy_from_str() {
141 assert_eq!(
142 UnmatchedPolicy::from_str("error").unwrap(),
143 UnmatchedPolicy::Error
144 );
145 assert_eq!(
146 UnmatchedPolicy::from_str("WARN").unwrap(),
147 UnmatchedPolicy::Warn
148 );
149 assert_eq!(
150 UnmatchedPolicy::from_str("Ignore").unwrap(),
151 UnmatchedPolicy::Ignore
152 );
153 assert!(UnmatchedPolicy::from_str("invalid").is_err());
154 }
155
156 #[test]
157 fn unmatched_policy_display() {
158 assert_eq!(UnmatchedPolicy::Error.to_string(), "error");
159 assert_eq!(UnmatchedPolicy::Warn.to_string(), "warn");
160 assert_eq!(UnmatchedPolicy::Ignore.to_string(), "ignore");
161 }
162
163 #[test]
164 fn config_serde_round_trip() {
165 let original = Config::sample();
166 let toml_str = toml::to_string(&original).expect("serialize to TOML");
167 let deserialized: Config = toml::from_str(&toml_str).expect("deserialize from TOML");
168
169 assert_eq!(
170 deserialized.settings.base_branch,
171 original.settings.base_branch
172 );
173 assert_eq!(
174 deserialized.settings.branch_template,
175 original.settings.branch_template
176 );
177 assert_eq!(
178 deserialized.settings.unmatched_files,
179 original.settings.unmatched_files
180 );
181 assert_eq!(
182 deserialized.settings.default_commit_type,
183 original.settings.default_commit_type
184 );
185 assert_eq!(deserialized.components.len(), original.components.len());
186 for (d, o) in deserialized
187 .components
188 .iter()
189 .zip(original.components.iter())
190 {
191 assert_eq!(d.name, o.name);
192 assert_eq!(d.globs, o.globs);
193 assert_eq!(d.commit_type, o.commit_type);
194 assert_eq!(d.branch, o.branch);
195 }
196 }
197
198 #[test]
199 fn unmatched_policy_serde_round_trip() {
200 #[derive(Debug, Serialize, Deserialize, PartialEq)]
201 struct Wrapper {
202 policy: UnmatchedPolicy,
203 }
204 for policy in [
205 UnmatchedPolicy::Error,
206 UnmatchedPolicy::Warn,
207 UnmatchedPolicy::Ignore,
208 ] {
209 let w = Wrapper {
210 policy: policy.clone(),
211 };
212 let serialized = toml::to_string(&w).expect("serialize");
213 let deserialized: Wrapper = toml::from_str(&serialized).expect("deserialize");
214 assert_eq!(deserialized.policy, policy);
215 }
216 }
217
218 #[test]
219 fn empty_string_from_str_returns_error() {
220 let result = UnmatchedPolicy::from_str("");
221 assert!(result.is_err());
222 assert!(
223 result
224 .unwrap_err()
225 .contains("invalid unmatched_files policy")
226 );
227 }
228}