use crate::config::release::ChangelogConfig;
use crate::config::unify::default_true;
use crate::error::{ConfigError, RailError, RailResult};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateSplitConfig {
pub remote: String,
pub branch: String,
pub mode: SplitMode,
#[serde(default)]
pub workspace_mode: WorkspaceMode,
#[serde(default)]
pub paths: Vec<CratePath>,
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SplitConfig {
pub name: String,
pub remote: String,
pub branch: String,
pub mode: SplitMode,
#[serde(default)]
pub workspace_mode: WorkspaceMode,
#[serde(default)]
pub paths: Vec<CratePath>,
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default = "default_true")]
pub publish: bool,
#[serde(default)]
pub changelog_path: Option<PathBuf>,
}
impl SplitConfig {
pub fn get_paths(&self) -> Vec<&PathBuf> {
self.paths.iter().map(|cp| &cp.path).collect()
}
pub fn target_repo_path(&self, workspace_root: &std::path::Path) -> PathBuf {
if crate::utils::is_local_path(&self.remote) {
PathBuf::from(&self.remote)
} else {
let remote_name = self
.remote
.rsplit('/')
.next()
.unwrap_or(&self.name)
.trim_end_matches(".git");
workspace_root.join("..").join(remote_name)
}
}
pub fn is_local_testing(&self) -> bool {
crate::utils::is_local_path(&self.remote)
}
pub fn validate(&self) -> RailResult<()> {
if self.paths.is_empty() {
return Err(RailError::with_help(
format!("Split '{}' must have at least one crate path", self.name),
format!("Add paths in rail.toml under [crates.{}.split]", self.name),
));
}
if self.remote.is_empty() {
return Err(RailError::Config(ConfigError::MissingField {
field: format!("remote for split '{}'", self.name),
}));
}
match self.mode {
SplitMode::Single => {
if self.paths.len() != 1 {
return Err(RailError::with_help(
format!(
"Single mode split '{}' must have exactly one path (found {})",
self.name,
self.paths.len()
),
"Change mode to 'combined' or remove extra paths",
));
}
}
SplitMode::Combined => {
if self.paths.len() < 2 {
return Err(RailError::with_help(
format!(
"Combined mode split '{}' should have multiple paths (found {})",
self.name,
self.paths.len()
),
"Change mode to 'single' or add more crate paths",
));
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CratePath {
#[serde(rename = "crate")]
pub path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SplitMode {
#[default]
Single,
Combined,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum WorkspaceMode {
#[default]
Standalone,
Workspace,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CrateSyncConfig {}
pub fn build_split_config(
name: String,
split: &CrateSplitConfig,
release_publish: Option<bool>,
changelog: Option<&ChangelogConfig>,
) -> SplitConfig {
SplitConfig {
name,
remote: split.remote.clone(),
branch: split.branch.clone(),
mode: split.mode.clone(),
workspace_mode: split.workspace_mode.clone(),
paths: split.paths.clone(),
include: split.include.clone(),
exclude: split.exclude.clone(),
publish: release_publish.unwrap_or(true),
changelog_path: changelog.and_then(|c| c.path.clone()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_config_validate_empty_paths() {
let config = SplitConfig {
name: "test-crate".to_string(),
remote: "git@github.com:user/test.git".to_string(),
branch: "main".to_string(),
mode: SplitMode::Single,
workspace_mode: WorkspaceMode::default(),
paths: vec![],
include: vec![],
exclude: vec![],
publish: true,
changelog_path: None,
};
let result = config.validate();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("at least one crate path"));
}
#[test]
fn test_split_config_validate_empty_remote() {
let config = SplitConfig {
name: "test-crate".to_string(),
remote: "".to_string(),
branch: "main".to_string(),
mode: SplitMode::Single,
workspace_mode: WorkspaceMode::default(),
paths: vec![CratePath {
path: PathBuf::from("crates/test"),
}],
include: vec![],
exclude: vec![],
publish: true,
changelog_path: None,
};
let result = config.validate();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("remote"));
}
#[test]
fn test_split_config_validate_single_mode_multiple_paths() {
let config = SplitConfig {
name: "test-crate".to_string(),
remote: "git@github.com:user/test.git".to_string(),
branch: "main".to_string(),
mode: SplitMode::Single,
workspace_mode: WorkspaceMode::default(),
paths: vec![
CratePath {
path: PathBuf::from("crates/a"),
},
CratePath {
path: PathBuf::from("crates/b"),
},
],
include: vec![],
exclude: vec![],
publish: true,
changelog_path: None,
};
let result = config.validate();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Single mode"));
assert!(err_msg.contains("exactly one path"));
}
#[test]
fn test_split_config_validate_combined_mode_single_path() {
let config = SplitConfig {
name: "test-crate".to_string(),
remote: "git@github.com:user/test.git".to_string(),
branch: "main".to_string(),
mode: SplitMode::Combined,
workspace_mode: WorkspaceMode::default(),
paths: vec![CratePath {
path: PathBuf::from("crates/a"),
}],
include: vec![],
exclude: vec![],
publish: true,
changelog_path: None,
};
let result = config.validate();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Combined mode"));
assert!(err_msg.contains("multiple paths"));
}
#[test]
fn test_split_config_validate_valid() {
let config = SplitConfig {
name: "test-crate".to_string(),
remote: "git@github.com:user/test.git".to_string(),
branch: "main".to_string(),
mode: SplitMode::Single,
workspace_mode: WorkspaceMode::default(),
paths: vec![CratePath {
path: PathBuf::from("crates/test"),
}],
include: vec![],
exclude: vec![],
publish: true,
changelog_path: None,
};
assert!(config.validate().is_ok());
}
}