summaryrefslogtreecommitdiff
path: root/src/lib/action.source.sh
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-17 22:20:43 +0300
committerPaul Buetow <paul@buetow.org>2026-06-17 22:20:43 +0300
commit2dc8a01b83734a0b01eb51d20c880883625e3c87 (patch)
tree6d00c42b62501d9fa158c4f994f27a29077a77b8 /src/lib/action.source.sh
parent5fb1f17e3b16c8ef4b71648419251d76ae1227b2 (diff)
gn0: dispatch CLI actions via registration table
Adding a CLI action previously required editing two case statements (run_action and run_configured_action). Introduce ACTION_SPECS, a single '|'-delimited registry (flag|handler|requires_config|validation_fn| validation_arg) matching the CLI_OPTION_SPEC / STATS_CATEGORIES encoding, and replace both dispatchers with table lookups via action_spec_field. - run_action: looks up requires_config; routes non-config actions (--version/--init) through run_unconfigured_action (shared config/override/force precheck) and the rest through run_configured_action. - run_configured_action: keeps the force-generate guard and config load/log, then runs the entry's validation_fn (with optional arg, used by --dry-run) and handler via run_configured_action_body. - Extracted the --clean inline rm body into a clean_dist handler and added an action_print_version handler so every action is just a registry entry plus named functions. - Unknown/empty actions have no entry, so dispatch falls through to the same usage + exit 1 behavior as the old case "*)" arm. Adding an action is now one ACTION_SPECS entry plus its handler/ validation functions; neither dispatcher changes (Open/Closed). Added test_action_dispatch_is_registry_driven proving every parser action flag has a registry entry, all handlers/validators resolve, and unknown actions are rejected. All existing action/dispatcher tests pass unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'src/lib/action.source.sh')
-rw-r--r--src/lib/action.source.sh304
1 files changed, 168 insertions, 136 deletions
diff --git a/src/lib/action.source.sh b/src/lib/action.source.sh
index 4ff5d68..918900f 100644
--- a/src/lib/action.source.sh
+++ b/src/lib/action.source.sh
@@ -1,26 +1,93 @@
-run_simple_action() {
- case "$SHURIKEN_CLI_ACTION" in
- --version)
- if [[ -n "$SHURIKEN_CLI_CONFIG_FILE" \
- || "$SHURIKEN_CLI_HAS_CONFIG_OVERRIDES" = 'yes' \
- || "$SHURIKEN_FORCE_GENERATE" = yes ]]; then
- usage
- exit 1
- fi
-
- printf 'This is Shuriken Version %s\n' "$VERSION"
- ;;
- --init)
- if [[ -n "$SHURIKEN_CLI_CONFIG_FILE" \
- || "$SHURIKEN_CLI_HAS_CONFIG_OVERRIDES" = 'yes' \
- || "$SHURIKEN_FORCE_GENERATE" = yes ]]; then
- usage
- exit 1
- fi
-
- init_config
- ;;
- esac
+# ----------------------------------------------------------------------------
+# Action registry (single source of truth, task gn0)
+# ----------------------------------------------------------------------------
+# ACTION_SPECS is the one place a CLI action is declared. Adding an action means
+# appending one entry here (plus its handler/validation functions); neither
+# run_action nor run_configured_action is touched again (Open/Closed). Each entry
+# is a '|'-delimited spec (same encoding as src/shuriken.sh's CLI_OPTION_SPEC and
+# stats-aggregate.source.sh's STATS_CATEGORIES):
+#
+# flag|handler|requires_config|validation_fn|validation_arg
+#
+# flag the CLI action flag (also the SHURIKEN_CLI_ACTION value).
+# handler function run to perform the action. For configured actions it
+# is invoked via run_configured_action_body (in-process, status
+# propagated); for non-config actions it is called directly.
+# requires_config yes -> dispatched through run_configured_action: the config
+# file is resolved/loaded/logged first, then the
+# validation_fn and handler run.
+# no -> dispatched without loading any config (e.g. --version,
+# --init); these reject any --config/override/--force via
+# the shared run_unconfigured_action precheck.
+# validation_fn validation function to run before the handler (empty for none,
+# e.g. --version prints inline). Its non-zero status aborts the
+# action before the handler runs, preserving the historical
+# per-action pre-checks (validate_generation_config, etc.).
+# validation_arg optional single argument passed to validation_fn (only
+# --dry-run uses it: "no" tells validate_generation_config to
+# skip the strict-generation checks).
+#
+# The array order is the canonical action order. Unknown or empty actions are not
+# in the table, so run_action's "no entry" path reproduces the old case "*)" arm
+# exactly (usage + exit 1).
+# Declared -g so it survives being sourced from inside a function (the test
+# harness sources the lib via test::source_shuriken_lib); a plain `declare -r`
+# would be function-local and vanish on return.
+declare -gra ACTION_SPECS=(
+ '--version|action_print_version|no||'
+ '--init|init_config|no||'
+ '--clean|clean_dist|yes|validate_clean_dist_dir|'
+ '--generate|generate_staged|yes|validate_generation_config|'
+ '--refresh-splash|refresh_splash|yes|validate_refresh_splash_config|'
+ '--sync|sync_dist|yes|validate_sync_config|'
+ '--dry-run|dry_run|yes|validate_generation_config|no'
+ '--print-config|print_config|yes|validate_print_config|'
+)
+
+# Look up a field of an action's registry entry by flag. Prints the requested
+# field's value (empty if the action is unknown or the field is empty). Fields
+# are addressed by zero-based index into the '|'-delimited spec:
+# 0 flag 1 handler 2 requires_config 3 validation_fn 4 validation_arg
+action_spec_field() {
+ local -r action="$1"; shift
+ local -ri field_index="$1"; shift
+ local spec
+ local -a fields=()
+
+ for spec in "${ACTION_SPECS[@]}"; do
+ IFS='|' read -r -a fields <<< "$spec"
+ if [ "${fields[0]}" = "$action" ]; then
+ printf '%s\n' "${fields[$field_index]:-}"
+ return 0
+ fi
+ done
+
+ return 1
+}
+
+# Print the registered handler for an action and the bundled version banner. Kept
+# as a named function (not an inline printf) so --version is just another
+# registry entry with a handler, like every other action.
+action_print_version() {
+ printf 'This is Shuriken Version %s\n' "$VERSION"
+}
+
+# Run a non-config action (requires_config=no): --version, --init. These never
+# load a config, so any --config/override/--force is a usage error (matching the
+# historical run_simple_action pre-check that guarded both arms identically).
+run_unconfigured_action() {
+ local -r action="$1"; shift
+ local handler
+
+ if [[ -n "$SHURIKEN_CLI_CONFIG_FILE" \
+ || "$SHURIKEN_CLI_HAS_CONFIG_OVERRIDES" = 'yes' \
+ || "$SHURIKEN_FORCE_GENERATE" = yes ]]; then
+ usage
+ exit 1
+ fi
+
+ handler=$(action_spec_field "$action" 1)
+ "$handler"
}
# Runs an action function in-process and propagates its exit status.
@@ -136,12 +203,46 @@ clean_generation_staging_artifacts() {
done
}
+# --clean handler: remove DIST_DIR plus any leftover staging/backup dirs.
+#
+# Kept as a named handler (referenced from ACTION_SPECS) so --clean dispatches
+# exactly like the other configured actions. Its validation (validate_clean_dist_dir,
+# declared in the registry) has already run before this is called, so DIST_DIR is
+# known safe -- unset, empty, and dangerous paths (/, HOME, cwd, system dirs) were
+# rejected before any destructive rm -rf could happen.
+clean_dist() {
+ if [ -d "$DIST_DIR" ]; then
+ log_info "Cleaning $DIST_DIR"
+ rm -rf "$DIST_DIR"
+ else
+ log_verbose "Output directory does not exist: $DIST_DIR"
+ fi
+
+ # Also remove any leftover staging/backup directories that the generation
+ # pipeline created as siblings of DIST_DIR. This stays behind the
+ # validate_clean_dist_dir guard (so a dangerous DIST_DIR aborts before any
+ # deletion).
+ clean_generation_staging_artifacts
+}
+
+# Dispatch a configured action (requires_config=yes) via the registry.
+#
+# Shared scaffolding for every config-backed action: enforce the force-generate
+# guard, resolve/load/log the config, then run the action's registered
+# validation_fn and handler looked up in ACTION_SPECS. Adding a configured action
+# is a registry entry plus its handler/validation functions -- this dispatcher
+# never changes (Open/Closed).
run_configured_action() {
+ local -r action="$SHURIKEN_CLI_ACTION"
local rc_file
+ local handler
+ local validation_fn
+ local validation_arg
local -i status=0
- if [[ "$SHURIKEN_FORCE_GENERATE" = yes \
- && "$SHURIKEN_CLI_ACTION" != --generate ]]; then
+ # --force only makes sense for --generate; any other configured action with
+ # --force set is a usage error (unchanged historical behavior).
+ if [[ "$SHURIKEN_FORCE_GENERATE" = yes && "$action" != --generate ]]; then
usage
exit 1
fi
@@ -155,120 +256,51 @@ run_configured_action() {
log_configured_action "$rc_file"
- case "$SHURIKEN_CLI_ACTION" in
- --clean)
- # Validate DIST_DIR before any destructive rm -rf. Unlike the bare
- # check that used to live here, validate_clean_dist_dir rejects unset,
- # empty, and dangerous paths (/, HOME, cwd, system dirs) so a
- # misconfigured DIST_DIR can never nuke the wrong tree.
- validate_clean_dist_dir
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
-
- if [ -d "$DIST_DIR" ]; then
- log_info "Cleaning $DIST_DIR"
- rm -rf "$DIST_DIR"
- else
- log_verbose "Output directory does not exist: $DIST_DIR"
- fi
-
- # Also remove any leftover staging/backup directories that the
- # generation pipeline created as siblings of DIST_DIR. This stays
- # behind the validate_clean_dist_dir guard above (so a dangerous
- # DIST_DIR aborts before any deletion).
- clean_generation_staging_artifacts
- ;;
- --generate)
- validate_generation_config
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
-
- run_configured_action_body generate_staged
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
- ;;
- --refresh-splash)
- validate_refresh_splash_config
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
-
- run_configured_action_body refresh_splash
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
- ;;
- --sync)
- validate_sync_config
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
-
- run_configured_action_body sync_dist
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
- ;;
- --dry-run)
- validate_generation_config no
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
-
- run_configured_action_body dry_run
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
- ;;
- --print-config)
- validate_print_config
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
-
- run_configured_action_body print_config
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
- ;;
- esac
+ validation_fn=$(action_spec_field "$action" 3)
+ validation_arg=$(action_spec_field "$action" 4)
+ if [ -n "$validation_fn" ]; then
+ # Pass validation_arg only when present (--dry-run uses "no"); otherwise
+ # call with no argument so validators see the same argv as before.
+ if [ -n "$validation_arg" ]; then
+ "$validation_fn" "$validation_arg"
+ else
+ "$validation_fn"
+ fi
+ status=$?
+ if (( status != 0 )); then
+ return "$status"
+ fi
+ fi
+
+ handler=$(action_spec_field "$action" 1)
+ run_configured_action_body "$handler"
+ status=$?
+ if (( status != 0 )); then
+ return "$status"
+ fi
}
+# Top-level action dispatcher. Looks the parsed action up in ACTION_SPECS and
+# routes it: requires_config=no actions run via run_unconfigured_action, the rest
+# via run_configured_action. An unknown or empty action has no registry entry, so
+# this reproduces the old case "*)" arm exactly: usage + exit 1.
run_action() {
+ local -r action="$SHURIKEN_CLI_ACTION"
+ local requires_config
local -i status=0
- case "$SHURIKEN_CLI_ACTION" in
- --version|--init)
- run_simple_action
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
- ;;
- --clean|--generate|--refresh-splash|--sync|--dry-run|--print-config)
- run_configured_action
- status=$?
- if (( status != 0 )); then
- return "$status"
- fi
- ;;
- *)
- usage
- exit 1
- ;;
- esac
+ if ! requires_config=$(action_spec_field "$action" 2); then
+ usage
+ exit 1
+ fi
+
+ if [ "$requires_config" = no ]; then
+ run_unconfigured_action "$action"
+ else
+ run_configured_action
+ fi
+ status=$?
+ if (( status != 0 )); then
+ return "$status"
+ fi
}