use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExcludeConfig {
#[serde(default)]
pub types: Vec<String>,
#[serde(default)]
pub functions: Vec<String>,
#[serde(default)]
pub methods: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct IncludeConfig {
#[serde(default)]
pub types: Vec<String>,
#[serde(default)]
pub functions: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OutputConfig {
pub python: Option<PathBuf>,
pub node: Option<PathBuf>,
pub ruby: Option<PathBuf>,
pub php: Option<PathBuf>,
pub elixir: Option<PathBuf>,
pub wasm: Option<PathBuf>,
pub ffi: Option<PathBuf>,
pub go: Option<PathBuf>,
pub java: Option<PathBuf>,
pub csharp: Option<PathBuf>,
pub r: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScaffoldConfig {
pub description: Option<String>,
pub license: Option<String>,
pub repository: Option<String>,
pub homepage: Option<String>,
#[serde(default)]
pub authors: Vec<String>,
#[serde(default)]
pub keywords: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadmeConfig {
pub template_dir: Option<PathBuf>,
pub snippets_dir: Option<PathBuf>,
pub config: Option<PathBuf>,
pub output_pattern: Option<String>,
pub discord_url: Option<String>,
pub banner_url: Option<String>,
#[serde(default)]
pub languages: HashMap<String, JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StringOrVec {
Single(String),
Multiple(Vec<String>),
}
impl StringOrVec {
pub fn commands(&self) -> Vec<&str> {
match self {
StringOrVec::Single(s) => vec![s.as_str()],
StringOrVec::Multiple(v) => v.iter().map(String::as_str).collect(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LintConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub format: Option<StringOrVec>,
pub check: Option<StringOrVec>,
pub typecheck: Option<StringOrVec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub update: Option<StringOrVec>,
pub upgrade: Option<StringOrVec>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TestConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub command: Option<StringOrVec>,
pub e2e: Option<StringOrVec>,
pub coverage: Option<StringOrVec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetupConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub install: Option<StringOrVec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CleanConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub clean: Option<StringOrVec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildCommandConfig {
pub precondition: Option<String>,
pub before: Option<StringOrVec>,
pub build: Option<StringOrVec>,
pub build_release: Option<StringOrVec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextReplacement {
pub path: String,
pub search: String,
pub replace: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_or_vec_single_from_toml() {
let toml_str = r#"format = "ruff format""#;
#[derive(Deserialize)]
struct T {
format: StringOrVec,
}
let t: T = toml::from_str(toml_str).unwrap();
assert_eq!(t.format.commands(), vec!["ruff format"]);
}
#[test]
fn string_or_vec_multiple_from_toml() {
let toml_str = r#"format = ["cmd1", "cmd2", "cmd3"]"#;
#[derive(Deserialize)]
struct T {
format: StringOrVec,
}
let t: T = toml::from_str(toml_str).unwrap();
assert_eq!(t.format.commands(), vec!["cmd1", "cmd2", "cmd3"]);
}
#[test]
fn lint_config_backward_compat_string() {
let toml_str = r#"
format = "ruff format ."
check = "ruff check ."
typecheck = "mypy ."
"#;
let cfg: LintConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.format.unwrap().commands(), vec!["ruff format ."]);
assert_eq!(cfg.check.unwrap().commands(), vec!["ruff check ."]);
assert_eq!(cfg.typecheck.unwrap().commands(), vec!["mypy ."]);
}
#[test]
fn lint_config_array_commands() {
let toml_str = r#"
format = ["cmd1", "cmd2"]
check = "single-check"
"#;
let cfg: LintConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.format.unwrap().commands(), vec!["cmd1", "cmd2"]);
assert_eq!(cfg.check.unwrap().commands(), vec!["single-check"]);
assert!(cfg.typecheck.is_none());
}
#[test]
fn lint_config_all_optional() {
let toml_str = "";
let cfg: LintConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.format.is_none());
assert!(cfg.check.is_none());
assert!(cfg.typecheck.is_none());
}
#[test]
fn update_config_from_toml() {
let toml_str = r#"
update = "cargo update"
upgrade = ["cargo upgrade --incompatible", "cargo update"]
"#;
let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.update.unwrap().commands(), vec!["cargo update"]);
assert_eq!(
cfg.upgrade.unwrap().commands(),
vec!["cargo upgrade --incompatible", "cargo update"]
);
}
#[test]
fn update_config_all_optional() {
let toml_str = "";
let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.update.is_none());
assert!(cfg.upgrade.is_none());
}
#[test]
fn string_or_vec_empty_array_from_toml() {
let toml_str = "format = []";
#[derive(Deserialize)]
struct T {
format: StringOrVec,
}
let t: T = toml::from_str(toml_str).unwrap();
assert!(matches!(t.format, StringOrVec::Multiple(_)));
assert!(t.format.commands().is_empty());
}
#[test]
fn string_or_vec_single_element_array_from_toml() {
let toml_str = r#"format = ["cmd"]"#;
#[derive(Deserialize)]
struct T {
format: StringOrVec,
}
let t: T = toml::from_str(toml_str).unwrap();
assert_eq!(t.format.commands(), vec!["cmd"]);
}
#[test]
fn setup_config_single_string() {
let toml_str = r#"install = "uv sync""#;
let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.install.unwrap().commands(), vec!["uv sync"]);
}
#[test]
fn setup_config_array_commands() {
let toml_str = r#"install = ["step1", "step2"]"#;
let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.install.unwrap().commands(), vec!["step1", "step2"]);
}
#[test]
fn setup_config_all_optional() {
let toml_str = "";
let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.install.is_none());
}
#[test]
fn clean_config_single_string() {
let toml_str = r#"clean = "rm -rf dist""#;
let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.clean.unwrap().commands(), vec!["rm -rf dist"]);
}
#[test]
fn clean_config_array_commands() {
let toml_str = r#"clean = ["step1", "step2"]"#;
let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.clean.unwrap().commands(), vec!["step1", "step2"]);
}
#[test]
fn clean_config_all_optional() {
let toml_str = "";
let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.clean.is_none());
}
#[test]
fn build_command_config_single_strings() {
let toml_str = r#"
build = "cargo build"
build_release = "cargo build --release"
"#;
let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.build.unwrap().commands(), vec!["cargo build"]);
assert_eq!(cfg.build_release.unwrap().commands(), vec!["cargo build --release"]);
}
#[test]
fn build_command_config_array_commands() {
let toml_str = r#"
build = ["step1", "step2"]
build_release = ["step1 --release", "step2 --release"]
"#;
let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.build.unwrap().commands(), vec!["step1", "step2"]);
assert_eq!(
cfg.build_release.unwrap().commands(),
vec!["step1 --release", "step2 --release"]
);
}
#[test]
fn build_command_config_all_optional() {
let toml_str = "";
let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.build.is_none());
assert!(cfg.build_release.is_none());
}
#[test]
fn test_config_backward_compat_string() {
let toml_str = r#"command = "pytest""#;
let cfg: TestConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
assert!(cfg.e2e.is_none());
assert!(cfg.coverage.is_none());
}
#[test]
fn test_config_array_command() {
let toml_str = r#"command = ["cmd1", "cmd2"]"#;
let cfg: TestConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.command.unwrap().commands(), vec!["cmd1", "cmd2"]);
}
#[test]
fn test_config_with_coverage() {
let toml_str = r#"
command = "pytest"
coverage = "pytest --cov=. --cov-report=term-missing"
"#;
let cfg: TestConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
assert_eq!(
cfg.coverage.unwrap().commands(),
vec!["pytest --cov=. --cov-report=term-missing"]
);
assert!(cfg.e2e.is_none());
}
#[test]
fn test_config_all_optional() {
let toml_str = "";
let cfg: TestConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.command.is_none());
assert!(cfg.e2e.is_none());
assert!(cfg.coverage.is_none());
}
#[test]
fn full_alef_toml_with_lint_and_update() {
let toml_str = r#"
languages = ["python", "node"]
[crate]
name = "test"
sources = ["src/lib.rs"]
[lint.python]
format = "ruff format ."
check = "ruff check --fix ."
[lint.node]
format = ["npx oxfmt", "npx oxlint --fix"]
[update.python]
update = "uv sync --upgrade"
upgrade = "uv sync --all-packages --all-extras --upgrade"
[update.node]
update = "pnpm up -r"
upgrade = ["corepack up", "pnpm up --latest -r -w"]
"#;
let cfg: super::super::AlefConfig = toml::from_str(toml_str).unwrap();
let lint_map = cfg.lint.as_ref().unwrap();
assert!(lint_map.contains_key("python"));
assert!(lint_map.contains_key("node"));
let py_lint = lint_map.get("python").unwrap();
assert_eq!(py_lint.format.as_ref().unwrap().commands(), vec!["ruff format ."]);
let node_lint = lint_map.get("node").unwrap();
assert_eq!(
node_lint.format.as_ref().unwrap().commands(),
vec!["npx oxfmt", "npx oxlint --fix"]
);
let update_map = cfg.update.as_ref().unwrap();
assert!(update_map.contains_key("python"));
assert!(update_map.contains_key("node"));
let node_update = update_map.get("node").unwrap();
assert_eq!(node_update.update.as_ref().unwrap().commands(), vec!["pnpm up -r"]);
assert_eq!(
node_update.upgrade.as_ref().unwrap().commands(),
vec!["corepack up", "pnpm up --latest -r -w"]
);
}
#[test]
fn lint_config_with_precondition_and_before() {
let toml_str = r#"
precondition = "test -f target/release/libfoo.so"
before = "cargo build --release -p foo-ffi"
format = "gofmt -w packages/go"
check = "golangci-lint run ./..."
"#;
let cfg: LintConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.precondition.as_deref(), Some("test -f target/release/libfoo.so"));
assert_eq!(cfg.before.unwrap().commands(), vec!["cargo build --release -p foo-ffi"]);
assert!(cfg.format.is_some());
assert!(cfg.check.is_some());
}
#[test]
fn test_config_with_before_list() {
let toml_str = r#"
before = ["cd packages/python && maturin develop", "echo ready"]
command = "pytest"
"#;
let cfg: TestConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.precondition.is_none());
assert_eq!(
cfg.before.unwrap().commands(),
vec!["cd packages/python && maturin develop", "echo ready"]
);
assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
}
#[test]
fn setup_config_with_precondition() {
let toml_str = r#"
precondition = "which rustup"
install = "rustup update"
"#;
let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.precondition.as_deref(), Some("which rustup"));
assert!(cfg.before.is_none());
assert!(cfg.install.is_some());
}
#[test]
fn build_command_config_with_before() {
let toml_str = r#"
before = "cargo build --release -p my-lib-ffi"
build = "cd packages/go && go build ./..."
"#;
let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.precondition.is_none());
assert_eq!(
cfg.before.unwrap().commands(),
vec!["cargo build --release -p my-lib-ffi"]
);
assert!(cfg.build.is_some());
}
#[test]
fn clean_config_precondition_and_before_optional() {
let toml_str = r#"clean = "cargo clean""#;
let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.precondition.is_none());
assert!(cfg.before.is_none());
assert!(cfg.clean.is_some());
}
#[test]
fn update_config_with_precondition() {
let toml_str = r#"
precondition = "test -f Cargo.lock"
update = "cargo update"
"#;
let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.precondition.as_deref(), Some("test -f Cargo.lock"));
assert!(cfg.before.is_none());
assert!(cfg.update.is_some());
}
#[test]
fn full_alef_toml_with_precondition_and_before_across_sections() {
let toml_str = r#"
languages = ["go", "python"]
[crate]
name = "mylib"
sources = ["src/lib.rs"]
[lint.go]
precondition = "test -f target/release/libmylib_ffi.so"
before = "cargo build --release -p mylib-ffi"
format = "gofmt -w packages/go"
check = "golangci-lint run ./..."
[lint.python]
format = "ruff format packages/python"
check = "ruff check --fix packages/python"
[test.go]
precondition = "test -f target/release/libmylib_ffi.so"
before = ["cargo build --release -p mylib-ffi", "cp target/release/libmylib_ffi.so packages/go/"]
command = "cd packages/go && go test ./..."
[test.python]
command = "cd packages/python && uv run pytest"
[build_commands.go]
precondition = "which go"
before = "cargo build --release -p mylib-ffi"
build = "cd packages/go && go build ./..."
build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
[update.go]
precondition = "test -d packages/go"
update = "cd packages/go && go get -u ./..."
[setup.python]
precondition = "which uv"
install = "cd packages/python && uv sync"
[clean.go]
before = "echo cleaning go"
clean = "cd packages/go && go clean -cache"
"#;
let cfg: super::super::AlefConfig = toml::from_str(toml_str).unwrap();
let lint_map = cfg.lint.as_ref().unwrap();
let go_lint = lint_map.get("go").unwrap();
assert_eq!(
go_lint.precondition.as_deref(),
Some("test -f target/release/libmylib_ffi.so"),
"lint.go precondition should be preserved"
);
assert_eq!(
go_lint.before.as_ref().unwrap().commands(),
vec!["cargo build --release -p mylib-ffi"],
"lint.go before should be preserved"
);
assert!(go_lint.format.is_some());
assert!(go_lint.check.is_some());
let py_lint = lint_map.get("python").unwrap();
assert!(
py_lint.precondition.is_none(),
"lint.python should have no precondition"
);
assert!(py_lint.before.is_none(), "lint.python should have no before");
let test_map = cfg.test.as_ref().unwrap();
let go_test = test_map.get("go").unwrap();
assert_eq!(
go_test.precondition.as_deref(),
Some("test -f target/release/libmylib_ffi.so"),
"test.go precondition should be preserved"
);
assert_eq!(
go_test.before.as_ref().unwrap().commands(),
vec![
"cargo build --release -p mylib-ffi",
"cp target/release/libmylib_ffi.so packages/go/"
],
"test.go before list should be preserved"
);
let build_map = cfg.build_commands.as_ref().unwrap();
let go_build = build_map.get("go").unwrap();
assert_eq!(
go_build.precondition.as_deref(),
Some("which go"),
"build_commands.go precondition should be preserved"
);
assert_eq!(
go_build.before.as_ref().unwrap().commands(),
vec!["cargo build --release -p mylib-ffi"],
"build_commands.go before should be preserved"
);
let update_map = cfg.update.as_ref().unwrap();
let go_update = update_map.get("go").unwrap();
assert_eq!(
go_update.precondition.as_deref(),
Some("test -d packages/go"),
"update.go precondition should be preserved"
);
assert!(go_update.before.is_none(), "update.go before should be None");
let setup_map = cfg.setup.as_ref().unwrap();
let py_setup = setup_map.get("python").unwrap();
assert_eq!(
py_setup.precondition.as_deref(),
Some("which uv"),
"setup.python precondition should be preserved"
);
assert!(py_setup.before.is_none(), "setup.python before should be None");
let clean_map = cfg.clean.as_ref().unwrap();
let go_clean = clean_map.get("go").unwrap();
assert!(go_clean.precondition.is_none(), "clean.go precondition should be None");
assert_eq!(
go_clean.before.as_ref().unwrap().commands(),
vec!["echo cleaning go"],
"clean.go before should be preserved"
);
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SyncConfig {
#[serde(default)]
pub extra_paths: Vec<String>,
#[serde(default)]
pub text_replacements: Vec<TextReplacement>,
}