`rigor-dry-validation` — slicing decision
Status: Design note. Authored 2026-05-17 in the
rigor-dry-types slice-4 commit. Decides the slice ordering for the
next dry-rb adapter beyond rigor-dry-types and rigor-dry-struct
(both landed in v0.1.6).
docs/design/20260509-dry-plugins-roadmap.md
§ “dry-validation” describes the gem’s three plugin-relevant DSL
surfaces; the user-facing programming shape is one of:
class NewUserContract < Dry::Validation::Contract params do # (1) params { ... } adapter — delegated to dry-schema required(:email).filled(:string) required(:age).value(:integer) end
rule(:email) do # (2) rule { ... } block — no type contribution key.failure('has invalid format') unless EMAIL_RE.match?(value) endend
result = contract.call(...) # (3) Contract#call → Dry::Validation::Resultresult.success? # narrows Resultresult.to_h # surfaces the typed params hashThe plugin gains user value when Rigor can answer:
- What is the type of
contract.call(...)? →Dry::Validation::Resultregardless of contract shape. - What is the typed shape of
result.to_hon success? → the dry-schema-derivedHashShape. Requires dry-schema. - What params keys exist on a given Contract?
→ set of
:email/:age/ … from theparams { ... }block (which is a dry-schema in disguise). Requires dry-schema. - What does
rule(:email)do for typing? → nothing; pure business rule.
Dependency ordering
Section titled “Dependency ordering” ┌──── rigor-dry-types (v0.1.6 — slices 1+2+3+4) │rigor-dry-schema (NOT YET) ←── consumes :dry_type_aliases │ ↓rigor-dry-validation (NOT YET) ←── consumes dry-schema's params shaperigor-dry-validation standalone (no dry-schema) can ONLY
contribute the Contract#call → Result fact. The rich payload —
typed result.to_h, params keys, per-key types — flows from
dry-schema. Without dry-schema, dry-validation is a one-row RBS
contribution:
module Dry module Validation class Contract def call: (Hash[Symbol, untyped]) -> Result end
class Result def success?: () -> bool def failure?: () -> bool def to_h: () -> Hash[Symbol, untyped] end endendThat’s ten lines of RBS overlay. Not worth a dedicated plugin slice — fold into a future “dry-rb core RBS bundle” alongside similar boundaries.
Decision: slice rigor-dry-schema BEFORE rigor-dry-validation.
The validation plugin without schema awareness contributes very
little; with it, the value scales with schema usage in user code.
rigor-dry-schema minimum-viable shape
Section titled “rigor-dry-schema minimum-viable shape”Per the dry-plugins roadmap § “dry-schema” entry:
NewUserSchema = Dry::Schema.Params do required(:email).filled(:string) required(:age).value(:integer)end
result = NewUserSchema.call(input)result.to_h # => HashShape[{email: String, age: Integer}]result.errors.to_h # => Hash[Symbol, Array[String]]Plugin contract (proposed):
- Recognise
Foo = Dry::Schema.{Params,JSON,define} { ... }at the project’s top level OR as a class-level constant. - Walk the block body for
required(:key).<predicates>andoptional(:key).<predicates>calls. - Map each predicate suffix (
filled(:string),value(:integer),value(:date)…) to the underlying class via the same CANONICAL_ALIASES tablerigor-dry-typesuses (:string→String,:integer→Integer, …). Consume the:dry_type_aliasesfact for any user-authored references (value(Types::Email)resolves through the cross-plugin fact channel). - Publish a
:dry_schema_tablefact:{schema_const_fqn => {required: {key => underlying_class}, optional: {...}}}. - Synthesise typed return for
result.to_hvia either the ADR-16 substrate (Tier CHeredocTemplatewithreturns_from_arg:consuming:dry_schema_table) or a bespoke walker if substrate parameterised returns are out of scope at the time.
Slice 1 of rigor-dry-schema floor: recognition + fact
publication (no diagnostics yet), mirroring rigor-dry-types
slice 1’s shape.
rigor-dry-validation slicing — proposed three slices
Section titled “rigor-dry-validation slicing — proposed three slices”Once rigor-dry-schema provides the underlying shape, the
validation plugin maps cleanly onto the substrate.
Slice 1 — Contract recognition + Result carrier
Section titled “Slice 1 — Contract recognition + Result carrier”- Walk the project for
class X < Dry::Validation::Contractsubclasses. - Synthesise
X#call(Hash[Symbol, untyped]) → Result(a genericResult, no schema awareness yet). - Hand-authored RBS overlay for
Dry::Validation::Result#{success?, failure?, to_h}so the chaincontract.call(...).to_hresolves toHash[Symbol, untyped].
Floor: every contract call site has a typed Result receiver
for downstream method-chain inference.
Slice 2 — params { ... } integration with dry-schema
Section titled “Slice 2 — params { ... } integration with dry-schema”- Recognise the
params do ... endblock inside a contract body. - Treat it as a dry-schema declaration (delegate to
rigor-dry-schema’s walker, or duplicate the relevant subset if the plugin coupling is a problem). - Publish a
:dry_validation_paramsfact:{contract_const_fqn => HashShape}. - Refine
Contract#call’s return soresult.to_htyped against the per-contract shape rather thanuntypedvalues.
Floor: NewUserContract.new.call(email: "x@y", age: 17).to_h
resolves to HashShape[{email: String, age: Integer}].
Slice 3 — json { ... } adapter parity
Section titled “Slice 3 — json { ... } adapter parity”The json { ... } block has the same shape as params but
applies stricter type expectations (no string-to-int coercion).
Apply the same walker; emit the same fact under a different
key (:dry_validation_json or shared :dry_validation_schema
with a kind: discriminator).
Floor: parity with params. Demand-driven if no project uses
json { ... }.
ADR amendments needed (if any)
Section titled “ADR amendments needed (if any)”None for the slicing above. dry-validation does NOT need the
Result[T, E] carrier amendment that rigor-dry-monads does
(see § “Open observation” below) — Dry::Validation::Result
is a generic class, not a sum type. Its #to_h payload IS the
typed shape, and the #success? / #failure? predicates
narrow downstream chains via the existing bool flow facts.
Open observation — rigor-dry-monads is separately blocked
Section titled “Open observation — rigor-dry-monads is separately blocked”The roadmap groups rigor-dry-validation with rigor-dry-monads
because both are next-tier dry-rb plugins. But dry-monads is
blocked on a different axis: it wants per-method return-type
wrapping (def x; Success(42); end → Result[Integer, untyped]).
The wrapped Result[T, E] / Maybe[T] carriers do not exist in
the Rigor::Type::* hierarchy today.
Two routes:
- (a) Implement
Result[T, E]/Maybe[T]carriers as newRigor::Type::*value classes. ADR-3 amendment level work (new type kinds, normalization rules, RBS erasure, display contract, equality / certainty surfaces). - (b) Express
Result[T, E]asUnion[T, E]andMaybe[T]asUnion[T, NilClass]. Loses the “tag” disambiguation that makesSuccess(v)vsFailure(e)precise but might be workable as a floor.
Decision: defer dry-monads until at least one of the two routes becomes concrete. dry-validation can ship without monads — the dependency goes the other direction (dry-validation uses dry-types
- dry-schema, not dry-monads).
Bottom line
Section titled “Bottom line”Order of work (queued, demand-driven):
rigor-dry-schemaslice 1 (recognition + fact publication)rigor-dry-schemaslice 2+ (per-schema-shape synthesis)rigor-dry-validationslice 1 (Contract recognition +Resultcarrier)rigor-dry-validationslice 2 (params dry-schema integration)rigor-dry-validationslice 3 (json adapter parity)rigor-dry-monads— only after (a) or (b) resolves theResult/Maybecarrier question
Total: 5-6 small-medium slices. Concrete user demand for any specific layer would justify pulling it forward.
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.