Add oneof: option for polymorphic Hash parameters (#2385)#2702
Merged
Conversation
fcaf1a0 to
730a7f0
Compare
Danger ReportNo issues found. |
730a7f0 to
642d457
Compare
Member
|
I like |
dblock
approved these changes
May 8, 2026
e72373d to
b542249
Compare
Resolves #2385. A Hash parameter can now declare multiple acceptable shapes: params do requires :value, type: Hash, oneof: [ proc { requires :fixed_price, type: Float }, proc do requires :time_unit, type: String requires :rate, type: Float end ] end At request time variants are tried in declaration order against a deep-dup of the value; the first variant whose validators raise no exceptions wins, and any coercions it performed are applied to the parent params. Each variant block runs in a real `ParamsScope` backed by a small `OneofCollector` that captures validators locally instead of registering them on the API. Because variants are evaluated through the standard scope, the full params DSL is available inside — `regexp:`, `values:`, `allow_blank:`, nested `Hash`/`Array`, and so on all work without re-implementation. Variant validators are themselves real frozen `Validators::Base` instances reused at request time. Definition-time guards: `oneof:` requires `type: Hash`, the array must be non-empty, and every entry must be a `Proc`. Otherwise an `ArgumentError` is raised at boot. Adds `Grape::Validations::OneofCollector`, `Grape::Validations::Validators::OneofValidator`, an `oneof` i18n key, and `process_oneof!` in `ParamsScope#validates`. README and CHANGELOG updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
b542249 to
2438049
Compare
Member
|
Haven't heard from @jcagarcia or @mia-n - should we merge this @ericproulx? I'm good with it. |
Contributor
Author
I'm good with it. |
Member
|
Squash it. |
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.
Resolves #2385. Adds an
oneof:option forrequires/optionalso a Hash parameter can accept one of several variant schemas without theoptional + mutually_exclusive + givenworkaround the issue describes.API
Both
{"value":{"fixed_price":100.0}}and{"value":{"time_unit":"hour","rate":50.0}}succeed. A request that doesn't match any variant returns400withvalue does not match any of the allowed schemas.Each variant is a
Procthat uses the normal GrapeparamsDSL — sorequires,optional,regexp:,values:,allow_blank:, nestedHash/Array,Grape::API::Boolean, custom types, etc. all work inside variants without re-implementation. Coercion through the winning variant is applied to the parent params (e.g."150.5"→150.5).Variants are tried in declaration order; the first variant whose validators raise no exceptions wins (deterministic).
Why a separate
oneof:and nottypes: [hash_schema {...}]I considered overloading
types:per @dblock's note in the issue thread, but keptoneof:distinct because:types:already drivesMultipleTypeCoercerfor primitive variants (types: [Integer, String]).types: [Integer, hash_schema { ... }]— produces a non-obvious dispatch.oneof:aligns with JSON Schema vocabulary and makes the user's intent explicit.If the maintainers prefer the
types:overload, the validator and collector machinery in this PR are reusable — onlyinfer_coercion/process_oneof!would need to change.Implementation
lib/grape/validations/oneof_collector.rb@api-compatible shim. ExposesOneofCollector.collect(block)which evaluates the variant block in a freshParamsScopeand returns the captured validators.lib/grape/validations/validators/oneof_validator.rbOneofValidator < Base. At request time deep-dups the candidate hash, runs each variant's validators against the dup, takes the first variant that produces no exceptions, and replacesparams[attr_name]with the (possibly coerced) result. RaisesValidationwith i18n keyoneofif no variant matches.lib/grape/validations/params_scope.rbvalidatescallsprocess_oneof!when:oneofis in the validations hash; the helper validates shape (non-empty Array of Procs,type: Hash) and replaces each Proc with itsOneofCollector.collect-built validator list before the standardvalidateloop instantiatesOneofValidator.lib/grape/locale/en.ymloneof: 'does not match any of the allowed schemas'key.README.mdoneof" between "Multiple Allowed Types" and "Validation of Nested Parameters".CHANGELOG.mdDefinition-time guards
oneof:requirestype: Hash(raisesArgumentError)ArgumentError)Proc(raisesArgumentError)Tests
19 new examples in
spec/grape/validations/validators/oneof_validator_spec.rb:optional :valuewith missing/invalid (2 cases)values:,regexp:(4 cases)Hashinside variants (3 cases)Test plan
bundle exec rspec spec/grape/validations/validators/oneof_validator_spec.rb— 19 examples, 0 failuresbundle exec rspec— full suite green (2,273 examples, up from 2,254)bundle exec rubocop lib/ spec/— cleanCredits
Same feature requested as in #2661 by @jcagarcia and @mia-n. This PR re-implements from scratch using the existing
ParamsScopeinfrastructure (instead of a parallel mini-DSL) so all Grape validators work inside variants by design.🤖 Generated with Claude Code