Skip to content

feat: SelfValidator for custom and cross-field validation#25

Open
mlwelles wants to merge 2 commits into
matthewmcneely:mainfrom
mlwelles:feature/self-validation
Open

feat: SelfValidator for custom and cross-field validation#25
mlwelles wants to merge 2 commits into
matthewmcneely:mainfrom
mlwelles:feature/self-validation

Conversation

@mlwelles

@mlwelles mlwelles commented Jun 4, 2026

Copy link
Copy Markdown

What this adds

SelfValidator, an opt-in seam that lets a type drive its own validation —
building on the existing StructValidator support rather than replacing it.

type SelfValidator interface {
    ValidateWith(ctx context.Context, v StructValidator) error
}

When a value passed to Insert/Upsert/Update implements SelfValidator,
the client calls ValidateWith instead of handing it straight to the configured
StructValidator. Plain structs are unaffected — they still flow through
StructCtx exactly as before.

The problem it solves

Struct tags validate one field at a time. Plenty of real validation lives
outside what a tag can express:

  • Cross-field rules — one field constrained by another (End >= Start,
    password == confirm).
  • Conditional rules — field B required only when field A has some value.
  • Computed / setter-derived values — state that is not a plain tagged field.
  • Business rules — anything that needs real logic.

SelfValidator is the seam for those. ValidateWith receives the configured
StructValidator, so an implementation can run ordinary tag-based validation
first and layer custom logic on top:

func (e *Event) ValidateWith(ctx context.Context, v mg.StructValidator) error {
    if v != nil {
        if err := v.StructCtx(ctx, e); err != nil { // tag-based checks first
            return err
        }
    }
    if e.End < e.Start { // then the cross-field rule
        return fmt.Errorf("End (%d) must be >= Start (%d)", e.End, e.Start)
    }
    return nil
}

validateStruct routes each element through a validateOne helper that detects
SelfValidator (on the value or its address) and otherwise falls back to
StructCtx — unchanged behavior for ordinary structs.

Tests

  • TestValidateRoutesToSelfValidator / TestValidateFallsBackToStructCtx
    routing and the unchanged fallback.
  • TestValidateSelfValidatorInSlice — slice elements route correctly, and the
    configured StructValidator is not also invoked (strengthened this round
    to assert the no-double-call, matching the scalar test).
  • TestSelfValidatorCustomCrossFieldRule — a concrete cross-field example.

The automated review (cubic) found no issues in the implementation.

Documentation

A runnable ExampleSelfValidator showing a cross-field rule that layers on the
configured StructValidator.

@mlwelles mlwelles requested a review from matthewmcneely as a code owner June 4, 2026 20:14

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Re-trigger cubic

Adds SelfValidator, an opt-in seam that lets a type drive its own
validation. When a value passed to Insert, Upsert, or Update implements
SelfValidator, the client calls ValidateWith instead of handing the value
straight to the configured StructValidator.

This covers validation that struct tags cannot express on their own:
cross-field rules (one field constrained by another), conditional rules,
checks on computed or setter-derived values, and broader business rules.
ValidateWith receives the configured StructValidator, so an implementation
can still run ordinary tag-based checks and layer custom logic on top.

validateStruct routes each element through a new validateOne helper that
detects SelfValidator (on the value or its address) and otherwise falls back
to StructCtx exactly as before — behavior is unchanged for ordinary structs.
@mlwelles mlwelles force-pushed the feature/self-validation branch from 501b3ef to 504231c Compare June 4, 2026 20:45
@mlwelles mlwelles changed the title feat: SelfValidator for private-field validation feat: SelfValidator for custom and cross-field validation Jun 4, 2026
…d example

- TestValidateSelfValidatorInSlice now asserts the configured StructValidator
  is never called for a SelfValidator slice element, matching the scalar test;
  a regression invoking both paths would otherwise pass silently.
- Add a runnable ExampleSelfValidator showing a cross-field rule that layers
  on the configured StructValidator.
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.

1 participant