summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-27 22:54:55 +0300
committerPaul Buetow <paul@buetow.org>2026-06-27 22:54:55 +0300
commit5c7b3a78f2195c217453eda86818d340332e76c5 (patch)
treec970f8037d2a50dc909f734acf7ff89edc5c3637
parent2a79fd67e8a29e4e37cda38acd9390c5f4594dc2 (diff)
stats: replace render_kind case with declare -F name dispatch
_stats_render_category dispatched on render_kind via a four-arm case (camera|ranked|ordered|month), a modification magnet inconsistent with the codebase's other data-driven dispatch (the STATS_RECORD_FUNCTIONS registry and template.source.sh's prepare_template_render_var__<kind> declare -F lookup). Resolve the per-kind renderer by name instead: dispatch to _stats_render_section__<render_kind> via `declare -F`, guarded so a missing handler fails loudly (and so set -euo pipefail's errexit does not trip on declare -F's non-zero "absent" status). Rename the three section renderers to the _stats_render_section__{ranked,ordered,month} convention. Fold the camera kind into ranked: the camera leaderboard differed from an ordinary ranked section only by the extra 'stats-leaderboard' <ul> class -- data, not logic -- so it becomes a 'ranked' entry carrying that class in a new optional 5th spec field (list_class), eliminating the camera special case. Stats body output is byte-identical before/after for every render kind (verified via snapshot diff); the existing stats tests stay green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rwxr-xr-xbin/shuriken83
-rw-r--r--src/lib/stats-aggregate.source.sh16
-rw-r--r--src/lib/stats-render.source.sh67
3 files changed, 86 insertions, 80 deletions
diff --git a/bin/shuriken b/bin/shuriken
index 5399e2b..49412e6 100755
--- a/bin/shuriken
+++ b/bin/shuriken
@@ -4083,20 +4083,24 @@ refresh_splash() {
# TEMPLATE_RENDER_FIELD_SPECS) holding everything the generic reset, recording
# dispatch and render loops need:
#
-# count_array|prefix|heading|render_kind
+# count_array|prefix|heading|render_kind[|list_class]
#
# count_array the STATS_* associative array holding this category's counts;
# also the array reset_photo_exif_stats clears each run.
# prefix the namespace passed to _stats_tally / _stats_filter_link (and
# the filter-page slug prefix, e.g. "iso" -> iso-400).
# heading the <h2> shown on the overview (and the per-section title).
-# render_kind how the overview renders this category's bars:
-# camera - count-desc leaderboard (extra 'stats-leaderboard'
-# <ul> class); only the camera category uses it.
-# ranked - count-desc bars (years, lenses, decoded enums).
+# render_kind how the overview renders this category's bars; resolved by name
+# to a _stats_render_section__<render_kind> handler (declare -F
+# dispatch in stats-render.source.sh):
+# ranked - count-desc bars (camera leaderboard, years, lenses,
+# decoded enums).
# ordered - fixed bucket-ladder order from
# STATS_CATEGORY_BUCKETS (apertures wide->narrow, ...).
# month - calendar Jan..Dec order with English month names.
+# list_class optional extra <ul> CSS class for the 'ranked' kind. The camera
+# leaderboard passes 'stats-leaderboard' (data-only difference, so
+# no separate camera render kind); other ranked categories omit it.
#
# The array order IS the overview/body display order, so it must reproduce the
# historical _stats_build_body sequence exactly (camera, year, month, the four
@@ -4112,7 +4116,7 @@ refresh_splash() {
# harness sources the lib via test::source_shuriken_lib); a plain `declare -r`
# would be function-local and vanish on return.
declare -gra STATS_CATEGORIES=(
- 'STATS_CAMERAS|camera|Camera leaderboard|camera'
+ 'STATS_CAMERAS|camera|Camera leaderboard|ranked|stats-leaderboard'
'STATS_YEARS|year|Photos per year|ranked'
'STATS_MONTHS|month|Photos per month|month'
'STATS_APERTURE|aperture|Aperture|ordered'
@@ -4829,8 +4833,10 @@ _stats_keys_by_count_desc() {
# bucket ladder is read from STATS_CATEGORY_BUCKETS[array_name] (tab-delimited).
# Only buckets that actually occurred are emitted, and the whole section is
# skipped when none did. Bucket labels are internal/trusted but still escaped for
-# safety.
-_stats_render_ordered_section() {
+# safety. Reached dynamically as the 'ordered' render kind via the declare -F
+# dispatch in _stats_render_category; it ignores the optional trailing list_class
+# (only 'ranked' uses it) so all handlers share one call signature.
+_stats_render_section__ordered() {
local -r heading="$1"; shift
local -r array_name="$1"; shift
local -r prefix="$1"; shift
@@ -4860,8 +4866,12 @@ _stats_render_ordered_section() {
# Render a section ranked by count. Used where there is no natural axis order:
# the camera leaderboard, years, lenses, and the decoded enum categories. An
# optional list_class adds an extra CSS class to the <ul> (the camera leaderboard
-# passes 'stats-leaderboard' to space out its long, wrapping camera names).
-_stats_render_ranked_section() {
+# passes 'stats-leaderboard' to space out its long, wrapping camera names) -- this
+# is the only difference between the camera leaderboard and an ordinary ranked
+# section, so both share this one handler with the class coming from the registry
+# spec. Reached dynamically as the 'ranked' render kind via the declare -F
+# dispatch in _stats_render_category.
+_stats_render_section__ranked() {
local -r heading="$1"; shift
local -r array_name="$1"; shift
local -r prefix="$1"; shift
@@ -4888,8 +4898,9 @@ _stats_render_ranked_section() {
# by zero-padded number (01..12); this maps each to its English name so the axis
# is readable, and reuses the ordered-section omit-when-empty behaviour inline.
# Signature matches the other render kinds (heading array_name prefix total) so
-# the registry dispatcher can call it uniformly.
-_stats_render_month_section() {
+# the declare -F dispatcher can call it uniformly; reached as the 'month' render
+# kind. It ignores the optional trailing list_class (only 'ranked' uses it).
+_stats_render_section__month() {
local -r heading="$1"; shift
local -r array_name="$1"; shift
local -r prefix="$1"; shift
@@ -4919,11 +4930,18 @@ _stats_render_month_section() {
_stats_section_close
}
-# Render one category from its registry spec. Dispatches on the spec's
-# render_kind to the matching section renderer, all of which self-skip when their
-# array is empty. This is the single per-category render path: a new category
-# just needs a STATS_CATEGORIES entry (no new render branch here unless it needs
-# a brand-new kind).
+# Render one category from its registry spec. Resolves the spec's render_kind to
+# a _stats_render_section__<render_kind> handler BY NAME via declare -F and calls
+# it, mirroring template.source.sh's prepare_template_render_var__<kind> dispatch
+# and the STATS_RECORD_FUNCTIONS registry -- so a new render kind just adds a
+# matching handler function, with no switch to edit here. Every handler self-skips
+# when its array is empty. The optional 5th spec field (list_class) is passed
+# through; only the 'ranked' handler uses it (the camera leaderboard's extra
+# 'stats-leaderboard' <ul> class), the others ignore the trailing argument so all
+# handlers share one call signature. An unknown render_kind has no handler, so we
+# fail loudly rather than silently dropping a category. declare -F returns
+# non-zero when the function is absent, so it is tested in an `if` to keep
+# set -euo pipefail's errexit from tripping on a legitimately-missing handler.
_stats_render_category() {
local -r spec="$1"; shift
local -ri total="$1"; shift
@@ -4934,30 +4952,15 @@ _stats_render_category() {
local -r prefix="${fields[1]}"
local -r heading="${fields[2]}"
local -r render_kind="${fields[3]}"
+ local -r list_class="${fields[4]:-}"
+ local -r handler="_stats_render_section__$render_kind"
- case "$render_kind" in
- camera)
- _stats_render_ranked_section "$heading" "$array_name" "$prefix" \
- "$total" stats-leaderboard
- ;;
- ranked)
- _stats_render_ranked_section "$heading" "$array_name" "$prefix" \
- "$total"
- ;;
- ordered)
- _stats_render_ordered_section "$heading" "$array_name" "$prefix" \
- "$total"
- ;;
- month)
- _stats_render_month_section "$heading" "$array_name" "$prefix" \
- "$total"
- ;;
- *)
- printf 'ERROR: unknown stats render kind %q for %s\n' \
- "$render_kind" "$array_name" >&2
- return 1
- ;;
- esac
+ if ! declare -F "$handler" > /dev/null; then
+ printf 'ERROR: unknown stats render kind %q for %s\n' \
+ "$render_kind" "$array_name" >&2
+ return 1
+ fi
+ "$handler" "$heading" "$array_name" "$prefix" "$total" "$list_class"
}
# Assemble the full stats body by iterating STATS_CATEGORIES in registry order
diff --git a/src/lib/stats-aggregate.source.sh b/src/lib/stats-aggregate.source.sh
index 77603c5..24a0687 100644
--- a/src/lib/stats-aggregate.source.sh
+++ b/src/lib/stats-aggregate.source.sh
@@ -50,20 +50,24 @@
# TEMPLATE_RENDER_FIELD_SPECS) holding everything the generic reset, recording
# dispatch and render loops need:
#
-# count_array|prefix|heading|render_kind
+# count_array|prefix|heading|render_kind[|list_class]
#
# count_array the STATS_* associative array holding this category's counts;
# also the array reset_photo_exif_stats clears each run.
# prefix the namespace passed to _stats_tally / _stats_filter_link (and
# the filter-page slug prefix, e.g. "iso" -> iso-400).
# heading the <h2> shown on the overview (and the per-section title).
-# render_kind how the overview renders this category's bars:
-# camera - count-desc leaderboard (extra 'stats-leaderboard'
-# <ul> class); only the camera category uses it.
-# ranked - count-desc bars (years, lenses, decoded enums).
+# render_kind how the overview renders this category's bars; resolved by name
+# to a _stats_render_section__<render_kind> handler (declare -F
+# dispatch in stats-render.source.sh):
+# ranked - count-desc bars (camera leaderboard, years, lenses,
+# decoded enums).
# ordered - fixed bucket-ladder order from
# STATS_CATEGORY_BUCKETS (apertures wide->narrow, ...).
# month - calendar Jan..Dec order with English month names.
+# list_class optional extra <ul> CSS class for the 'ranked' kind. The camera
+# leaderboard passes 'stats-leaderboard' (data-only difference, so
+# no separate camera render kind); other ranked categories omit it.
#
# The array order IS the overview/body display order, so it must reproduce the
# historical _stats_build_body sequence exactly (camera, year, month, the four
@@ -79,7 +83,7 @@
# harness sources the lib via test::source_shuriken_lib); a plain `declare -r`
# would be function-local and vanish on return.
declare -gra STATS_CATEGORIES=(
- 'STATS_CAMERAS|camera|Camera leaderboard|camera'
+ 'STATS_CAMERAS|camera|Camera leaderboard|ranked|stats-leaderboard'
'STATS_YEARS|year|Photos per year|ranked'
'STATS_MONTHS|month|Photos per month|month'
'STATS_APERTURE|aperture|Aperture|ordered'
diff --git a/src/lib/stats-render.source.sh b/src/lib/stats-render.source.sh
index 1050ef2..e9b99c2 100644
--- a/src/lib/stats-render.source.sh
+++ b/src/lib/stats-render.source.sh
@@ -134,8 +134,10 @@ _stats_keys_by_count_desc() {
# bucket ladder is read from STATS_CATEGORY_BUCKETS[array_name] (tab-delimited).
# Only buckets that actually occurred are emitted, and the whole section is
# skipped when none did. Bucket labels are internal/trusted but still escaped for
-# safety.
-_stats_render_ordered_section() {
+# safety. Reached dynamically as the 'ordered' render kind via the declare -F
+# dispatch in _stats_render_category; it ignores the optional trailing list_class
+# (only 'ranked' uses it) so all handlers share one call signature.
+_stats_render_section__ordered() {
local -r heading="$1"; shift
local -r array_name="$1"; shift
local -r prefix="$1"; shift
@@ -165,8 +167,12 @@ _stats_render_ordered_section() {
# Render a section ranked by count. Used where there is no natural axis order:
# the camera leaderboard, years, lenses, and the decoded enum categories. An
# optional list_class adds an extra CSS class to the <ul> (the camera leaderboard
-# passes 'stats-leaderboard' to space out its long, wrapping camera names).
-_stats_render_ranked_section() {
+# passes 'stats-leaderboard' to space out its long, wrapping camera names) -- this
+# is the only difference between the camera leaderboard and an ordinary ranked
+# section, so both share this one handler with the class coming from the registry
+# spec. Reached dynamically as the 'ranked' render kind via the declare -F
+# dispatch in _stats_render_category.
+_stats_render_section__ranked() {
local -r heading="$1"; shift
local -r array_name="$1"; shift
local -r prefix="$1"; shift
@@ -193,8 +199,9 @@ _stats_render_ranked_section() {
# by zero-padded number (01..12); this maps each to its English name so the axis
# is readable, and reuses the ordered-section omit-when-empty behaviour inline.
# Signature matches the other render kinds (heading array_name prefix total) so
-# the registry dispatcher can call it uniformly.
-_stats_render_month_section() {
+# the declare -F dispatcher can call it uniformly; reached as the 'month' render
+# kind. It ignores the optional trailing list_class (only 'ranked' uses it).
+_stats_render_section__month() {
local -r heading="$1"; shift
local -r array_name="$1"; shift
local -r prefix="$1"; shift
@@ -224,11 +231,18 @@ _stats_render_month_section() {
_stats_section_close
}
-# Render one category from its registry spec. Dispatches on the spec's
-# render_kind to the matching section renderer, all of which self-skip when their
-# array is empty. This is the single per-category render path: a new category
-# just needs a STATS_CATEGORIES entry (no new render branch here unless it needs
-# a brand-new kind).
+# Render one category from its registry spec. Resolves the spec's render_kind to
+# a _stats_render_section__<render_kind> handler BY NAME via declare -F and calls
+# it, mirroring template.source.sh's prepare_template_render_var__<kind> dispatch
+# and the STATS_RECORD_FUNCTIONS registry -- so a new render kind just adds a
+# matching handler function, with no switch to edit here. Every handler self-skips
+# when its array is empty. The optional 5th spec field (list_class) is passed
+# through; only the 'ranked' handler uses it (the camera leaderboard's extra
+# 'stats-leaderboard' <ul> class), the others ignore the trailing argument so all
+# handlers share one call signature. An unknown render_kind has no handler, so we
+# fail loudly rather than silently dropping a category. declare -F returns
+# non-zero when the function is absent, so it is tested in an `if` to keep
+# set -euo pipefail's errexit from tripping on a legitimately-missing handler.
_stats_render_category() {
local -r spec="$1"; shift
local -ri total="$1"; shift
@@ -239,30 +253,15 @@ _stats_render_category() {
local -r prefix="${fields[1]}"
local -r heading="${fields[2]}"
local -r render_kind="${fields[3]}"
+ local -r list_class="${fields[4]:-}"
+ local -r handler="_stats_render_section__$render_kind"
- case "$render_kind" in
- camera)
- _stats_render_ranked_section "$heading" "$array_name" "$prefix" \
- "$total" stats-leaderboard
- ;;
- ranked)
- _stats_render_ranked_section "$heading" "$array_name" "$prefix" \
- "$total"
- ;;
- ordered)
- _stats_render_ordered_section "$heading" "$array_name" "$prefix" \
- "$total"
- ;;
- month)
- _stats_render_month_section "$heading" "$array_name" "$prefix" \
- "$total"
- ;;
- *)
- printf 'ERROR: unknown stats render kind %q for %s\n' \
- "$render_kind" "$array_name" >&2
- return 1
- ;;
- esac
+ if ! declare -F "$handler" > /dev/null; then
+ printf 'ERROR: unknown stats render kind %q for %s\n' \
+ "$render_kind" "$array_name" >&2
+ return 1
+ fi
+ "$handler" "$heading" "$array_name" "$prefix" "$total" "$list_class"
}
# Assemble the full stats body by iterating STATS_CATEGORIES in registry order