use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[derive(Default)]
pub struct Config {
pub splitrs: SplitRsConfig,
pub naming: NamingConfig,
pub output: OutputConfig,
#[serde(default, rename = "target_modules")]
pub target_modules: Vec<TargetModule>,
}
impl Config {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let contents =
fs::read_to_string(path.as_ref()).context("Failed to read configuration file")?;
let config: Config =
toml::from_str(&contents).context("Failed to parse TOML configuration")?;
Ok(config)
}
pub fn load_from_current_dir() -> Self {
Self::find_and_load(".").unwrap_or_default()
}
pub fn find_and_load<P: AsRef<Path>>(start_dir: P) -> Option<Self> {
let mut current_dir = start_dir.as_ref().to_path_buf();
loop {
let config_path = current_dir.join(".splitrs.toml");
if config_path.exists() {
return Self::from_file(&config_path).ok();
}
if !current_dir.pop() {
break;
}
}
None
}
#[allow(dead_code)]
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let toml_string =
toml::to_string_pretty(self).context("Failed to serialize configuration to TOML")?;
fs::write(path.as_ref(), toml_string).context("Failed to write configuration file")?;
Ok(())
}
pub fn merge_with_args(
&mut self,
max_lines: Option<usize>,
max_impl_lines: Option<usize>,
split_impl_blocks: Option<bool>,
) {
if let Some(max_lines) = max_lines {
self.splitrs.max_lines = max_lines;
}
if let Some(max_impl_lines) = max_impl_lines {
self.splitrs.max_impl_lines = max_impl_lines;
}
if let Some(split_impl_blocks) = split_impl_blocks {
self.splitrs.split_impl_blocks = split_impl_blocks;
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SplitRsConfig {
pub max_lines: usize,
pub max_impl_lines: usize,
pub split_impl_blocks: bool,
pub incremental: bool,
pub generate_tests: bool,
pub extract_tests: bool,
}
impl Default for SplitRsConfig {
fn default() -> Self {
Self {
max_lines: 1000,
max_impl_lines: 500,
split_impl_blocks: false,
incremental: false,
generate_tests: false,
extract_tests: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TargetModule {
pub name: String,
#[serde(default)]
pub items: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TargetModulesFile {
#[serde(default)]
pub target_modules: Vec<TargetModule>,
}
impl TargetModulesFile {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let contents = fs::read_to_string(path.as_ref())
.context("Failed to read target-modules configuration file")?;
let spec: TargetModulesFile = toml::from_str(&contents)
.context("Failed to parse target-modules TOML configuration")?;
Ok(spec)
}
}
pub fn matches_pattern(item_name: &str, pattern: &str) -> bool {
if pattern == "*" {
return true;
}
if let Some(prefix) = pattern.strip_suffix('*') {
if !prefix.contains('*') {
return item_name.starts_with(prefix);
}
}
if let Some(suffix) = pattern.strip_prefix('*') {
if !suffix.contains('*') {
return item_name.ends_with(suffix);
}
}
item_name == pattern
}
pub fn route_item<'a>(item_name: &str, target_modules: &'a [TargetModule]) -> Option<&'a str> {
for tm in target_modules {
for pattern in &tm.items {
if matches_pattern(item_name, pattern) {
return Some(&tm.name);
}
}
}
None
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct NamingConfig {
pub strategy: String,
pub type_module_suffix: String,
pub impl_module_suffix: String,
pub trait_module_suffix: String,
pub use_snake_case: bool,
#[serde(default)]
pub custom_type_mappings: std::collections::HashMap<String, String>,
#[serde(default)]
pub custom_pattern_mappings: std::collections::HashMap<String, String>,
}
impl Default for NamingConfig {
fn default() -> Self {
Self {
strategy: "snake_case".to_string(),
type_module_suffix: "_type".to_string(),
impl_module_suffix: "_impl".to_string(),
trait_module_suffix: "_traits".to_string(),
use_snake_case: true,
custom_type_mappings: std::collections::HashMap::new(),
custom_pattern_mappings: std::collections::HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct OutputConfig {
pub module_doc_template: String,
pub preserve_comments: bool,
pub format_output: bool,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
module_doc_template: "//! Auto-generated module\n".to_string(),
preserve_comments: true,
format_output: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.splitrs.max_lines, 1000);
assert_eq!(config.splitrs.max_impl_lines, 500);
assert!(!config.splitrs.split_impl_blocks);
}
#[test]
fn test_config_serialization() {
let config = Config::default();
let toml_string = toml::to_string(&config).unwrap();
assert!(toml_string.contains("max_lines"));
assert!(toml_string.contains("max_impl_lines"));
}
#[test]
fn test_config_deserialization() {
let toml_str = r#"
[splitrs]
max_lines = 800
max_impl_lines = 400
split_impl_blocks = true
[naming]
type_module_suffix = "_types"
impl_module_suffix = "_methods"
[output]
preserve_comments = false
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.splitrs.max_lines, 800);
assert_eq!(config.splitrs.max_impl_lines, 400);
assert!(config.splitrs.split_impl_blocks);
assert_eq!(config.naming.type_module_suffix, "_types");
assert!(!config.output.preserve_comments);
}
#[test]
fn test_config_merge_with_args() {
let mut config = Config::default();
config.merge_with_args(Some(1500), Some(600), Some(true));
assert_eq!(config.splitrs.max_lines, 1500);
assert_eq!(config.splitrs.max_impl_lines, 600);
assert!(config.splitrs.split_impl_blocks);
}
#[test]
fn test_config_save_and_load() {
let temp_dir = env::temp_dir();
let config_path = temp_dir.join("test_splitrs.toml");
let config = Config::default();
config.save_to_file(&config_path).unwrap();
let loaded_config = Config::from_file(&config_path).unwrap();
assert_eq!(loaded_config.splitrs.max_lines, config.splitrs.max_lines);
let _ = fs::remove_file(config_path);
}
#[test]
fn test_extract_tests_default_is_false() {
let config = Config::default();
assert!(!config.splitrs.extract_tests);
}
#[test]
fn test_extract_tests_deserialization() {
let toml_str = r#"
[splitrs]
extract_tests = true
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert!(config.splitrs.extract_tests);
}
#[test]
fn test_target_modules_embedded_in_main_config() {
let toml_str = r#"
[splitrs]
max_lines = 1000
[[target_modules]]
name = "v3"
items = ["FooV3", "BarV3"]
[[target_modules]]
name = "core"
items = ["*"]
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.target_modules.len(), 2);
assert_eq!(config.target_modules[0].name, "v3");
assert_eq!(config.target_modules[0].items, vec!["FooV3", "BarV3"]);
assert_eq!(config.target_modules[1].name, "core");
assert_eq!(config.target_modules[1].items, vec!["*"]);
}
#[test]
fn test_target_modules_standalone_file() {
let toml_str = r#"
[[target_modules]]
name = "v3"
items = ["BoundaryExtV3", "StreamingBoundaryEstimator"]
[[target_modules]]
name = "extended"
items = ["BoundaryExt*"]
"#;
let spec: TargetModulesFile = toml::from_str(toml_str).unwrap();
assert_eq!(spec.target_modules.len(), 2);
assert_eq!(spec.target_modules[0].name, "v3");
assert_eq!(spec.target_modules[1].items, vec!["BoundaryExt*"]);
}
#[test]
fn test_matches_pattern_exact() {
assert!(matches_pattern("Foo", "Foo"));
assert!(!matches_pattern("Foo", "Bar"));
assert!(!matches_pattern("FooBar", "Foo"));
}
#[test]
fn test_matches_pattern_prefix() {
assert!(matches_pattern("FooBar", "Foo*"));
assert!(matches_pattern("Foo", "Foo*"));
assert!(matches_pattern("FooBarBaz", "Foo*"));
assert!(!matches_pattern("Bar", "Foo*"));
}
#[test]
fn test_matches_pattern_suffix() {
assert!(matches_pattern("MyConfig", "*Config"));
assert!(matches_pattern("Config", "*Config"));
assert!(!matches_pattern("ConfigPath", "*Config"));
}
#[test]
fn test_matches_pattern_wildcard() {
assert!(matches_pattern("", "*"));
assert!(matches_pattern("AnyThing", "*"));
assert!(matches_pattern("foo_bar", "*"));
}
#[test]
fn test_route_item_first_match_wins() {
let rules = vec![
TargetModule {
name: "v3".to_string(),
items: vec!["FooV3".to_string()],
},
TargetModule {
name: "extended".to_string(),
items: vec!["Foo*".to_string()],
},
TargetModule {
name: "core".to_string(),
items: vec!["*".to_string()],
},
];
assert_eq!(route_item("FooV3", &rules), Some("v3"));
assert_eq!(route_item("FooBar", &rules), Some("extended"));
assert_eq!(route_item("Quux", &rules), Some("core"));
}
#[test]
fn test_route_item_no_match_returns_none() {
let rules = vec![TargetModule {
name: "v3".to_string(),
items: vec!["FooV3".to_string()],
}];
assert_eq!(route_item("Bar", &rules), None);
}
}