use std::sync::LazyLock;
use derive_builder::Builder;
use regex::Regex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
config::{prerelease::PrereleaseConfig, release_type::ReleaseType},
result::{ReleasaurusError, Result},
};
pub const GENERIC_VERSION_REGEX_PATTERN: &str = r#"(?mi)(?<start>.*version"?:?\s*=?\s*['"]?)(?<version>\d+\.\d+\.\d+-?.*?)(?<end>['",].*)?$"#;
pub static GENERIC_VERSION_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(GENERIC_VERSION_REGEX_PATTERN).unwrap());
pub const DEFAULT_TAG_PREFIX: &str = "v";
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum AdditionalManifestSpec {
Path(String),
Full(AdditionalManifest),
}
impl AdditionalManifestSpec {
pub fn into_manifest(self) -> AdditionalManifest {
match self {
AdditionalManifestSpec::Path(path) => AdditionalManifest {
path,
version_regex: Some(GENERIC_VERSION_REGEX_PATTERN.to_string()),
},
AdditionalManifestSpec::Full(mut manifest) => {
if manifest.version_regex.is_none() {
manifest.version_regex =
Some(GENERIC_VERSION_REGEX_PATTERN.to_string());
}
manifest
}
}
}
}
#[derive(
Debug, Default, Clone, Serialize, Deserialize, JsonSchema, Builder,
)]
pub struct AdditionalManifest {
pub path: String,
pub version_regex: Option<String>,
}
#[derive(
Debug, Default, Clone, Serialize, Deserialize, JsonSchema, Builder,
)]
pub struct SubPackage {
pub name: String,
pub path: String,
pub release_type: Option<ReleaseType>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Builder)]
#[serde(default)] #[builder(setter(into, strip_option), default)]
pub struct PackageConfig {
pub name: String,
pub workspace_root: String,
pub path: String,
pub release_type: Option<ReleaseType>,
pub tag_prefix: Option<String>,
pub sub_packages: Option<Vec<SubPackage>>,
pub prerelease: Option<PrereleaseConfig>,
pub auto_start_next: Option<bool>,
pub additional_paths: Option<Vec<String>>,
pub additional_manifest_files: Option<Vec<AdditionalManifestSpec>>,
pub breaking_always_increment_major: Option<bool>,
pub features_always_increment_minor: Option<bool>,
pub custom_major_increment_regex: Option<String>,
pub custom_minor_increment_regex: Option<String>,
}
impl Default for PackageConfig {
fn default() -> Self {
Self {
name: "".into(),
path: ".".into(),
workspace_root: ".".into(),
sub_packages: None,
release_type: None,
tag_prefix: None,
prerelease: None,
auto_start_next: None,
additional_paths: None,
additional_manifest_files: None,
breaking_always_increment_major: None,
features_always_increment_minor: None,
custom_major_increment_regex: None,
custom_minor_increment_regex: None,
}
}
}
impl PackageConfig {
pub fn tag_prefix(&self) -> Result<String> {
self.tag_prefix.clone().ok_or_else(|| {
ReleasaurusError::invalid_config(format!(
"failed to resolve tag prefix for package: {}",
self.name
))
})
}
}
impl From<SubPackage> for PackageConfig {
fn from(value: SubPackage) -> Self {
Self {
path: value.path,
release_type: value.release_type,
..Default::default()
}
}
}
impl From<&SubPackage> for PackageConfig {
fn from(value: &SubPackage) -> Self {
Self {
path: value.path.clone(),
release_type: value.release_type,
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserializes_string_path_format() {
let toml = r#"
additional_manifest_files = ["VERSION", "README.md"]
"#;
#[derive(Deserialize)]
struct TestConfig {
additional_manifest_files: Option<Vec<AdditionalManifestSpec>>,
}
let config: TestConfig = toml::from_str(toml).unwrap();
let specs = config.additional_manifest_files.unwrap();
assert_eq!(specs.len(), 2);
let manifest1 = specs[0].clone().into_manifest();
assert_eq!(manifest1.path, "VERSION");
assert_eq!(
manifest1.version_regex,
Some(GENERIC_VERSION_REGEX_PATTERN.to_string())
);
let manifest2 = specs[1].clone().into_manifest();
assert_eq!(manifest2.path, "README.md");
assert_eq!(
manifest2.version_regex,
Some(GENERIC_VERSION_REGEX_PATTERN.to_string())
);
}
#[test]
fn deserializes_full_object_format() {
let toml = r#"
[[additional_manifest_files]]
path = "VERSION"
version_regex = "version:\\s*(\\d+\\.\\d+\\.\\d+)"
"#;
#[derive(Deserialize)]
struct TestConfig {
additional_manifest_files: Option<Vec<AdditionalManifestSpec>>,
}
let config: TestConfig = toml::from_str(toml).unwrap();
let specs = config.additional_manifest_files.unwrap();
assert_eq!(specs.len(), 1);
let manifest = specs[0].clone().into_manifest();
assert_eq!(manifest.path, "VERSION");
assert_eq!(
manifest.version_regex,
Some("version:\\s*(\\d+\\.\\d+\\.\\d+)".to_string())
);
}
#[test]
fn deserializes_mixed_format() {
let toml = r#"
additional_manifest_files = [
"VERSION",
{ path = "config.yml", version_regex = "v:\\s*(\\d+\\.\\d+\\.\\d+)" }
]
"#;
#[derive(Deserialize)]
struct TestConfig {
additional_manifest_files: Option<Vec<AdditionalManifestSpec>>,
}
let config: TestConfig = toml::from_str(toml).unwrap();
let specs = config.additional_manifest_files.unwrap();
assert_eq!(specs.len(), 2);
let manifest1 = specs[0].clone().into_manifest();
assert_eq!(manifest1.path, "VERSION");
assert_eq!(
manifest1.version_regex,
Some(GENERIC_VERSION_REGEX_PATTERN.to_string())
);
let manifest2 = specs[1].clone().into_manifest();
assert_eq!(manifest2.path, "config.yml");
assert_eq!(
manifest2.version_regex,
Some("v:\\s*(\\d+\\.\\d+\\.\\d+)".to_string())
);
}
#[test]
fn deserializes_full_package_config_with_manifest_files() {
let toml = r#"
[[package]]
path = "."
release_type = "rust"
additional_manifest_files = ["VERSION", "README.md"]
[[package]]
path = "packages/api"
release_type = "node"
additional_manifest_files = [
"VERSION",
{ path = "config.yml", version_regex = "v:\\s*(\\d+\\.\\d+\\.\\d+)" }
]
"#;
#[derive(Deserialize)]
struct TestConfig {
package: Vec<PackageConfig>,
}
let config: TestConfig = toml::from_str(toml).unwrap();
assert_eq!(config.package.len(), 2);
let pkg1_specs = config.package[0]
.additional_manifest_files
.as_ref()
.unwrap();
assert_eq!(pkg1_specs.len(), 2);
let manifest1 = pkg1_specs[0].clone().into_manifest();
assert_eq!(manifest1.path, "VERSION");
assert_eq!(
manifest1.version_regex,
Some(GENERIC_VERSION_REGEX_PATTERN.to_string())
);
let pkg2_specs = config.package[1]
.additional_manifest_files
.as_ref()
.unwrap();
assert_eq!(pkg2_specs.len(), 2);
let manifest2_1 = pkg2_specs[0].clone().into_manifest();
assert_eq!(manifest2_1.path, "VERSION");
assert_eq!(
manifest2_1.version_regex,
Some(GENERIC_VERSION_REGEX_PATTERN.to_string())
);
let manifest2_2 = pkg2_specs[1].clone().into_manifest();
assert_eq!(manifest2_2.path, "config.yml");
assert_eq!(
manifest2_2.version_regex,
Some("v:\\s*(\\d+\\.\\d+\\.\\d+)".to_string())
);
}
#[test]
fn normalizes_full_variant_with_none_to_default_pattern() {
let spec = AdditionalManifestSpec::Full(AdditionalManifest {
path: "VERSION".to_string(),
version_regex: None,
});
let manifest = spec.into_manifest();
assert_eq!(manifest.path, "VERSION");
assert_eq!(
manifest.version_regex,
Some(GENERIC_VERSION_REGEX_PATTERN.to_string())
);
}
#[test]
fn preserves_full_variant_custom_regex() {
let custom_pattern = "custom:\\s*(\\d+\\.\\d+\\.\\d+)".to_string();
let spec = AdditionalManifestSpec::Full(AdditionalManifest {
path: "config.yml".to_string(),
version_regex: Some(custom_pattern.clone()),
});
let manifest = spec.into_manifest();
assert_eq!(manifest.path, "config.yml");
assert_eq!(manifest.version_regex, Some(custom_pattern));
}
}