1use crate::config::release::ChangelogConfig;
4use crate::config::unify::default_true;
5use crate::error::{ConfigError, RailError, RailResult};
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct CrateSplitConfig {
12 pub remote: String,
14 pub branch: String,
16 pub mode: SplitMode,
18 #[serde(default)]
20 pub workspace_mode: WorkspaceMode,
21 #[serde(default)]
23 pub paths: Vec<CratePath>,
24 #[serde(default)]
26 pub include: Vec<String>,
27 #[serde(default)]
29 pub exclude: Vec<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SplitConfig {
35 pub name: String,
37 pub remote: String,
39 pub branch: String,
41 pub mode: SplitMode,
43 #[serde(default)]
45 pub workspace_mode: WorkspaceMode,
46 #[serde(default)]
48 pub paths: Vec<CratePath>,
49 #[serde(default)]
51 pub include: Vec<String>,
52 #[serde(default)]
54 pub exclude: Vec<String>,
55
56 #[serde(default = "default_true")]
58 pub publish: bool,
59
60 #[serde(default)]
62 pub changelog_path: Option<PathBuf>,
63}
64
65impl SplitConfig {
66 pub fn get_paths(&self) -> Vec<&PathBuf> {
68 self.paths.iter().map(|cp| &cp.path).collect()
69 }
70
71 pub fn target_repo_path(&self, workspace_root: &std::path::Path) -> PathBuf {
76 if crate::utils::is_local_path(&self.remote) {
77 PathBuf::from(&self.remote)
78 } else {
79 let remote_name = self
80 .remote
81 .rsplit('/')
82 .next()
83 .unwrap_or(&self.name)
84 .trim_end_matches(".git");
85 workspace_root.join("..").join(remote_name)
86 }
87 }
88
89 pub fn is_local_testing(&self) -> bool {
91 crate::utils::is_local_path(&self.remote)
92 }
93
94 pub fn validate(&self) -> RailResult<()> {
96 if self.paths.is_empty() {
98 return Err(RailError::with_help(
99 format!("Split '{}' must have at least one crate path", self.name),
100 format!("Add paths in rail.toml under [crates.{}.split]", self.name),
101 ));
102 }
103
104 if self.remote.is_empty() {
106 return Err(RailError::Config(ConfigError::MissingField {
107 field: format!("remote for split '{}'", self.name),
108 }));
109 }
110
111 match self.mode {
113 SplitMode::Single => {
114 if self.paths.len() != 1 {
115 return Err(RailError::with_help(
116 format!(
117 "Single mode split '{}' must have exactly one path (found {})",
118 self.name,
119 self.paths.len()
120 ),
121 "Change mode to 'combined' or remove extra paths",
122 ));
123 }
124 }
125 SplitMode::Combined => {
126 if self.paths.len() < 2 {
127 return Err(RailError::with_help(
128 format!(
129 "Combined mode split '{}' should have multiple paths (found {})",
130 self.name,
131 self.paths.len()
132 ),
133 "Change mode to 'single' or add more crate paths",
134 ));
135 }
136 }
137 }
138 Ok(())
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct CratePath {
145 #[serde(rename = "crate")]
147 pub path: PathBuf,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
152#[serde(rename_all = "lowercase")]
153pub enum SplitMode {
154 #[default]
156 Single,
157 Combined,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
163#[serde(rename_all = "lowercase")]
164pub enum WorkspaceMode {
165 #[default]
167 Standalone,
168 Workspace,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, Default)]
177pub struct CrateSyncConfig {}
178
179pub fn build_split_config(
181 name: String,
182 split: &CrateSplitConfig,
183 release_publish: Option<bool>,
184 changelog: Option<&ChangelogConfig>,
185) -> SplitConfig {
186 SplitConfig {
187 name,
188 remote: split.remote.clone(),
189 branch: split.branch.clone(),
190 mode: split.mode.clone(),
191 workspace_mode: split.workspace_mode.clone(),
192 paths: split.paths.clone(),
193 include: split.include.clone(),
194 exclude: split.exclude.clone(),
195 publish: release_publish.unwrap_or(true),
196 changelog_path: changelog.and_then(|c| c.path.clone()),
197 }
198}
199
200#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_split_config_validate_empty_paths() {
208 let config = SplitConfig {
209 name: "test-crate".to_string(),
210 remote: "git@github.com:user/test.git".to_string(),
211 branch: "main".to_string(),
212 mode: SplitMode::Single,
213 workspace_mode: WorkspaceMode::default(),
214 paths: vec![],
215 include: vec![],
216 exclude: vec![],
217 publish: true,
218 changelog_path: None,
219 };
220
221 let result = config.validate();
222 assert!(result.is_err());
223 let err_msg = result.unwrap_err().to_string();
224 assert!(err_msg.contains("at least one crate path"));
225 }
226
227 #[test]
228 fn test_split_config_validate_empty_remote() {
229 let config = SplitConfig {
230 name: "test-crate".to_string(),
231 remote: "".to_string(),
232 branch: "main".to_string(),
233 mode: SplitMode::Single,
234 workspace_mode: WorkspaceMode::default(),
235 paths: vec![CratePath {
236 path: PathBuf::from("crates/test"),
237 }],
238 include: vec![],
239 exclude: vec![],
240 publish: true,
241 changelog_path: None,
242 };
243
244 let result = config.validate();
245 assert!(result.is_err());
246 let err_msg = result.unwrap_err().to_string();
247 assert!(err_msg.contains("remote"));
248 }
249
250 #[test]
251 fn test_split_config_validate_single_mode_multiple_paths() {
252 let config = SplitConfig {
253 name: "test-crate".to_string(),
254 remote: "git@github.com:user/test.git".to_string(),
255 branch: "main".to_string(),
256 mode: SplitMode::Single,
257 workspace_mode: WorkspaceMode::default(),
258 paths: vec![
259 CratePath {
260 path: PathBuf::from("crates/a"),
261 },
262 CratePath {
263 path: PathBuf::from("crates/b"),
264 },
265 ],
266 include: vec![],
267 exclude: vec![],
268 publish: true,
269 changelog_path: None,
270 };
271
272 let result = config.validate();
273 assert!(result.is_err());
274 let err_msg = result.unwrap_err().to_string();
275 assert!(err_msg.contains("Single mode"));
276 assert!(err_msg.contains("exactly one path"));
277 }
278
279 #[test]
280 fn test_split_config_validate_combined_mode_single_path() {
281 let config = SplitConfig {
282 name: "test-crate".to_string(),
283 remote: "git@github.com:user/test.git".to_string(),
284 branch: "main".to_string(),
285 mode: SplitMode::Combined,
286 workspace_mode: WorkspaceMode::default(),
287 paths: vec![CratePath {
288 path: PathBuf::from("crates/a"),
289 }],
290 include: vec![],
291 exclude: vec![],
292 publish: true,
293 changelog_path: None,
294 };
295
296 let result = config.validate();
297 assert!(result.is_err());
298 let err_msg = result.unwrap_err().to_string();
299 assert!(err_msg.contains("Combined mode"));
300 assert!(err_msg.contains("multiple paths"));
301 }
302
303 #[test]
304 fn test_split_config_validate_valid() {
305 let config = SplitConfig {
306 name: "test-crate".to_string(),
307 remote: "git@github.com:user/test.git".to_string(),
308 branch: "main".to_string(),
309 mode: SplitMode::Single,
310 workspace_mode: WorkspaceMode::default(),
311 paths: vec![CratePath {
312 path: PathBuf::from("crates/test"),
313 }],
314 include: vec![],
315 exclude: vec![],
316 publish: true,
317 changelog_path: None,
318 };
319
320 assert!(config.validate().is_ok());
321 }
322}