Skip to content

feat(binder)!: allow overriding structural error codes and messages - #186

Merged
Reefact merged 5 commits into
mainfrom
claude/issue-149-status-c0j81v
Jul 18, 2026
Merged

feat(binder)!: allow overriding structural error codes and messages#186
Reefact merged 5 commits into
mainfrom
claude/issue-149-status-c0j81v

Conversation

@Reefact

@Reefact Reefact commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

The request binder's two structural errors (REQUEST_ARGUMENT_REQUIRED / REQUEST_ARGUMENT_INVALID) had hardcoded English public messages with no override or localization hook (#149). Each structural error is now a BinderErrorDefinition — a code bundled with a message builder evaluated at emission — configured on RequestBinderOptions. This replaces the code-only override shipped by ADR-0016 (#147): a code and its message are one coherent unit, never independently overridable, so a consumer can't strand a code from a message that no longer matches it.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring
  • Analyzer / diagnostic change
  • Tests
  • Documentation

Changes

  • Add BindingMessage (short + optional detailed public message carrier) and BinderErrorDefinition (code + message builder, immutable, WithCode / WithMessage).
  • RequestBindingError exposes DefaultArgumentRequired / DefaultArgumentInvalid definitions (default codes, English messages); DefaultArgumentRequiredCode / DefaultArgumentInvalidCode remain for symbolic branching.
  • RequestBinderOptions now carries ArgumentRequired / ArgumentInvalid (BinderErrorDefinition), replacing the flat ArgumentRequiredCode / ArgumentInvalidCode (ErrorCode) properties ADR-0016 shipped.
  • The message builder runs when the error is raised, not when the options are built, so it can read the ambient culture and localize per request; the diagnostic message stays English by convention (unchanged library-wide rule).
  • Migrated all callers (RequestBinder, NestedFailure, the complex-property converters) to the new definitions; rebased onto main after PR Binder: factor out the duplicated missing / element-loop / wrapping logic across converters #150's converter refactor and resolved the resulting conflict against the new shared ConvertEachElement primitive.
  • Tests: default rendering preserved, message-only override, code+message override together, per-emission localization, and custom options that don't touch the definitions still keep the shipped defaults — covering both the required and the invalid path.

Testing

  • dotnet build FirstClassErrors.sln
  • dotnet test FirstClassErrors.sln
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests)

All green: 0 warnings/errors under the solution's total TreatWarningsAsErrors ratchet; 1014 tests passed, 0 failed, across all 8 test projects.

Documentation

  • Public API / error documentation updated
  • README / doc/ updated
  • French translation (doc/handwritten/for-users/README.fr.md) updated if user-facing behavior changed
  • No documentation change required

doc/handwritten/for-users/RequestBinder.en.md and its French counterpart RequestBinder.fr.md were updated together (the structural-errors section and the app-default example). The top-level README.fr.md doesn't describe binder internals, so it wasn't touched.

Architecture decisions

  • No architectural decision in this pull request
  • Supersedes an existing ADR — successor proposed, status not flipped: ADR-0018 supersedes ADR-0016

ADR-0018 was drafted as Proposed, then set to Accepted (and ADR-0016 to Superseded, cross-linked) at @Reefact's explicit direction during this session — not flipped unilaterally by the agent.

Related issues

Closes #149


Generated by Claude Code

claude added 5 commits July 18, 2026 16:37
The binder's two structural errors (REQUEST_ARGUMENT_REQUIRED /
REQUEST_ARGUMENT_INVALID) had hardcoded English public messages with no
override or localization hook, breaking the library's own i18n story at the
primary-adapter boundary.

Introduce BinderErrorDefinition — a code bundled with its public-message
builder (Func<string, BindingMessage>), immutable, derivable with WithCode /
WithMessage — and a BindingMessage carrier (short + optional detailed). Carry
one definition per structural error on RequestBinderOptions, so a code and its
message are configured as a single coherent unit and can never drift apart.

The message builder runs at error emission, so it may read the ambient culture
and return a message localized per request. Only the two public messages are
localizable; the diagnostic message stays English by convention. Options that
do not override a definition keep the shipped default (code and messages), and
the default definitions are exposed for symbolic branching.

BREAKING CHANGE: RequestBinderOptions no longer takes argumentRequiredCode /
argumentInvalidCode (ErrorCode); it takes argumentRequired / argumentInvalid
(BinderErrorDefinition). The ArgumentRequiredCode / ArgumentInvalidCode
properties are replaced by ArgumentRequired / ArgumentInvalid. The
RequestBindingError.DefaultArgumentRequiredCode / DefaultArgumentInvalidCode
accessors remain for symbolic branching. Acceptable at v0.1.0-preview.1 with
no external consumers.

Refs: #149

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sD2irTr5PBVpxHt2dkRzQ
Add the <returns> tag to BinderErrorDefinition.WithCode / WithMessage, for
consistency with GetMessage and the sibling PublicMessageStage.WithPublicMessage.

Refs: #149

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sD2irTr5PBVpxHt2dkRzQ
The invalid factory has its own message-wiring path; exercise it end-to-end —
override the invalid message (keeping the default code) and a full code+message
definition — and assert the raised REQUEST_ARGUMENT_INVALID carries the custom
text while still wrapping the converter's cause.

Refs: #149

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sD2irTr5PBVpxHt2dkRzQ
Record the decision to configure each structural error as a code bundled with a
per-emission message builder, superseding the code-only override of ADR-0016 and
making the public messages localizable per request. Drafted Proposed (EN + FR)
and indexed; ADR-0016's status is left for the maintainer to decide on acceptance.

Refs: #149

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sD2irTr5PBVpxHt2dkRzQ
Per the maintainer's decision: ADR-0018 (bundle the binder's structural
error code and messages) is Accepted; ADR-0016 (the code-only override it
replaces) is marked Superseded, linked to its successor. Also correct
ADR-0018's French version, whose section headers and metadata labels had
been left in English instead of following the established French ADR
convention (Statut/Décideurs/Contexte/Décision/Justification/...).

Refs: #149

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sD2irTr5PBVpxHt2dkRzQ
@Reefact
Reefact merged commit d21db0b into main Jul 18, 2026
13 checks passed
@Reefact
Reefact deleted the claude/issue-149-status-c0j81v branch July 18, 2026 20:09
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.

Binder: public error messages are hardcoded in English with no i18n extension point

2 participants