Documentation
¶
Overview ¶
Package binder builds Cobra flags (and, in form.go, an interactive huh form) from a Go struct by reflection. In V1 it ships for exactly one consumer: the jenticctl installer's generated BackendConfig (impl/6.0 §3) — the API-surface binder described in impl/2.2 stays a post-V1 exploration (00_openapi_gen.md §3).
The installer config is NESTED and not uniformly one level deep (databases.registry.host is depth 2; security.jwt_verification.* nests further), so the binder walks the struct recursively and binds every SCALAR leaf under its full dotted path with dots/underscores rendered as dashes: server.public_base_url → --server-public-base-url, databases.registry.host → --databases-registry-host. That is deliberately the same naming rule as the backend's JENTIC__SECTION__KEY env overrides and the YAML keys — one mental model across flags, env, and file. Non-scalar leaves (slices, maps) are skipped: they stay reachable only via YAML/--out, never a flag (impl/6.0 §3, "flatten scalars, never collections").
Two lessons from impl/2.2 §1a carry over: unmapped scalar-ish kinds FAIL LOUD (panic at construction, caught in CI, never a silently-undsettable field), and pointer-ness is used only for allocation during hydration, never as a semantic signal.
Index ¶
- func BindFlags(cmd *cobra.Command, target interface{})
- func BindFlagsWithOptions(cmd *cobra.Command, target interface{}, opts BindOptions)
- func BuildDynamicForm(target interface{}, opts FormOptions) *huh.Form
- func ChangedOverrides(cmd *cobra.Command, target interface{}) (map[string]any, error)
- func HydrateStruct(cmd *cobra.Command, target interface{}) error
- func LeafPaths(target interface{}) []string
- func NonZeroOverrides(target interface{}, exclude map[string]bool) map[string]any
- type BindOptions
- type FormOptions
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BindFlags ¶
BindFlags registers one Cobra flag per scalar leaf of the nested config struct, named by its dotted path flattened to kebab-case (impl/6.0 §3). It is idempotent-safe only on a fresh command; re-binding the same struct onto a command that already has a colliding flag will panic via pflag, which is the desired loud failure.
func BindFlagsWithOptions ¶
func BindFlagsWithOptions(cmd *cobra.Command, target interface{}, opts BindOptions)
BindFlagsWithOptions is BindFlags with the installer's Exclude/Hidden controls.
func BuildDynamicForm ¶
func BuildDynamicForm(target interface{}, opts FormOptions) *huh.Form
BuildDynamicForm inspects the nested config struct and generates a grouped interactive huh.Form: one huh.Group per top-level section, one huh.Field per scalar leaf inside it (impl/6.1 §3). Grouping is STRUCTURAL — AppConfig's section models already partition every leaf into the logical clusters the wizard wants, so there are no group tags to inject into the read-only generated struct. Deeper scalar leaves surface under their dotted path within their section's screen, matching the flag flatten rule. Non-scalar leaves (slices/maps) and excluded (sensitive) leaves are skipped — reachable only via flags/YAML.
The returned form's fields bind directly to the struct's leaves, so on form.Run() completion the config is populated in place. Pointer sections/leaves are allocated first (via the shared walk) so Bubble Tea never nil-derefs.
func ChangedOverrides ¶
ChangedOverrides returns a nested map of ONLY the flags the operator explicitly set, keyed by the config's dotted path split back into a section tree (databases-registry-host, set, → {"databases":{"registry":{"host":...}}}). This is the flag layer of the installer's precedence ladder: it feeds straight into the settings merge (ctl.ResolveSettings) so unset flags never contribute a zero-value that would clobber a schema default or preset (impl/6.0 §3.5). The target is used only to enumerate the leaves and their kinds; its field values are not read.
func HydrateStruct ¶
HydrateStruct writes every EXPLICITLY-SET flag back onto its leaf, leaving unset leaves untouched so the defaults<preset<flags precedence ladder (impl/6.0 §3.5) holds: applyDefaults/applyPreset populate cfg first, then this overrides only what the operator actually passed. Flags the user did not set are skipped via pflag's Changed(), never clobbering a preset/default value with a flag zero-value.
func LeafPaths ¶
func LeafPaths(target interface{}) []string
LeafPaths returns the dotted path of every scalar leaf of the nested config struct, in walk order. Callers use it to build Exclude sets for a whole section (e.g. everything under `telemetry.`) without duplicating the leaf walk that BindFlags and the form generator share.
func NonZeroOverrides ¶
NonZeroOverrides returns a nested map of the struct's scalar leaves that hold a NON-ZERO value, skipping any path in exclude. It is the read-back for the interactive config form (impl/6.1): the form binds a fresh struct, the operator fills in only what they want to change, and this collects exactly those leaves as overrides ("" / 0 / false == "leave the wizard's value alone"). exclude carries the sensitive paths so a secret typed into a (non-excluded-by-accident) field can never leak into the overlay.
Types ¶
type BindOptions ¶
BindOptions tune BindFlags for the installer's needs (impl/6.0): Exclude drops leaves entirely (sensitive secret-bearing paths never become flags), and Hidden registers the flags but marks them hidden so they work yet stay out of --help and the public cli-reference noise. Paths are dotted (server.public_base_url).
type FormOptions ¶
FormOptions tune the generated interactive form. Exclude drops leaves (the installer passes the sensitive-path set so secrets are never prompted as plain form fields — they belong in the secret-generation flow, impl/6.0).