Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a3ea7e5
docs: add design spec for argument and type system redesign
markdumay Jul 12, 2026
3f8dc6e
docs: add CloudCannon and Bookshop compatibility constraints to spec
markdumay Jul 12, 2026
acc53fa
docs: add implementation plan for the argument and type system redesign
markdumay Jul 12, 2026
f77c8f1
test: add golden-file harness with envelope characterization
markdumay Jul 12, 2026
0702e4f
test: characterize defaults, casting, and options behavior
markdumay Jul 12, 2026
fcafbdf
test: correct casting characterization comments to match observed beh…
markdumay Jul 12, 2026
be2bebc
test: characterize positional, required, and group-scoped arguments
markdumay Jul 12, 2026
2ff2ef0
test: characterize nested types, child structures, and frontmatter ty…
markdumay Jul 12, 2026
ec10692
test: characterize bookshop blueprint, sidecar, and cloudcannon payloads
markdumay Jul 12, 2026
e78bfca
test: document default-survival behavior in cloudcannon-null-heavy ca…
markdumay Jul 12, 2026
6d9b2f8
feat: add ArgsSchema compiler with recursive type resolution
markdumay Jul 12, 2026
f6f8143
fix: pass through reflect-type names and flatten schema error messages
markdumay Jul 12, 2026
57ca49c
feat: add Args entry point with recursive validation
markdumay Jul 12, 2026
293dbaf
refactor: rewire InitArgs as a compatibility shim over Args
markdumay Jul 12, 2026
48f773f
fix: classify excess positional arguments as newly detectable
markdumay Jul 12, 2026
4495934
test: characterize the legacy InitTypes contract
markdumay Jul 12, 2026
2c64ed8
refactor: rewire InitTypes as a compatibility shim over ArgsSchema
markdumay Jul 12, 2026
848429b
perf: cache compiled argument schemas per structure
markdumay Jul 12, 2026
cfc4df9
docs: document the Args validation API and strictness rollout
markdumay Jul 12, 2026
b20a676
feat: release the argument system redesign as a major version
markdumay Jul 12, 2026
de461f1
Merge branch 'main' into feat/args-system-redesign
markdumay Jul 12, 2026
8946736
fix(test): make golden comparison line-ending agnostic
markdumay Jul 12, 2026
2659fd2
Merge remote-tracking branch 'origin/feat/args-system-redesign' into …
markdumay Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Golden test files are byte-compared against Hugo output; never convert their line endings.
tests/golden/*.json text eol=lf
133 changes: 133 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,139 @@ This module supports the following parameters (see the section `params.modules`
| utils.filter | `[^0-9A-Za-zŽžÀ-ÿ ;.,\/'’"]` | Defines the regular expression for characters to remove from page descriptions. These page descriptions are used to define card content and metadata for search indexes. Adjust the filter to define which characters to support. You may need to adjust these settings to support specific diacritical letters. |
| utils.raw | false | Flag to indicate page descriptions should be returned as-is. In this setting, the filter is ignored. |

## Argument validation

This module provides the argument and type validation system used by shortcodes, partials, and
Bookshop components across the Hinode ecosystem. As of v6 the system is split into a cached
schema compiler (`ArgsSchema.html`), a strict recursive validator (`Args.html`), and two
compatibility shims (`InitArgs.html`, `InitTypes.html`) that preserve the legacy contract for
existing call sites.

### `Args.html` — clean entry point

```text
partial "utilities/Args.html" (dict
"structure" S | "bookshop" B # exactly one required
"child" C # optional
"args" .Params # map (named) or slice (positional)
"named" true|false # default true
"group" "shortcode"|"partial"|… # optional required-arg filter
"strict" true|false # default true
)
```

Returns a separated envelope — user values never share a namespace with bookkeeping keys:

```text
{ args: { <camelKey> → value }, # camelCase-only canonical keys
err: bool, errmsg: []string, warnmsg: []string, defaulted: []string }
```

`defaulted` lists the dotted path (e.g. `heading.align`) of every value that was filled from a
default, at any nesting depth.

The legacy `bookshop-` prefix sugar (passing `structure: "bookshop-hero"` to redirect to the
`hero` Bookshop component) is **not** part of the clean API — it is preserved only inside the
`InitArgs.html` shim for backward compatibility. Callers using `Args.html` directly must pass
`bookshop` explicitly.

### `ArgsSchema.html` — cached schema compiler

`Args.html` resolves its schema through `ArgsSchema.html`, invoked (and cached) per
`(structure, bookshop, child)` triple:

```text
partial "utilities/ArgsSchema.html" (dict "structure" S "bookshop" B "child" C)
→ dict:
"schema" map: declared argument name → node
"positions" map: position as string → argument name
"err" bool
"errmsg" []string, each prefixed "schema:" for compile-time problems
node:
"name" string declared argument name
"camelKey" string canonical access key (kebab/snake → camelCase, leading _ kept)
"types" []string declared type names
"accepts" []string value-kind tokens: string|bool|int|float|map|slice|maplist
"kind" string scalar|dict|list
"children" map member name → node (single user-defined type only)
"udtType" string the user-defined type name (only when children present)
"default", "config", "options", "position", "group" ([]string),
"deprecated", "alternative", "release", "parent", "comment" — present only when defined
"optional" bool always present on every node
"reflects" []string optional; raw package-qualified Go reflect-type names (e.g.
"template.HTML") declared in the data files, matched verbatim against
printf "%T" of the value at validation time
```

Callers MUST invoke it through
`partialCached "utilities/ArgsSchema.html" (dict …) (or $structure "") (or $bookshop "") (or $child "")`
to benefit from caching; a plain `partial` call still works but recompiles the schema on every
call.

### Migration note

`InitArgs.html` and `InitTypes.html` remain supported as compatibility shims within v5. They
preserve the legacy flat-map return shape (argument values under their declared names plus
camelCase duplicates, merged with `err`/`errmsg`/`warnmsg`/`default`) and run `Args.html` in
non-strict mode. New code should call `Args.html` (and `ArgsSchema.html`, where the raw compiled
schema is needed) directly. There is no plan to remove the shims within v5; migrating existing
call sites across Hinode, mod-blocks, and other modules to the clean API is a separate, deferred
effort.

### Null-tolerance rule

At every nesting level, `null`/absent is treated as "not provided": it is eligible for
defaulting (an `isset`-based `config` site-parameter lookup first — honoring an explicit `false`
or `0` site value — then the static `default`) and exempt from type, option, and range errors.
This is distinct from a value the caller *explicitly* provided as `false`, `0`, or `""`, which
**is** validated against the declared type. This is what lets a CloudCannon-authored payload
(every blueprint key present, typically `null`, plus `_bookshop_name`/`_ordinal`) build clean
while a genuinely wrong explicit value is still caught.

### Warnings-first strictness rollout

The following newly detectable problem classes surface as **warnings** (`warnmsg`, `err: false`)
through the `InitArgs.html`/`InitTypes.html` shims, rather than errors, for the full v6 release
line:

- Falsy-value type/select mismatches at the top level (e.g. `0` failing an `int` check, `false`
failing a `select` check).
- Nested member type/select/range findings (anything below the top level).
- Unknown nested attributes.
- Excess positional arguments.

Calling `Args.html` directly with `strict: true` (the default) already treats every one of these
as an error today. The promotion of these warnings to errors in `InitArgs.html`/`InitTypes.html`
is planned as the next major release (v7). Watch your own build's `warnmsg` output (or switch
early to `Args.html`) to find call sites that need fixing before that promotion release.

### Migrating from v5 to v6

v6 is a major release: the API of `InitArgs.html`/`InitTypes.html` is drop-in compatible, but
behavior changes in ways that can affect rendered output and build logs.

1. **Import path.** Update `go.mod` and the `[module.imports]` path in your Hugo configuration
from `github.com/gethinode/mod-utils/v5` to `github.com/gethinode/mod-utils/v6`, then re-vendor
(`npm run mod:vendor` or `hugo mod vendor`).
2. **CloudCannon expose configuration.** If your site uses `setup-cloudcannon-cms`, update any
expose globs that hardcode the vendored path (e.g.
`_vendor/github.com/gethinode/mod-utils/v5/...` → `.../v6/...`) and regenerate
`bookshop.config.cjs`; otherwise the live editor silently loses access to the validation
partials and structure data.
3. **Behavior changes to expect.**
- Nested defaults now apply correctly (e.g. an omitted `heading` receives its members' real
defaults such as `align: start`, `width: 8`) — previously they resolved to null. Rendered
output can change without any content edit.
- Defaults with falsy values (`false`, `0`) are now applied; previously they were silently
ignored.
- Explicit `null` members inside nested arguments are dropped from the returned map instead of
passed through.
- All validation problems are reported per call instead of only the first one.
- Site parameters explicitly set to `false`/`0` now take precedence over static defaults for
`config`-driven arguments.
- New warnings (see the rollout section above) may appear in your build logs; they are
non-blocking in v6 and indicate call sites to clean up before v7.

## Contributing

This module uses [semantic-release][semantic-release] to automate the release of new versions. The package uses `husky` and `commitlint` to ensure commit messages adhere to the [Conventional Commits][conventionalcommits] specification. You can run `npx git-cz` from the terminal to help prepare the commit message.
Expand Down
Loading
Loading