Skip to content

refactor(projectors): single-source decodePayload[T] + DRY completeness guard (WS16b)#438

Merged
PaulDotterer merged 1 commit into
mainfrom
refactor/projector-decodepayload-dry
Jun 15, 2026
Merged

refactor(projectors): single-source decodePayload[T] + DRY completeness guard (WS16b)#438
PaulDotterer merged 1 commit into
mainfrom
refactor/projector-decodepayload-dry

Conversation

@PaulDotterer

@PaulDotterer PaulDotterer commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

WS16b DRY-discipline — projector decode collapse (closes #437)

The ~90 *FromEvent decoders in internal/projectors/ each hand-rolled the identical shape:
(stream_type, event_type) guard → ErrIgnoredEvent; empty-payload guard; json.Unmarshal(e.Data, &p); canonical "projector: invalid <event> payload: %w" wrap.

What changed

  • decodePayload[T] (new decode.go) centralizes that boilerplate; per-event field validation stays in the caller. 40 decoders converted, −305 LOC net across 18 files. Error-message forms are byte-identical (the comprehensive projector suite asserts them — all green).
  • TestDecodePayloadHelperUsedByAllProjectors (self-discovering AST guard) fails the build if any *FromEvent hand-rolls json.Unmarshal without routing through the helper, unless it's a recorded exception. The allowlist is guarded against stale entries and a vacuous (zero-match) walk. It empirically caught 3 decoders during authoring.
  • Legitimate exceptions (allowlisted with reasons): empty-payload-valid (decoder returns an envelope-derived default on empty), multi-stream / runtime-eventType (guard spans two stream types or a runtime event-type param — the fixed-arg helper can't express it), and non-canonical DB-params shape. Decoders that route through a shared per-cluster sub-decoder carry no direct json.Unmarshal and are neither flagged nor allowlisted.
  • ADR 0021 records the single-source-helper + completeness-test DRY policy and the decision to defer the noisy dupl CI backstop in favor of specific completeness tests.

Gates

gofmt / go vet clean; full go test ./internal/projectors/ green (37s, asserts exact error strings); local CodeRabbit: no findings.

Part of the security/RBAC hardening work plan (WS16b). The cert/CSR test-fixture row lands separately in sdk + agent.

Summary by CodeRabbit

  • Documentation

    • Added architectural decision record documenting a code quality enforcement strategy for payload decoding.
  • Refactor

    • Standardized internal payload decoding across multiple event handlers using a shared helper function for improved code consistency and maintainability.
  • Tests

    • Added automated test to enforce consistent payload decoding patterns across the codebase.

…ss guard

WS16b DRY-discipline. The ~90 *FromEvent decoders in internal/projectors/ each
hand-rolled the identical decode shape: a (stream_type, event_type) guard
returning ErrIgnoredEvent, an empty-payload guard, json.Unmarshal(e.Data, &p),
and the canonical "projector: invalid <event> payload: %w" wrap. Collapse the
boilerplate into one generic decodePayload[T] helper; per-event field
validation stays in the caller. 40 decoders converted (-305 LOC net).

TestDecodePayloadHelperUsedByAllProjectors AST-walks the package and fails the
build if any *FromEvent hand-rolls json.Unmarshal without routing through the
helper, unless it is a recorded exception (empty-payload-valid, multi-stream /
runtime-eventType, or non-canonical DB-params shape). The allowlist is guarded
against stale entries and a vacuous walk. Decoders that route through a shared
per-cluster sub-decoder carry no direct json.Unmarshal and are neither flagged
nor allowlisted.

ADR 0021 records the single-source-helper + self-discovering-test DRY policy and
the decision to defer the noisy `dupl` CI backstop in favor of specific
completeness tests.

Closes #437
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 08af3870-04bf-41af-b990-7bdd6b7c709f

📥 Commits

Reviewing files that changed from the base of the PR and between 3b5bf61 and 65ada20.

📒 Files selected for processing (22)
  • docs/adr/0021-single-source-helpers-dry.md
  • internal/projectors/action.go
  • internal/projectors/action_set.go
  • internal/projectors/assignment.go
  • internal/projectors/compliance.go
  • internal/projectors/compliance_policy.go
  • internal/projectors/decode.go
  • internal/projectors/decode_completeness_test.go
  • internal/projectors/definition.go
  • internal/projectors/device.go
  • internal/projectors/device_group.go
  • internal/projectors/execution.go
  • internal/projectors/identity_provider.go
  • internal/projectors/lps_password.go
  • internal/projectors/luks_key.go
  • internal/projectors/role.go
  • internal/projectors/scim_group_mapping.go
  • internal/projectors/token.go
  • internal/projectors/user.go
  • internal/projectors/user_group.go
  • internal/projectors/user_role.go
  • internal/projectors/user_selection.go

📝 Walkthrough

Walkthrough

Introduces a generic decodePayload[T] helper in internal/projectors/decode.go that centralizes stream/event-type validation, empty-payload rejection, and JSON unmarshalling. Refactors ~20 projector files to delegate their decode boilerplate to this helper. Adds an AST-based completeness test that fails if any *FromEvent decoder hand-rolls json.Unmarshal outside an explicit allowlist. Documents the approach in ADR 0021.

Changes

DRY projector decode helper + completeness guard

Layer / File(s) Summary
decodePayload[T] helper and ADR 0021
internal/projectors/decode.go, docs/adr/0021-single-source-helpers-dry.md
decode.go introduces the generic helper enforcing stream/event identity, empty-payload rejection, and canonical json.Unmarshal error wrapping. ADR 0021 documents the strategy, the allowlist exception mechanism, and the deferred dupl CI job.
Bulk refactor of all *FromEvent decoders
internal/projectors/action.go, internal/projectors/action_set.go, internal/projectors/assignment.go, internal/projectors/compliance.go, internal/projectors/compliance_policy.go, internal/projectors/definition.go, internal/projectors/device.go, internal/projectors/device_group.go, internal/projectors/execution.go, internal/projectors/identity_provider.go, internal/projectors/lps_password.go, internal/projectors/luks_key.go, internal/projectors/role.go, internal/projectors/scim_group_mapping.go, internal/projectors/token.go, internal/projectors/user.go, internal/projectors/user_group.go, internal/projectors/user_role.go, internal/projectors/user_selection.go
All affected *FromEvent functions replace inline stream/event gating, empty-payload checks, and json.Unmarshal with a single decodePayload[T] call and direct error return. Several files drop the now-unused encoding/json import.
AST-based completeness enforcement test
internal/projectors/decode_completeness_test.go
TestDecodePayloadHelperUsedByAllProjectors scans all non-test Go files using AST inspection, fails if any *FromEvent function calls json.Unmarshal directly without being in the explicit allowlist, and also fails on stale allowlist entries.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manchtools/power-manage-server#192: Refactors the same *FromEvent decoder functions to use shared eventtypes/payloads.* structs — the direct predecessor to this PR's further centralization via decodePayload[T].
  • manchtools/power-manage-server#189: Touches the same internal/projectors/*FromEvent decoder functions (e.g., ActionCreatedFromEvent, ComplianceResultUpdatedFromEvent) by refactoring event-type matching to use internal/eventtypes constants.
  • manchtools/power-manage-server#179: Modifies ComplianceResultUpdatedFromEvent/ComplianceResultRemovedFromEvent payload decoding logic that this PR replaces with the shared decodePayload[T] helper.

Poem

🐇 A hundred decoders, all doing the same,
Stream-type! Event-type! Unmarshal again!
One helper to rule them, decodePayload[T],
The AST watchdog won't let boilerplate free.
The allowlist holds the exceptions with care,
And ADR 0021 documents what's there! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: introducing a single-source decodePayload[T] helper and implementing a DRY completeness guard test across projectors.
Linked Issues check ✅ Passed The PR successfully addresses all objectives from issue #437: introduces decodePayload[T] helper [decode.go], refactors 40+ *FromEvent decoders to use it [action.go-user_selection.go], implements AST-based completeness test with allowlist [decode_completeness_test.go], and includes ADR 0021 documentation.
Out of Scope Changes check ✅ Passed All changes are strictly within scope: introducing the helper function, refactoring existing decoders to use it, adding the completeness test, and documenting the decision in ADR 0021. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 98.08% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/projector-decodepayload-dry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@PaulDotterer PaulDotterer merged commit 62f1f14 into main Jun 15, 2026
9 checks passed
@PaulDotterer PaulDotterer deleted the refactor/projector-decodepayload-dry branch June 15, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(projectors): single-source decodePayload[T] helper + DRY completeness guard (WS16b)

1 participant