diff --git a/base_api_manifest.yaml b/base_api_manifest.yaml index 60221ff..d1e4102 100644 --- a/base_api_manifest.yaml +++ b/base_api_manifest.yaml @@ -175,7 +175,7 @@ modules: dependencies: std optional_commands: none artifacts: lib/bash/cli/lib_cli.sh,lib/bash/cli/README.md,lib/bash/cli/tests/lib_cli.bats - public_symbols: base_cli_command,base_cli_complete,base_cli_completion_script,base_cli_help,base_cli_model_init,base_cli_option,base_cli_parse,base_cli_positional,base_cli_result_count,base_cli_result_get,base_cli_result_get_positional,base_cli_run + public_symbols: base_cli_command,base_cli_complete,base_cli_completion_script,base_cli_help,base_cli_model_init,base_cli_option,base_cli_parse,base_cli_positional,base_cli_result_count,base_cli_result_get,base_cli_result_get_positional,base_cli_run,base_cli_validate_model signature_source: lib/bash/cli/README.md inputs: documented per symbol in the module README and API charter outputs: documented per symbol; parsed results use BASE_BASH_LIBS_CLI_RESULT_* globals and named result variables diff --git a/docs/api-reference.md b/docs/api-reference.md index fe12a64..5d68683 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -268,6 +268,7 @@ statuses, and side effects are normative in the linked module README and - `base_cli_result_get` — signature: see [`lib/bash/cli/README.md`](../lib/bash/cli/README.md). - `base_cli_result_get_positional` — signature: see [`lib/bash/cli/README.md`](../lib/bash/cli/README.md). - `base_cli_run` — signature: see [`lib/bash/cli/README.md`](../lib/bash/cli/README.md). +- `base_cli_validate_model` — signature: see [`lib/bash/cli/README.md`](../lib/bash/cli/README.md). ### `app` diff --git a/lib/bash/cli/README.md b/lib/bash/cli/README.md index 69428ea..2ee989e 100644 --- a/lib/bash/cli/README.md +++ b/lib/bash/cli/README.md @@ -8,6 +8,8 @@ metadata in one source of truth. It is one sourceable file and requires ## Public API - `base_cli_model_init MODEL [name=PROGRAM] [version=VERSION] [description=TEXT] [handler=FUNCTION]` +- `base_cli_validate_model MODEL` checks all declared handlers after the model + and application functions have been loaded; use it in tests or CI. starts or replaces a model. `MODEL` is an in-process identifier and `name` is the executable name shown in usage and completion output. - `base_cli_command MODEL PATH DESCRIPTION [HANDLER] [aliases=A,B]` declares a diff --git a/lib/bash/cli/lib_cli.sh b/lib/bash/cli/lib_cli.sh index cfedbeb..00849d4 100644 --- a/lib/bash/cli/lib_cli.sh +++ b/lib/bash/cli/lib_cli.sh @@ -344,6 +344,44 @@ base_cli_model_init() { return 0 } +# base_cli_validate_model - Verify that every declared command handler exists. +# +# Declaration remains order-independent: callers may declare a model before +# defining its handlers. Call this explicitly from tests or CI after all +# handlers have been loaded to fail early on wiring mistakes. +base_cli_validate_model() { + local model="${1-}" key path handler + local -a missing_handlers=() + + (($# == 1)) || { + __base_bash_libs_cli_declaration_usage__ 'base_cli_validate_model: expected a model identifier.' + return 2 + } + if ! __base_bash_libs_cli_valid_model__ "$model" || ! __base_bash_libs_cli_model_exists__ "$model"; then + __base_bash_libs_cli_declaration_usage__ "base_cli_validate_model: model '$model' is not initialized." + return 2 + fi + + handler="${__base_bash_libs_cli_models["$model|meta|handler"]-}" + if [[ -n "$handler" ]] && ! declare -F "$handler" >/dev/null 2>&1; then + missing_handlers+=(":$handler") + fi + for key in "${!__base_bash_libs_cli_models[@]}"; do + [[ "$key" == "$model|command|handler|"* ]] || continue + path="${key#"$model|command|handler|"}" + handler="${__base_bash_libs_cli_models["$key"]-}" + [[ -n "$handler" ]] || continue + if ! declare -F "$handler" >/dev/null 2>&1; then + missing_handlers+=("$path:$handler") + fi + done + if ((${#missing_handlers[@]} > 0)); then + __base_bash_libs_cli_declaration_usage__ "base_cli_validate_model: handlers are not defined: ${missing_handlers[*]}" + return 2 + fi + return 0 +} + # base_cli_command - Adds a command path such as `admin/user` to a model. # Usage: base_cli_command model path description [handler] [aliases=a,b] base_cli_command() { diff --git a/lib/bash/cli/tests/lib_cli.bats b/lib/bash/cli/tests/lib_cli.bats index 4e63a2d..f1641fb 100644 --- a/lib/bash/cli/tests/lib_cli.bats +++ b/lib/bash/cli/tests/lib_cli.bats @@ -142,6 +142,21 @@ declare_demo_model() { [ "$handler_calls" -eq 1 ] } +@test "explicit model validation catches undeclared handlers" { + base_cli_model_init validate name=validate handler=missing_root + base_cli_command validate run "Run" handler=missing_run + + bats_run base_cli_validate_model validate + + [ "$status" -eq 2 ] + [[ "$output" == *":missing_root"* ]] + [[ "$output" == *"run:missing_run"* ]] + + missing_root() { :; } + missing_run() { :; } + base_cli_validate_model validate +} + @test "completion emits aliases and a self-contained completion adapter" { declare_demo_model