cmdkit 0.3.0

Deterministic command execution runtime for structured command graphs with configurable execution topology.
Documentation
use std::sync::{Arc, Mutex};

use cmdkit::{Argument, ArgumentValue, CMDKit, argument, command, switch};

fn format_switches(switches: &[String]) -> String {
    switches.to_vec().join(",")
}

fn format_arguments(arguments: &[Argument]) -> String {
    arguments
        .iter()
        .map(|argument| match argument.value {
            ArgumentValue::String(ref value) => format!("{}={value}", argument.name),
            ArgumentValue::Int(value) => format!("{}={value}", argument.name),
            ArgumentValue::Float(value) => format!("{}={value}", argument.name),
            _ => argument.name.to_string(),
        })
        .collect::<Vec<String>>()
        .join(",")
}

#[test]
fn strategy_chain_handles_subtask_tokens() {
    let captured = Arc::new(Mutex::new(Vec::new()));
    let captured_for_strategy = Arc::clone(&captured);
    let core = CMDKit::builder()
        .register(
            command("parent", "Parent command")
                .handler_fn(move |_, invocation| {
                    let options = invocation.switches;
                    let arguments = invocation.args;
                    let params = invocation.params;
                    let mut guard = captured_for_strategy
                        .lock()
                        .expect("capture lock should not be poisoned");
                    guard.push(vec![
                        format!("options={}", format_switches(&options)),
                        format!("arguments={}", format_arguments(&arguments)),
                        format!("params={params:?}"),
                    ]);
                    Ok(())
                })
                .with_options(vec![switch("opt", "option")])
                .with_arguments(vec![argument("flag", "flag")])
                .build(),
        )
        .build();

    let args = vec![
        "app".to_string(),
        "parent".to_string(),
        "--opt".to_string(),
        "--flag".to_string(),
        "value".to_string(),
    ];
    assert!(core.try_run_from_args(&args).is_ok());

    let guard = captured
        .lock()
        .expect("capture lock should not be poisoned");
    assert_eq!(guard.len(), 1);
    assert!(guard[0][0].contains("options=opt"));
    assert!(guard[0][1].contains("flag=value"));
    assert!(guard[0][2].contains("[]"));
}

#[test]
fn command_builder_registers_leaf_command_without_exposing_strategy_types() {
    let captured = Arc::new(Mutex::new(Vec::new()));
    let captured_for_handler = Arc::clone(&captured);

    let core = CMDKit::builder()
        .register(
            command("echo", "Echo command")
                .handler_fn(move |_, invocation| {
                    let options = invocation.switches;
                    let arguments = invocation.args;
                    let params = invocation.params;
                    captured_for_handler
                        .lock()
                        .expect("capture lock should not be poisoned")
                        .push(vec![
                            format!("options={}", format_switches(&options)),
                            format!("arguments={}", format_arguments(&arguments)),
                            format!("params={params:?}"),
                        ]);
                    Ok(())
                })
                .with_arguments(vec![argument("message", "message")])
                .build(),
        )
        .build();

    let args = vec![
        "app".to_string(),
        "echo".to_string(),
        "--message".to_string(),
        "hello".to_string(),
    ];

    assert!(core.try_run_from_args(&args).is_ok());
    let guard = captured
        .lock()
        .expect("capture lock should not be poisoned");
    assert!(guard[0][0].contains("options="));
    assert!(guard[0][1].contains("message=hello"));
    assert!(guard[0][2].contains("[]"));
}

#[test]
fn command_builder_registers_recursive_subcommands_without_router_exposure() {
    let captured = Arc::new(Mutex::new(Vec::new()));
    let captured_for_handler = Arc::clone(&captured);

    let core = CMDKit::builder()
        .register(
            command("tool", "tool root")
                .subcommand(
                    command("run", "run tasks").subcommand(
                        command("one", "run one task")
                            .handler_fn(move |_, invocation| {
                                let options = invocation.switches;
                                let arguments = invocation.args;
                                let params = invocation.params;
                                captured_for_handler
                                    .lock()
                                    .expect("capture lock should not be poisoned")
                                    .push(vec![
                                        format!("options={}", format_switches(&options)),
                                        format!("arguments={}", format_arguments(&arguments)),
                                        format!("params={params:?}"),
                                    ]);
                                Ok(())
                            })
                            .with_arguments(vec![argument("value", "value")]),
                    ),
                )
                .build(),
        )
        .build();

    let args = vec![
        "app".to_string(),
        "tool".to_string(),
        "run".to_string(),
        "one".to_string(),
        "--value".to_string(),
        "alpha".to_string(),
    ];

    assert!(core.try_run_from_args(&args).is_ok());
    let guard = captured
        .lock()
        .expect("capture lock should not be poisoned");
    assert!(guard[0][0].contains("options="));
    assert!(guard[0][1].contains("value=alpha"));
    assert!(guard[0][2].contains("[]"));
}