Skip to content

Commit 6c28d4e

Browse files
committed
Add kdl support for all toml related files.
1 parent d503ad3 commit 6c28d4e

File tree

9 files changed

+961
-67
lines changed

9 files changed

+961
-67
lines changed

Cargo.lock

Lines changed: 280 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rb-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ env_logger = "0.11"
2828
semver = "1.0.26"
2929
which = "6.0"
3030
toml = "0.8"
31+
kdl = "6.0.0"
3132
serde = { version = "1.0", features = ["derive"] }
3233

3334
[dev-dependencies]

crates/rb-cli/src/commands/run.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,13 @@ fn list_available_scripts(butler_runtime: ButlerRuntime, project_file: Option<Pa
5757
eprintln!();
5858
eprintln!("No project configuration detected in the current directory hierarchy.");
5959
eprintln!();
60+
eprintln!("To define project scripts, create one of these files (in priority order):");
6061
eprintln!(
61-
"To define project scripts, create an {} or {} file:",
62-
"rbproject.toml".cyan(),
63-
"gem.toml".cyan()
62+
" {} {} {} {}",
63+
"gem.kdl".cyan(),
64+
"gem.toml".cyan(),
65+
"rbproject.kdl".cyan(),
66+
"rbproject.toml".cyan()
6467
);
6568
eprintln!();
6669
eprintln!(" {}", "[scripts]".bright_black());
@@ -73,7 +76,7 @@ fn list_available_scripts(butler_runtime: ButlerRuntime, project_file: Option<Pa
7376
);
7477
eprintln!();
7578
eprintln!(
76-
"Or specify a custom location: {} -P path/to/rbproject.toml run",
79+
"Or specify a custom location: {} -P path/to/gem.kdl run",
7780
"rb".green().bold()
7881
);
7982
std::process::exit(1);
@@ -238,9 +241,15 @@ pub fn run_command(
238241
eprintln!("No project configuration detected in the current directory hierarchy.");
239242
eprintln!();
240243
eprintln!(
241-
"To use project scripts, please create an {} or {} file with script definitions:",
244+
"To use project scripts, please create one of these files with script definitions:"
245+
);
246+
eprintln!(
247+
" {} {} {} {} {}",
242248
"rbproject.toml".cyan(),
243-
"gem.toml".cyan()
249+
"rb.toml".cyan(),
250+
"rb.kdl".cyan(),
251+
"gem.toml".cyan(),
252+
"gem.kdl".cyan()
244253
);
245254
eprintln!();
246255
eprintln!(" {}", "[scripts]".bright_black());
@@ -253,7 +262,7 @@ pub fn run_command(
253262
);
254263
eprintln!();
255264
eprintln!(
256-
"Or specify a custom location with: {} -P path/to/rbproject.toml run {}",
265+
"Or specify a custom location with: {} -P path/to/rb.toml run {}",
257266
"rb".green().bold(),
258267
script_name.cyan()
259268
);

crates/rb-cli/src/config/loader.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,24 @@ use std::path::PathBuf;
77
/// Load configuration from file
88
/// Returns default config if no file is found
99
///
10+
/// Supports both TOML and KDL formats (detected by file extension)
11+
///
1012
/// # Arguments
1113
/// * `override_path` - Optional path to explicitly load config from (for testing)
1214
pub fn load_config(override_path: Option<PathBuf>) -> Result<RbConfig, ConfigError> {
1315
if let Some(config_path) = locate_config_file(override_path.clone()) {
1416
info!("Loading configuration from: {}", config_path.display());
1517

1618
let contents = fs::read_to_string(&config_path)?;
17-
let config: RbConfig = toml::from_str(&contents)?;
19+
20+
// Determine format based on file extension
21+
let config: RbConfig = if config_path.extension().and_then(|s| s.to_str()) == Some("kdl") {
22+
debug!("Parsing configuration as KDL format");
23+
parse_kdl_config(&contents)?
24+
} else {
25+
debug!("Parsing configuration as TOML format");
26+
toml::from_str(&contents)?
27+
};
1828

1929
// Log what was loaded
2030
debug!("Configuration file contents parsed successfully");
@@ -39,6 +49,44 @@ pub fn load_config(override_path: Option<PathBuf>) -> Result<RbConfig, ConfigErr
3949
}
4050
}
4151

52+
/// Parse KDL configuration into RbConfig
53+
fn parse_kdl_config(content: &str) -> Result<RbConfig, ConfigError> {
54+
let doc: kdl::KdlDocument = content.parse().map_err(|e: kdl::KdlError| {
55+
std::io::Error::new(
56+
std::io::ErrorKind::InvalidData,
57+
format!("Failed to parse KDL: {}", e),
58+
)
59+
})?;
60+
61+
let mut config = RbConfig::default();
62+
63+
// Parse rubies-dir
64+
if let Some(node) = doc.get("rubies-dir")
65+
&& let Some(entry) = node.entries().first()
66+
&& let Some(value) = entry.value().as_string()
67+
{
68+
config.rubies_dir = Some(PathBuf::from(value));
69+
}
70+
71+
// Parse ruby-version
72+
if let Some(node) = doc.get("ruby-version")
73+
&& let Some(entry) = node.entries().first()
74+
&& let Some(value) = entry.value().as_string()
75+
{
76+
config.ruby_version = Some(value.to_string());
77+
}
78+
79+
// Parse gem-home
80+
if let Some(node) = doc.get("gem-home")
81+
&& let Some(entry) = node.entries().first()
82+
&& let Some(value) = entry.value().as_string()
83+
{
84+
config.gem_home = Some(PathBuf::from(value));
85+
}
86+
87+
Ok(config)
88+
}
89+
4290
#[cfg(test)]
4391
mod tests {
4492
use super::*;
@@ -78,4 +126,32 @@ mod tests {
78126
// Cleanup
79127
let _ = fs::remove_file(&config_path);
80128
}
129+
130+
#[test]
131+
fn test_load_kdl_config() {
132+
use std::fs;
133+
134+
let temp_dir = std::env::temp_dir();
135+
let config_path = temp_dir.join("test_rb_config.kdl");
136+
137+
// Create a test KDL config file
138+
let kdl_content = r#"
139+
rubies-dir "/opt/rubies"
140+
ruby-version "3.3.0"
141+
gem-home "/opt/gems"
142+
"#;
143+
fs::write(&config_path, kdl_content).expect("Failed to write KDL config");
144+
145+
// Load config from KDL path
146+
let result = load_config(Some(config_path.clone()));
147+
assert!(result.is_ok());
148+
149+
let config = result.unwrap();
150+
assert_eq!(config.rubies_dir, Some(PathBuf::from("/opt/rubies")));
151+
assert_eq!(config.ruby_version, Some("3.3.0".to_string()));
152+
assert_eq!(config.gem_home, Some(PathBuf::from("/opt/gems")));
153+
154+
// Cleanup
155+
let _ = fs::remove_file(&config_path);
156+
}
81157
}

crates/rb-cli/src/config/locator.rs

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ use std::path::PathBuf;
33

44
/// Locate the configuration file following XDG Base Directory specification
55
///
6+
/// Supports both rb.kdl and rb.toml (preferring .kdl)
7+
///
68
/// Priority order:
79
/// 1. Explicit override path (if provided)
810
/// 2. $RB_CONFIG environment variable
9-
/// 3. $XDG_CONFIG_HOME/rb/rb.toml (Unix/Linux)
10-
/// 4. ~/.config/rb/rb.toml (Unix/Linux fallback)
11-
/// 5. %APPDATA%/rb/rb.toml (Windows)
12-
/// 6. ~/.rb.toml (cross-platform fallback)
11+
/// 3. $XDG_CONFIG_HOME/rb/rb.kdl or rb.toml (Unix/Linux)
12+
/// 4. ~/.config/rb/rb.kdl or rb.toml (Unix/Linux fallback)
13+
/// 5. %APPDATA%/rb/rb.kdl or rb.toml (Windows)
14+
/// 6. ~/.rb.kdl or ~/.rb.toml (cross-platform fallback)
1315
pub fn locate_config_file(override_path: Option<PathBuf>) -> Option<PathBuf> {
1416
debug!("Searching for configuration file...");
1517

@@ -34,49 +36,58 @@ pub fn locate_config_file(override_path: Option<PathBuf>) -> Option<PathBuf> {
3436

3537
// 3. Try XDG_CONFIG_HOME (Unix/Linux)
3638
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
37-
let config_path = PathBuf::from(xdg_config).join("rb").join("rb.toml");
38-
debug!(" Checking XDG_CONFIG_HOME: {}", config_path.display());
39-
if config_path.exists() {
40-
debug!(" Found configuration file in XDG_CONFIG_HOME");
41-
return Some(config_path);
39+
let base_path = PathBuf::from(xdg_config).join("rb");
40+
// Try .kdl first, then .toml
41+
for ext in &["rb.kdl", "rb.toml"] {
42+
let config_path = base_path.join(ext);
43+
debug!(" Checking XDG_CONFIG_HOME: {}", config_path.display());
44+
if config_path.exists() {
45+
debug!(" Found configuration file in XDG_CONFIG_HOME");
46+
return Some(config_path);
47+
}
4248
}
4349
}
4450

4551
// Try home directory based paths
4652
if let Some(home_dir) = home::home_dir() {
47-
// Unix/Linux: ~/.config/rb/rb.toml
53+
// Unix/Linux: ~/.config/rb/rb.kdl or rb.toml
4854
#[cfg(not(target_os = "windows"))]
4955
{
50-
let config_path = home_dir.join(".config").join("rb").join("rb.toml");
51-
debug!(" Checking ~/.config/rb/rb.toml: {}", config_path.display());
52-
if config_path.exists() {
53-
debug!(" Found configuration file in ~/.config/rb/");
54-
return Some(config_path);
56+
let base_path = home_dir.join(".config").join("rb");
57+
for ext in &["rb.kdl", "rb.toml"] {
58+
let config_path = base_path.join(ext);
59+
debug!(" Checking ~/.config/rb/{}: {}", ext, config_path.display());
60+
if config_path.exists() {
61+
debug!(" Found configuration file in ~/.config/rb/");
62+
return Some(config_path);
63+
}
5564
}
5665
}
5766

58-
// Windows: %APPDATA%/rb/rb.toml
67+
// Windows: %APPDATA%/rb/rb.kdl or rb.toml
5968
#[cfg(target_os = "windows")]
6069
{
6170
if let Ok(appdata) = std::env::var("APPDATA") {
62-
let config_path = PathBuf::from(appdata).join("rb").join("rb.toml");
63-
debug!(" Checking %APPDATA%/rb/rb.toml: {}", config_path.display());
64-
if config_path.exists() {
65-
debug!(" Found configuration file in %APPDATA%/rb/");
66-
return Some(config_path);
71+
let base_path = PathBuf::from(appdata).join("rb");
72+
for ext in &["rb.kdl", "rb.toml"] {
73+
let config_path = base_path.join(ext);
74+
debug!(" Checking %APPDATA%/rb/{}: {}", ext, config_path.display());
75+
if config_path.exists() {
76+
debug!(" Found configuration file in %APPDATA%/rb/");
77+
return Some(config_path);
78+
}
6779
}
6880
}
6981
}
7082

71-
// Cross-platform fallback: ~/.rb.toml
72-
let fallback_path = home_dir.join(".rb.toml");
73-
debug!(
74-
" Checking fallback ~/.rb.toml: {}",
75-
fallback_path.display()
76-
);
77-
if fallback_path.exists() {
78-
debug!(" Found configuration file at ~/.rb.toml");
79-
return Some(fallback_path);
83+
// Cross-platform fallback: ~/.rb.kdl or ~/.rb.toml
84+
for ext in &[".rb.kdl", ".rb.toml"] {
85+
let fallback_path = home_dir.join(ext);
86+
debug!(" Checking fallback ~/{}: {}", ext, fallback_path.display());
87+
if fallback_path.exists() {
88+
debug!(" Found configuration file at ~/{}", ext);
89+
return Some(fallback_path);
90+
}
8091
}
8192
}
8293

crates/rb-core/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ log = "0.4"
1818
colored = "2.1.0"
1919
which = "6.0"
2020
toml = "0.8"
21+
kdl = "6.0"
2122
serde = { version = "1.0", features = ["derive"] }
23+
miette = { version = "7.0", features = ["fancy"] }
2224

2325
[dev-dependencies]
2426
tempfile = "3.21.0"

crates/rb-core/src/project/detector.rs

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ pub struct RbprojectDetector;
77

88
impl RbprojectDetector {
99
/// Supported project file names in order of preference
10-
const PROJECT_FILENAMES: &'static [&'static str] = &["rbproject.toml", "gem.toml"];
10+
/// Priority: gem.kdl > gem.toml > rbproject.kdl > rbproject.toml
11+
const PROJECT_FILENAMES: &'static [&'static str] =
12+
&["gem.kdl", "gem.toml", "rbproject.kdl", "rbproject.toml"];
1113

12-
/// Discover a ProjectRuntime by searching for project config files (rbproject.toml or gem.toml)
14+
/// Discover a ProjectRuntime by searching for project config files
1315
/// in the current directory and walking up the directory tree until one is found or we reach the root.
1416
pub fn discover(start_dir: &Path) -> std::io::Result<Option<ProjectRuntime>> {
1517
debug!(
@@ -286,7 +288,7 @@ test = "rspec"
286288
}
287289

288290
#[test]
289-
fn discover_prefers_rbproject_toml_over_gem_toml() -> io::Result<()> {
291+
fn discover_prefers_gem_toml_over_rbproject_toml() -> io::Result<()> {
290292
let temp_dir = TempDir::new()?;
291293
let project_dir = temp_dir.path();
292294

@@ -309,10 +311,58 @@ test = "rspec from gem.toml"
309311
assert!(result.is_some());
310312
let project_runtime = result.unwrap();
311313
assert_eq!(project_runtime.root, project_dir);
312-
assert_eq!(project_runtime.config_filename, "rbproject.toml");
314+
assert_eq!(project_runtime.config_filename, "gem.toml");
315+
assert_eq!(
316+
project_runtime.get_script_command("test"),
317+
Some("rspec from gem.toml")
318+
);
319+
320+
Ok(())
321+
}
322+
323+
#[test]
324+
fn discover_respects_priority_order() -> io::Result<()> {
325+
let temp_dir = TempDir::new()?;
326+
let project_dir = temp_dir.path();
327+
328+
// Create all supported project files
329+
create_rbproject_toml(
330+
project_dir,
331+
r#"[scripts]
332+
test = "from rbproject.toml"
333+
"#,
334+
)?;
335+
336+
fs::write(
337+
project_dir.join("rbproject.kdl"),
338+
r#"scripts {
339+
test "from rbproject.kdl"
340+
}"#,
341+
)?;
342+
343+
fs::write(
344+
project_dir.join("gem.toml"),
345+
r#"[scripts]
346+
test = "from gem.toml"
347+
"#,
348+
)?;
349+
350+
fs::write(
351+
project_dir.join("gem.kdl"),
352+
r#"scripts {
353+
test "from gem.kdl"
354+
}"#,
355+
)?;
356+
357+
let result = RbprojectDetector::discover(project_dir)?;
358+
359+
assert!(result.is_some());
360+
let project_runtime = result.unwrap();
361+
// Should pick gem.kdl (highest priority)
362+
assert_eq!(project_runtime.config_filename, "gem.kdl");
313363
assert_eq!(
314364
project_runtime.get_script_command("test"),
315-
Some("rspec from rbproject.toml")
365+
Some("from gem.kdl")
316366
);
317367

318368
Ok(())

0 commit comments

Comments
 (0)