Add reusable field groups: Permittable.fields and use - #16
Open
VSN2015 wants to merge 1 commit into
Open
Conversation
VSN2015
commented
Sep 5, 2026
| # overriding one field of a group is deliberate: `use G, except: [:city]` | ||
| # and then declare `:city` yourself. | ||
| def use(group, only: nil, except: nil, optional: false) | ||
| fields = select_group_fields!(group_fields!(group), only: only, except: except) |
Owner
Author
There was a problem hiding this comment.
Relaxing only the top-level spliced fields via optional: true is an intuitive design choice: it makes use BaseFields, optional: true directly usable for PATCH endpoints without inadvertently making nested sub-fields optional when provided.
| end | ||
|
|
||
| def assert_group_names!(fields, names, label) | ||
| wanted = Array(names).map(&:to_sym) |
Owner
Author
There was a problem hiding this comment.
Validating that only: and except: names match declared fields at class load time prevents silent typos from inadvertently dropping contract fields.
A growing API produces two kinds of duplication the DSL had no answer
for: the address block three controllers want, and the update contract
that is the create contract with nothing mandatory. Both were
copy-paste, and copy-paste in a contract is how validation and
documentation drift apart.
Permittable.fields { ... } builds a FieldGroup — a frozen, reusable
field list, the same data a contract's fields already are — and the
builder's `use` verb splices one in at the point of use, in the group's
own order, exactly as if those fields had been typed there. Everything
downstream is therefore unchanged by construction: request-time
behaviour, the drift guard, sensitive: registration, the exported
schema. It works at the top level of a contract, inside a nested or
array block, and inside another group, so groups compose.
`use G, optional: true` relaxes every spliced field, which is the
create-to-update answer: types, bounds and default: intact, nothing
mandatory. It relaxes the top level only — if a client sends an address
at all, the address's own required sub-fields still hold, which is what
a PATCH actually means.
Because a group is built by the same builder a contract is, its
declarations are validated when the GROUP is defined: a typo fails
once, at the group, instead of at every contract using it. Two more
mistakes fail at class load rather than silently — only:/except:
naming a field the group doesn't declare (so a typo cannot quietly drop
a field), and a field declared twice, which is what makes overriding
one field of a group deliberate rather than positional.
A group is deliberately not a contract: no root:, unknown:, model: or
mode:, and finalize is rejected, because those describe the request
being validated rather than a set of fields. Permittable::Contract now
answers #fields, though, so `use SomeContract` lets a webhook payload
and a controller action share one definition instead of two that
drift.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VSN2015
force-pushed
the
feature/field-groups
branch
from
September 11, 2026 22:01
6480a65 to
42a1aca
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The duplication
A growing API produces two kinds the DSL had no answer for:
addressblock three controllers want.updatecontract that is thecreatecontract with nothing mandatory.Both were copy-paste — and copy-paste in a contract is precisely how validation and documentation drift apart. It's also the one thing
docs/comparison.mdconcedes to dry-validation ("reusable schema fragments shared across services").The fix
A
FieldGroupis a frozen, reusable field list — the same data a contract's fields already are.usesplices it in at the point of use, in the group's own order, exactly as if those fields had been typed there, so everything downstream is unchanged by construction: request-time behaviour, the drift guard,sensitive:registration, the exported schema. It works at the top level of a contract, inside a nested or array block, and inside another group.optional: trueaddressat all, the address's own required sub-fields still hold, which is what aPATCHactually means. Types, bounds anddefault:untouchedonly:/except:Mistakes fail at class load, not silently
only:/except:naming a field the group doesn't declare raises, naming what the group does declare. A typo cannot quietly drop a field.What a group deliberately is not
A contract. No
root:,unknown:,model:ormode:— those describe the request being validated, not a set of fields — andfinalizeis rejected for the same reason.But
Permittable::Contractnow answers#fields, souse SomeContractlets a webhook payload and a controller action share one definition instead of two that drift.Verification
spec/field_group_spec.rb, written before the implementation)