cmdkit 0.2.0

Core library for CLI tools, providing common functionality and utilities for building command-line applications.
Documentation
use std::sync::{Arc, Mutex};

use cmdkit::{Argument, CliCore, Switch, argument, command, switch};

fn format_switches(switches: &[Switch]) -> String {
    switches
        .iter()
        .map(|switch| switch.name.clone())
        .collect::<Vec<String>>()
        .join(",")
}

fn format_arguments(arguments: &[Argument]) -> String {
    arguments
        .iter()
        .map(|argument| {
            format!(
                "{}={}",
                argument.name,
                argument.value.clone().unwrap_or_default()
            )
        })
        .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 = CliCore::new();
    core.register(
        command("parent", "Parent command")
            .handler_fn(move |options, arguments, 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(),
    );

    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 = CliCore::new();
    core.register(
        command("echo", "Echo command")
            .handler_fn(move |options, arguments, 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(),
    );

    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 = CliCore::new();
    core.register(
        command("tool", "tool root")
            .subcommand(
                command("run", "run tasks").subcommand(
                    command("one", "run one task")
                        .handler_fn(move |options, arguments, 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(),
    );

    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("[]"));
}