1use std::sync::LazyLock;
2
3use derive_builder::Builder;
4use regex::Regex;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 config::{prerelease::PrereleaseConfig, release_type::ReleaseType},
10 result::{ReleasaurusError, Result},
11};
12
13pub const GENERIC_VERSION_REGEX_PATTERN: &str = r#"(?mi)(?<start>.*version"?:?\s*=?\s*['"]?)(?<version>\d+\.\d+\.\d+-?.*?)(?<end>['",].*)?$"#;
15
16pub static GENERIC_VERSION_REGEX: LazyLock<Regex> =
18 LazyLock::new(|| Regex::new(GENERIC_VERSION_REGEX_PATTERN).unwrap());
19
20pub const DEFAULT_TAG_PREFIX: &str = "v";
22
23#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
41#[serde(untagged)]
42pub enum AdditionalManifestSpec {
43 Path(String),
45 Full(AdditionalManifest),
47}
48
49impl AdditionalManifestSpec {
50 pub fn into_manifest(self) -> AdditionalManifest {
55 match self {
56 AdditionalManifestSpec::Path(path) => AdditionalManifest {
57 path,
58 version_regex: Some(GENERIC_VERSION_REGEX_PATTERN.to_string()),
59 },
60 AdditionalManifestSpec::Full(mut manifest) => {
61 if manifest.version_regex.is_none() {
63 manifest.version_regex =
64 Some(GENERIC_VERSION_REGEX_PATTERN.to_string());
65 }
66 manifest
67 }
68 }
69 }
70}
71
72#[derive(
75 Debug, Default, Clone, Serialize, Deserialize, JsonSchema, Builder,
76)]
77pub struct AdditionalManifest {
78 pub path: String,
80 pub version_regex: Option<String>,
83}
84
85#[derive(
90 Debug, Default, Clone, Serialize, Deserialize, JsonSchema, Builder,
91)]
92pub struct SubPackage {
93 pub name: String,
98 pub path: String,
101 pub release_type: Option<ReleaseType>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Builder)]
107#[serde(default)] #[builder(setter(into, strip_option), default)]
109pub struct PackageConfig {
110 pub name: String,
115 pub workspace_root: String,
118 pub path: String,
120 pub release_type: Option<ReleaseType>,
122 pub tag_prefix: Option<String>,
124 pub sub_packages: Option<Vec<SubPackage>>,
128 pub prerelease: Option<PrereleaseConfig>,
130 pub auto_start_next: Option<bool>,
133 pub additional_paths: Option<Vec<String>>,
135 pub additional_manifest_files: Option<Vec<AdditionalManifestSpec>>,
139 pub breaking_always_increment_major: Option<bool>,
141 pub features_always_increment_minor: Option<bool>,
143 pub custom_major_increment_regex: Option<String>,
149 pub custom_minor_increment_regex: Option<String>,
154}
155
156impl Default for PackageConfig {
157 fn default() -> Self {
158 Self {
159 name: "".into(),
160 path: ".".into(),
161 workspace_root: ".".into(),
162 sub_packages: None,
163 release_type: None,
164 tag_prefix: None,
165 prerelease: None,
166 auto_start_next: None,
167 additional_paths: None,
168 additional_manifest_files: None,
169 breaking_always_increment_major: None,
170 features_always_increment_minor: None,
171 custom_major_increment_regex: None,
172 custom_minor_increment_regex: None,
173 }
174 }
175}
176
177impl PackageConfig {
178 pub fn tag_prefix(&self) -> Result<String> {
179 self.tag_prefix.clone().ok_or_else(|| {
180 ReleasaurusError::invalid_config(format!(
181 "failed to resolve tag prefix for package: {}",
182 self.name
183 ))
184 })
185 }
186}
187
188impl From<SubPackage> for PackageConfig {
189 fn from(value: SubPackage) -> Self {
190 Self {
191 path: value.path,
192 release_type: value.release_type,
193 ..Default::default()
194 }
195 }
196}
197
198impl From<&SubPackage> for PackageConfig {
199 fn from(value: &SubPackage) -> Self {
200 Self {
201 path: value.path.clone(),
202 release_type: value.release_type,
203 ..Default::default()
204 }
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn deserializes_string_path_format() {
214 let toml = r#"
215 additional_manifest_files = ["VERSION", "README.md"]
216 "#;
217
218 #[derive(Deserialize)]
219 struct TestConfig {
220 additional_manifest_files: Option<Vec<AdditionalManifestSpec>>,
221 }
222
223 let config: TestConfig = toml::from_str(toml).unwrap();
224 let specs = config.additional_manifest_files.unwrap();
225
226 assert_eq!(specs.len(), 2);
227
228 let manifest1 = specs[0].clone().into_manifest();
229 assert_eq!(manifest1.path, "VERSION");
230 assert_eq!(
231 manifest1.version_regex,
232 Some(GENERIC_VERSION_REGEX_PATTERN.to_string())
233 );
234
235 let manifest2 = specs[1].clone().into_manifest();
236 assert_eq!(manifest2.path, "README.md");
237 assert_eq!(
238 manifest2.version_regex,
239 Some(GENERIC_VERSION_REGEX_PATTERN.to_string())
240 );
241 }
242
243 #[test]
244 fn deserializes_full_object_format() {
245 let toml = r#"
246 [[additional_manifest_files]]
247 path = "VERSION"
248 version_regex = "version:\\s*(\\d+\\.\\d+\\.\\d+)"
249 "#;
250
251 #[derive(Deserialize)]
252 struct TestConfig {
253 additional_manifest_files: Option<Vec<AdditionalManifestSpec>>,
254 }
255
256 let config: TestConfig = toml::from_str(toml).unwrap();
257 let specs = config.additional_manifest_files.unwrap();
258
259 assert_eq!(specs.len(), 1);
260
261 let manifest = specs[0].clone().into_manifest();
262 assert_eq!(manifest.path, "VERSION");
263 assert_eq!(
264 manifest.version_regex,
265 Some("version:\\s*(\\d+\\.\\d+\\.\\d+)".to_string())
266 );
267 }
268
269 #[test]
270 fn deserializes_mixed_format() {
271 let toml = r#"
272 additional_manifest_files = [
273 "VERSION",
274 { path = "config.yml", version_regex = "v:\\s*(\\d+\\.\\d+\\.\\d+)" }
275 ]
276 "#;
277
278 #[derive(Deserialize)]
279 struct TestConfig {
280 additional_manifest_files: Option<Vec<AdditionalManifestSpec>>,
281 }
282
283 let config: TestConfig = toml::from_str(toml).unwrap();
284 let specs = config.additional_manifest_files.unwrap();
285
286 assert_eq!(specs.len(), 2);
287
288 let manifest1 = specs[0].clone().into_manifest();
289 assert_eq!(manifest1.path, "VERSION");
290 assert_eq!(
291 manifest1.version_regex,
292 Some(GENERIC_VERSION_REGEX_PATTERN.to_string())
293 );
294
295 let manifest2 = specs[1].clone().into_manifest();
296 assert_eq!(manifest2.path, "config.yml");
297 assert_eq!(
298 manifest2.version_regex,
299 Some("v:\\s*(\\d+\\.\\d+\\.\\d+)".to_string())
300 );
301 }
302
303 #[test]
304 fn deserializes_full_package_config_with_manifest_files() {
305 let toml = r#"
306 [[package]]
307 path = "."
308 release_type = "rust"
309 additional_manifest_files = ["VERSION", "README.md"]
310
311 [[package]]
312 path = "packages/api"
313 release_type = "node"
314 additional_manifest_files = [
315 "VERSION",
316 { path = "config.yml", version_regex = "v:\\s*(\\d+\\.\\d+\\.\\d+)" }
317 ]
318 "#;
319
320 #[derive(Deserialize)]
321 struct TestConfig {
322 package: Vec<PackageConfig>,
323 }
324
325 let config: TestConfig = toml::from_str(toml).unwrap();
326
327 assert_eq!(config.package.len(), 2);
328
329 let pkg1_specs = config.package[0]
331 .additional_manifest_files
332 .as_ref()
333 .unwrap();
334 assert_eq!(pkg1_specs.len(), 2);
335 let manifest1 = pkg1_specs[0].clone().into_manifest();
336 assert_eq!(manifest1.path, "VERSION");
337 assert_eq!(
338 manifest1.version_regex,
339 Some(GENERIC_VERSION_REGEX_PATTERN.to_string())
340 );
341
342 let pkg2_specs = config.package[1]
344 .additional_manifest_files
345 .as_ref()
346 .unwrap();
347 assert_eq!(pkg2_specs.len(), 2);
348 let manifest2_1 = pkg2_specs[0].clone().into_manifest();
349 assert_eq!(manifest2_1.path, "VERSION");
350 assert_eq!(
351 manifest2_1.version_regex,
352 Some(GENERIC_VERSION_REGEX_PATTERN.to_string())
353 );
354
355 let manifest2_2 = pkg2_specs[1].clone().into_manifest();
356 assert_eq!(manifest2_2.path, "config.yml");
357 assert_eq!(
358 manifest2_2.version_regex,
359 Some("v:\\s*(\\d+\\.\\d+\\.\\d+)".to_string())
360 );
361 }
362
363 #[test]
364 fn normalizes_full_variant_with_none_to_default_pattern() {
365 let spec = AdditionalManifestSpec::Full(AdditionalManifest {
367 path: "VERSION".to_string(),
368 version_regex: None,
369 });
370
371 let manifest = spec.into_manifest();
372 assert_eq!(manifest.path, "VERSION");
373 assert_eq!(
374 manifest.version_regex,
375 Some(GENERIC_VERSION_REGEX_PATTERN.to_string())
376 );
377 }
378
379 #[test]
380 fn preserves_full_variant_custom_regex() {
381 let custom_pattern = "custom:\\s*(\\d+\\.\\d+\\.\\d+)".to_string();
383 let spec = AdditionalManifestSpec::Full(AdditionalManifest {
384 path: "config.yml".to_string(),
385 version_regex: Some(custom_pattern.clone()),
386 });
387
388 let manifest = spec.into_manifest();
389 assert_eq!(manifest.path, "config.yml");
390 assert_eq!(manifest.version_regex, Some(custom_pattern));
391 }
392}