use std::path::Path;
use std::process::ExitCode;
use fallow_config::{FallowConfig, OutputFormat};
use crate::error::emit_error;
const EXIT_NO_CONFIG: u8 = 3;
#[cfg(test)]
pub fn run_config(
root: &Path,
explicit_config: Option<&Path>,
path_only: bool,
output: OutputFormat,
quiet: bool,
) -> ExitCode {
run_config_with_options(RunConfigInput {
root,
explicit_config,
path_only,
output,
quiet,
json_style: crate::json_style::JsonStyle::Compact,
load_options: fallow_config::ConfigLoadOptions::default(),
})
}
#[derive(Clone, Copy)]
pub struct RunConfigInput<'a> {
pub(crate) root: &'a Path,
pub(crate) explicit_config: Option<&'a Path>,
pub(crate) path_only: bool,
pub(crate) output: OutputFormat,
pub(crate) quiet: bool,
pub(crate) json_style: crate::json_style::JsonStyle,
pub(crate) load_options: fallow_config::ConfigLoadOptions,
}
pub fn run_config_with_options(input: RunConfigInput<'_>) -> ExitCode {
let output = input.output;
let result = match input.explicit_config {
Some(path) => FallowConfig::load_with_options(path, input.load_options)
.map(|c| Some((c, path.to_path_buf())))
.map_err(|e| format!("failed to load config '{}': {e}", path.display())),
None => FallowConfig::find_and_load_with_options(input.root, input.load_options),
};
match result {
Ok(Some((config, path))) => {
crate::runtime_support::warn_unknown_security_categories(&config.security);
if let Err(errors) = config.validate_resolved_boundaries(input.root) {
let joined = errors
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n - ");
let msg = format!("invalid boundary configuration:\n - {joined}");
return emit_error(&msg, 2, output);
}
if input.path_only {
println!("{}", path.display());
} else {
if !input.quiet {
eprintln!("loaded config: {}", path.display());
}
match input.json_style.serialize(&config) {
Ok(json) => println!("{json}"),
Err(e) => {
return emit_error(&format!("failed to serialize config: {e}"), 2, output);
}
}
}
ExitCode::SUCCESS
}
Ok(None) => {
if input.path_only {
return ExitCode::from(EXIT_NO_CONFIG);
}
if !input.quiet {
eprintln!("no config file found, using defaults");
}
match input.json_style.serialize(&FallowConfig::default()) {
Ok(json) => {
println!("{json}");
ExitCode::SUCCESS
}
Err(e) => emit_error(&format!("failed to serialize config: {e}"), 2, output),
}
}
Err(e) => emit_error(&e, 2, output),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_config_no_file_prints_defaults_and_succeeds() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join(".git")).unwrap();
let exit = run_config(dir.path(), None, false, OutputFormat::Human, false);
assert_eq!(format!("{exit:?}"), format!("{:?}", ExitCode::SUCCESS));
}
#[test]
fn run_config_with_file_returns_success() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join(".git")).unwrap();
std::fs::write(
dir.path().join(".fallowrc.json"),
r#"{"entry": ["src/index.ts"]}"#,
)
.unwrap();
let exit = run_config(dir.path(), None, false, OutputFormat::Human, false);
assert_eq!(format!("{exit:?}"), format!("{:?}", ExitCode::SUCCESS));
}
#[test]
fn run_config_path_only_with_file_returns_success() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join(".git")).unwrap();
std::fs::write(dir.path().join(".fallowrc.json"), "{}").unwrap();
let exit = run_config(dir.path(), None, true, OutputFormat::Human, false);
assert_eq!(format!("{exit:?}"), format!("{:?}", ExitCode::SUCCESS));
}
#[test]
fn run_config_path_only_no_file_returns_exit_3() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join(".git")).unwrap();
let exit = run_config(dir.path(), None, true, OutputFormat::Human, false);
assert_eq!(
format!("{exit:?}"),
format!("{:?}", ExitCode::from(EXIT_NO_CONFIG))
);
}
#[test]
fn run_config_explicit_config_path_is_used_over_discovery() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join(".git")).unwrap();
let discovered = dir.path().join(".fallowrc.json");
std::fs::write(&discovered, r#"{"entry": ["src/discovered.ts"]}"#).unwrap();
let explicit = dir.path().join("explicit.json");
std::fs::write(&explicit, r#"{"entry": ["src/explicit.ts"]}"#).unwrap();
let exit = run_config(
dir.path(),
Some(&explicit),
true,
OutputFormat::Human,
false,
);
assert_eq!(format!("{exit:?}"), format!("{:?}", ExitCode::SUCCESS));
}
#[test]
fn run_config_explicit_config_missing_returns_error() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("does-not-exist.json");
let exit = run_config(
dir.path(),
Some(&missing),
false,
OutputFormat::Human,
false,
);
assert_eq!(format!("{exit:?}"), format!("{:?}", ExitCode::from(2)));
}
#[test]
fn run_config_rejects_unknown_boundary_zone_reference() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join(".git")).unwrap();
std::fs::write(
dir.path().join(".fallowrc.json"),
r#"{
"boundaries": {
"zones": [{ "name": "ui", "patterns": ["src/ui/**"] }],
"rules": [{ "from": "ui", "allow": ["typo-zone"] }]
}
}"#,
)
.unwrap();
let exit = run_config(dir.path(), None, false, OutputFormat::Human, false);
assert_eq!(format!("{exit:?}"), format!("{:?}", ExitCode::from(2)));
}
}