1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5use crate::commit::{CommitType, DEFAULT_COMMIT_PATTERN, default_commit_types};
6use crate::error::ReleaseError;
7use crate::version::BumpLevel;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(default)]
11pub struct ReleaseConfig {
12 pub branches: Vec<String>,
13 pub tag_prefix: String,
14 pub commit_pattern: String,
15 pub breaking_section: String,
16 pub misc_section: String,
17 pub types: Vec<CommitType>,
18 pub changelog: ChangelogConfig,
19 pub version_files: Vec<String>,
20 pub version_files_strict: bool,
21 pub artifacts: Vec<String>,
22 pub floating_tags: bool,
23 pub build_command: Option<String>,
24 pub stage_files: Vec<String>,
26 pub prerelease: Option<String>,
29 pub pre_release_command: Option<String>,
31 pub post_release_command: Option<String>,
33}
34
35impl Default for ReleaseConfig {
36 fn default() -> Self {
37 Self {
38 branches: vec!["main".into(), "master".into()],
39 tag_prefix: "v".into(),
40 commit_pattern: DEFAULT_COMMIT_PATTERN.into(),
41 breaking_section: "Breaking Changes".into(),
42 misc_section: "Miscellaneous".into(),
43 types: default_commit_types(),
44 changelog: ChangelogConfig::default(),
45 version_files: vec![],
46 version_files_strict: false,
47 artifacts: vec![],
48 floating_tags: false,
49 build_command: None,
50 stage_files: vec![],
51 prerelease: None,
52 pre_release_command: None,
53 post_release_command: None,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Default, Serialize, Deserialize)]
59#[serde(default)]
60pub struct ChangelogConfig {
61 pub file: Option<String>,
62 pub template: Option<String>,
63}
64
65impl ReleaseConfig {
66 pub fn load(path: &Path) -> Result<Self, ReleaseError> {
68 if !path.exists() {
69 return Ok(Self::default());
70 }
71
72 let contents =
73 std::fs::read_to_string(path).map_err(|e| ReleaseError::Config(e.to_string()))?;
74
75 serde_yaml_ng::from_str(&contents).map_err(|e| ReleaseError::Config(e.to_string()))
76 }
77}
78
79impl<'de> Deserialize<'de> for BumpLevel {
81 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
82 where
83 D: serde::Deserializer<'de>,
84 {
85 let s = String::deserialize(deserializer)?;
86 match s.as_str() {
87 "major" => Ok(BumpLevel::Major),
88 "minor" => Ok(BumpLevel::Minor),
89 "patch" => Ok(BumpLevel::Patch),
90 _ => Err(serde::de::Error::custom(format!("unknown bump level: {s}"))),
91 }
92 }
93}
94
95impl Serialize for BumpLevel {
96 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
97 where
98 S: serde::Serializer,
99 {
100 let s = match self {
101 BumpLevel::Major => "major",
102 BumpLevel::Minor => "minor",
103 BumpLevel::Patch => "patch",
104 };
105 serializer.serialize_str(s)
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use std::io::Write;
113
114 #[test]
115 fn default_values() {
116 let config = ReleaseConfig::default();
117 assert_eq!(config.branches, vec!["main", "master"]);
118 assert_eq!(config.tag_prefix, "v");
119 assert_eq!(config.commit_pattern, DEFAULT_COMMIT_PATTERN);
120 assert_eq!(config.breaking_section, "Breaking Changes");
121 assert_eq!(config.misc_section, "Miscellaneous");
122 assert!(!config.types.is_empty());
123 assert!(!config.version_files_strict);
124 assert!(config.artifacts.is_empty());
125 assert!(!config.floating_tags);
126 }
127
128 #[test]
129 fn load_missing_file() {
130 let dir = tempfile::tempdir().unwrap();
131 let path = dir.path().join("nonexistent.yml");
132 let config = ReleaseConfig::load(&path).unwrap();
133 assert_eq!(config.tag_prefix, "v");
134 }
135
136 #[test]
137 fn load_valid_yaml() {
138 let dir = tempfile::tempdir().unwrap();
139 let path = dir.path().join("config.yml");
140 let mut f = std::fs::File::create(&path).unwrap();
141 writeln!(f, "branches:\n - develop\ntag_prefix: release-").unwrap();
142
143 let config = ReleaseConfig::load(&path).unwrap();
144 assert_eq!(config.branches, vec!["develop"]);
145 assert_eq!(config.tag_prefix, "release-");
146 }
147
148 #[test]
149 fn load_partial_yaml() {
150 let dir = tempfile::tempdir().unwrap();
151 let path = dir.path().join("config.yml");
152 std::fs::write(&path, "tag_prefix: rel-\n").unwrap();
153
154 let config = ReleaseConfig::load(&path).unwrap();
155 assert_eq!(config.tag_prefix, "rel-");
156 assert_eq!(config.branches, vec!["main", "master"]);
157 assert_eq!(config.commit_pattern, DEFAULT_COMMIT_PATTERN);
159 assert_eq!(config.breaking_section, "Breaking Changes");
160 assert!(!config.types.is_empty());
161 }
162
163 #[test]
164 fn load_yaml_with_artifacts() {
165 let dir = tempfile::tempdir().unwrap();
166 let path = dir.path().join("config.yml");
167 std::fs::write(
168 &path,
169 "artifacts:\n - \"dist/*.tar.gz\"\n - \"build/output-*\"\n",
170 )
171 .unwrap();
172
173 let config = ReleaseConfig::load(&path).unwrap();
174 assert_eq!(config.artifacts, vec!["dist/*.tar.gz", "build/output-*"]);
175 assert_eq!(config.tag_prefix, "v");
177 }
178
179 #[test]
180 fn load_yaml_with_floating_tags() {
181 let dir = tempfile::tempdir().unwrap();
182 let path = dir.path().join("config.yml");
183 std::fs::write(&path, "floating_tags: true\n").unwrap();
184
185 let config = ReleaseConfig::load(&path).unwrap();
186 assert!(config.floating_tags);
187 assert_eq!(config.tag_prefix, "v");
189 }
190
191 #[test]
192 fn bump_level_roundtrip() {
193 for (level, expected) in [
194 (BumpLevel::Major, "major"),
195 (BumpLevel::Minor, "minor"),
196 (BumpLevel::Patch, "patch"),
197 ] {
198 let yaml = serde_yaml_ng::to_string(&level).unwrap();
199 assert!(yaml.contains(expected));
200 let parsed: BumpLevel = serde_yaml_ng::from_str(&yaml).unwrap();
201 assert_eq!(parsed, level);
202 }
203 }
204
205 #[test]
206 fn types_roundtrip() {
207 let config = ReleaseConfig::default();
208 let yaml = serde_yaml_ng::to_string(&config).unwrap();
209 let parsed: ReleaseConfig = serde_yaml_ng::from_str(&yaml).unwrap();
210 assert_eq!(parsed.types.len(), config.types.len());
211 assert_eq!(parsed.types[0].name, "feat");
212 assert_eq!(parsed.commit_pattern, config.commit_pattern);
213 assert_eq!(parsed.breaking_section, config.breaking_section);
214 }
215}