cuenv_release/
config.rs

1//! Release configuration types.
2//!
3//! This module defines the Rust representations of the release configuration
4//! that can be specified in `env.cue` files.
5
6use serde::{Deserialize, Serialize};
7
8/// Complete release configuration.
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10#[serde(default)]
11pub struct ReleaseConfig {
12    /// Git-related release settings.
13    pub git: ReleaseGitConfig,
14    /// Package grouping configuration.
15    pub packages: ReleasePackagesConfig,
16    /// Changelog generation configuration.
17    pub changelog: ChangelogConfig,
18}
19
20/// Git-related release configuration.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(default)]
23pub struct ReleaseGitConfig {
24    /// Default branch for releases.
25    #[serde(rename = "defaultBranch")]
26    pub default_branch: String,
27    /// Tag format template with ${package} and ${version} placeholders.
28    #[serde(rename = "tagFormat")]
29    pub tag_format: String,
30    /// Whether to create tags during release.
31    #[serde(rename = "createTags")]
32    pub create_tags: bool,
33    /// Whether to push tags to remote.
34    #[serde(rename = "pushTags")]
35    pub push_tags: bool,
36}
37
38impl Default for ReleaseGitConfig {
39    fn default() -> Self {
40        Self {
41            default_branch: "main".to_string(),
42            tag_format: "v${version}".to_string(),
43            create_tags: true,
44            push_tags: true,
45        }
46    }
47}
48
49impl ReleaseGitConfig {
50    /// Format a tag name for a package and version.
51    ///
52    /// Replaces `${package}` and `${version}` placeholders in the tag format.
53    #[must_use]
54    pub fn format_tag(&self, package: &str, version: &str) -> String {
55        self.tag_format
56            .replace("${package}", package)
57            .replace("${version}", version)
58    }
59}
60
61/// Package grouping configuration for version management.
62#[derive(Debug, Clone, Default, Serialize, Deserialize)]
63#[serde(default)]
64pub struct ReleasePackagesConfig {
65    /// Fixed groups: packages that share the same version (lockstep versioning).
66    pub fixed: Vec<Vec<String>>,
67    /// Linked groups: packages that are bumped together but can have different versions.
68    pub linked: Vec<Vec<String>>,
69}
70
71impl ReleasePackagesConfig {
72    /// Check if a package is in a fixed group.
73    #[must_use]
74    pub fn is_in_fixed_group(&self, package: &str) -> bool {
75        self.fixed
76            .iter()
77            .any(|group| group.contains(&package.to_string()))
78    }
79
80    /// Get the fixed group containing a package, if any.
81    #[must_use]
82    pub fn get_fixed_group(&self, package: &str) -> Option<&Vec<String>> {
83        self.fixed
84            .iter()
85            .find(|group| group.contains(&package.to_string()))
86    }
87
88    /// Check if a package is in a linked group.
89    #[must_use]
90    pub fn is_in_linked_group(&self, package: &str) -> bool {
91        self.linked
92            .iter()
93            .any(|group| group.contains(&package.to_string()))
94    }
95
96    /// Get the linked group containing a package, if any.
97    #[must_use]
98    pub fn get_linked_group(&self, package: &str) -> Option<&Vec<String>> {
99        self.linked
100            .iter()
101            .find(|group| group.contains(&package.to_string()))
102    }
103}
104
105/// Changelog generation configuration.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(default)]
108pub struct ChangelogConfig {
109    /// Path to the CHANGELOG file relative to project/package root.
110    pub path: String,
111    /// Whether to generate changelogs for each package.
112    #[serde(rename = "perPackage")]
113    pub per_package: bool,
114    /// Whether to generate a root changelog for the entire workspace.
115    pub workspace: bool,
116}
117
118impl Default for ChangelogConfig {
119    fn default() -> Self {
120        Self {
121            path: "CHANGELOG.md".to_string(),
122            per_package: true,
123            workspace: true,
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_release_config_default() {
134        let config = ReleaseConfig::default();
135        assert_eq!(config.git.default_branch, "main");
136        assert_eq!(config.git.tag_format, "v${version}");
137        assert!(config.git.create_tags);
138        assert!(config.git.push_tags);
139    }
140
141    #[test]
142    fn test_git_config_format_tag() {
143        let config = ReleaseGitConfig::default();
144        assert_eq!(config.format_tag("my-pkg", "1.0.0"), "v1.0.0");
145
146        let monorepo_config = ReleaseGitConfig {
147            tag_format: "${package}-v${version}".to_string(),
148            ..Default::default()
149        };
150        assert_eq!(
151            monorepo_config.format_tag("my-pkg", "1.0.0"),
152            "my-pkg-v1.0.0"
153        );
154    }
155
156    #[test]
157    fn test_packages_config_fixed_groups() {
158        let config = ReleasePackagesConfig {
159            fixed: vec![
160                vec!["pkg-a".to_string(), "pkg-b".to_string()],
161                vec!["pkg-c".to_string()],
162            ],
163            linked: vec![],
164        };
165
166        assert!(config.is_in_fixed_group("pkg-a"));
167        assert!(config.is_in_fixed_group("pkg-b"));
168        assert!(config.is_in_fixed_group("pkg-c"));
169        assert!(!config.is_in_fixed_group("pkg-d"));
170
171        let group = config.get_fixed_group("pkg-a").unwrap();
172        assert!(group.contains(&"pkg-a".to_string()));
173        assert!(group.contains(&"pkg-b".to_string()));
174    }
175
176    #[test]
177    fn test_packages_config_linked_groups() {
178        let config = ReleasePackagesConfig {
179            fixed: vec![],
180            linked: vec![vec!["pkg-x".to_string(), "pkg-y".to_string()]],
181        };
182
183        assert!(config.is_in_linked_group("pkg-x"));
184        assert!(config.is_in_linked_group("pkg-y"));
185        assert!(!config.is_in_linked_group("pkg-z"));
186    }
187
188    #[test]
189    fn test_changelog_config_default() {
190        let config = ChangelogConfig::default();
191        assert_eq!(config.path, "CHANGELOG.md");
192        assert!(config.per_package);
193        assert!(config.workspace);
194    }
195
196    #[test]
197    fn test_config_serialization() {
198        let config = ReleaseConfig::default();
199        let json = serde_json::to_string(&config).unwrap();
200        let parsed: ReleaseConfig = serde_json::from_str(&json).unwrap();
201        assert_eq!(parsed.git.default_branch, config.git.default_branch);
202    }
203}