diff --git a/CHANGELOG.md b/CHANGELOG.md index 55a8300..df97058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ and versions are tracked in the repo-root `VERSION` file. restores an existing model exactly or removes a partial new model. - Rejected unreachable CLI command routes and ancestor/child option collisions, including canonical-name, alias, option-name, and option-token conflicts. +- Rejected application declaration attributes outside each public API's + documented context instead of silently accepting and discarding them. - Prevented list, CLI, and application call paths from creating or overwriting caller-visible variables through undeclared internal scratch assignments. - Eliminated an intermittent macOS Bash process-group race in supervised diff --git a/lib/bash/app/README.md b/lib/bash/app/README.md index 826fdb0..9cbc8aa 100644 --- a/lib/bash/app/README.md +++ b/lib/bash/app/README.md @@ -11,6 +11,12 @@ unknown, or explicitly requested missing file is an error. ## Configuration +`base_app_init MODEL` accepts only `name=APP_KEY` and `description=TEXT`. +`base_app_config_define MODEL KEY TYPE` accepts `env=NAME`, `default=VALUE`, +`required=BOOL`, `secret=BOOL`, `enum=A,B`, `validator=FUNCTION`, and +`help=TEXT`. Attributes from one declaration context are rejected in the +other with usage status `2`. + ```bash base_app_init deploy name=deploy description="Example application" base_app_config_define deploy channel enum \ diff --git a/lib/bash/app/lib_app.sh b/lib/bash/app/lib_app.sh index a225d3f..0f91378 100644 --- a/lib/bash/app/lib_app.sh +++ b/lib/bash/app/lib_app.sh @@ -95,6 +95,21 @@ __base_bash_libs_app_attr_allowed__() { esac } +__base_bash_libs_app_restrict_attrs__() { + local owner="$1" allowed="$2" key + + for key in "${!__base_bash_libs_app_attrs[@]}"; do + __base_bash_libs_app_attr_allowed__ "$key" || return $? + case ",$allowed," in + *,"$key",*) ;; + *) + __base_bash_libs_app_error__ "$owner: attribute '$key' is not valid for this declaration." + return 2 + ;; + esac + done +} + __base_bash_libs_app_validate_bool__() { [[ "${1-}" =~ ^(0|1|true|false|yes|no|on|off)$ ]] } @@ -248,6 +263,7 @@ __base_bash_libs_app_cleanup_dispatch__() { } # base_app_init - Initializes an optional application policy model. +# Usage: base_app_init MODEL [name=APP_KEY] [description=TEXT] base_app_init() { local model="${1-}" key @@ -261,9 +277,7 @@ base_app_init() { } shift __base_bash_libs_app_parse_attrs__ "$@" || return $? - for key in "${!__base_bash_libs_app_attrs[@]}"; do - __base_bash_libs_app_attr_allowed__ "$key" || return $? - done + __base_bash_libs_app_restrict_attrs__ base_app_init 'name,description' || return $? if [[ -n "${__base_bash_libs_app_attrs[name]+set}" ]] && ! __base_bash_libs_app_valid_key__ "${__base_bash_libs_app_attrs[name]}"; then __base_bash_libs_app_error__ 'base_app_init: name must be a lowercase application key.' @@ -304,6 +318,9 @@ base_app_init() { } # base_app_config_define - Defines one typed configuration value. +# Usage: base_app_config_define MODEL KEY TYPE [env=NAME] [default=VALUE] +# [required=BOOL] [secret=BOOL] [enum=A,B] [validator=FUNCTION] +# [help=TEXT] base_app_config_define() { local model="${1-}" key="${2-}" type="${3-}" argument config_key local -a attrs=() @@ -333,9 +350,8 @@ base_app_config_define() { shift 3 for argument; do attrs+=("$argument"); done __base_bash_libs_app_parse_attrs__ "${attrs[@]+${attrs[@]}}" || return $? - for argument in "${!__base_bash_libs_app_attrs[@]}"; do - __base_bash_libs_app_attr_allowed__ "$argument" || return $? - done + __base_bash_libs_app_restrict_attrs__ base_app_config_define \ + 'env,default,required,secret,enum,validator,help' || return $? if [[ -n "${__base_bash_libs_app_attrs[env]+set}" ]] && ! __base_bash_libs_app_valid_identifier__ "${__base_bash_libs_app_attrs[env]}"; then __base_bash_libs_app_error__ "configuration '$key' has an invalid environment variable name." diff --git a/lib/bash/app/tests/lib_app.bats b/lib/bash/app/tests/lib_app.bats index 6b04afc..bd40cc3 100644 --- a/lib/bash/app/tests/lib_app.bats +++ b/lib/bash/app/tests/lib_app.bats @@ -141,6 +141,50 @@ assert_demo_snapshot() { [ "$source" = cli ] } +@test "application declaration APIs accept only their documented attributes" { + base_app_init alpha name=alpha-app description="Alpha application" + [ "${__base_bash_libs_app_models[alpha\|name]}" = alpha-app ] + [ "${__base_bash_libs_app_models[alpha\|description]}" = "Alpha application" ] + + base_app_config_define alpha mode string \ + env=APP_TEST_MODE default=dev required=true secret=true \ + enum=dev,prod validator=validate_test_label help="Execution mode" + [ "${__base_bash_libs_app_config[alpha\|mode\|env]}" = APP_TEST_MODE ] + [ "${__base_bash_libs_app_config[alpha\|mode\|default]}" = dev ] + [ "${__base_bash_libs_app_config[alpha\|mode\|required]}" = true ] + [ "${__base_bash_libs_app_config[alpha\|mode\|secret]}" = true ] + [ "${__base_bash_libs_app_config[alpha\|mode\|enum]}" = dev,prod ] + [ "${__base_bash_libs_app_config[alpha\|mode\|validator]}" = validate_test_label ] + [ "${__base_bash_libs_app_config[alpha\|mode\|help]}" = "Execution mode" ] +} + +@test "cross-context application attributes fail before model mutation" { + base_app_init alpha name=original description="Original model" + base_app_config_define alpha existing string default=preserved + local before_keys="${__base_bash_libs_app_models[alpha\|config-keys]}" + + if base_app_init alpha env=IGNORED; then + false + else + [ "$?" -eq 2 ] + fi + [ "${__base_bash_libs_app_models[alpha\|name]}" = original ] + [ "${__base_bash_libs_app_models[alpha\|description]}" = "Original model" ] + [ "${__base_bash_libs_app_config[alpha\|existing\|default]}" = preserved ] + + if base_app_config_define alpha ignored string name=discarded; then + false + else + [ "$?" -eq 2 ] + fi + [ -z "${__base_bash_libs_app_config[alpha\|ignored\|type]+set}" ] + [ "${__base_bash_libs_app_models[alpha\|config-keys]}" = "$before_keys" ] + + bats_run base_app_config_define alpha another string description=discarded + [ "$status" -eq 2 ] + [[ "$output" == *"attribute 'description' is not valid"* ]] +} + @test "missing required configuration and explicitly requested files fail clearly" { declare_test_config bats_run base_app_config_load demo