Skip to content

Fix validation extension for operations taking no input - #46

Open
aaronmallen wants to merge 1 commit into
mainfrom
bugfix/validate-operation-call-signature
Open

Fix validation extension for operations taking no input#46
aaronmallen wants to merge 1 commit into
mainfrom
bugfix/validate-operation-call-signature

Conversation

@aaronmallen

Copy link
Copy Markdown
Member

The Validation extension wrapped step methods with |input = {}, *rest, **kwargs, &block| and always forwarded super(validated_input, ...). An operation defining def call with no arguments therefore raised ArgumentError: wrong number of arguments (given 1, expected 0) on call, even when no contract, params, or schema was defined.

Two changes:

  • With no contract, there is nothing to validate, so the wrapper now forwards the arguments it was called with untouched instead of manufacturing an empty hash.

  • With a contract defined for a method that cannot receive the input, we now raise Dry::Operation::ValidationInputError at class-definition time, whether the contract is declared before or after the method, or inherited by a subclass that defines it. An injected #contract dependency isn't visible then, so the same check also runs on call.

The signature reflection is extracted into a Signature module shared by both paths. It walks past the generic forwarders Dry::Operation prepends to find the method declaring the real signature, which also lets a no-argument call into a keyword-only method pass its validated input as kwargs rather than as a positional hash.

Note that def call(input) with no contract can no longer be invoked as operation.call; it raises ArgumentError, as the signature says it should.

The Validation extension wrapped step methods with `|input = {}, *rest, **kwargs, &block|`
and always forwarded `super(validated_input, ...)`. An operation defining `def call` with no
arguments therefore raised `ArgumentError: wrong number of arguments (given 1, expected 0)`
on call, even when no contract, params, or schema was defined.

Two changes:

- With no contract, there is nothing to validate, so the wrapper now forwards the arguments
  it was called with untouched instead of manufacturing an empty hash.

- With a contract defined for a method that cannot receive the input, we now raise
  `Dry::Operation::ValidationInputError` at class-definition time, whether the contract is
  declared before or after the method, or inherited by a subclass that defines it. An
  injected `#contract` dependency isn't visible then, so the same check also runs on call.

The signature reflection is extracted into a `Signature` module shared by both paths. It
walks past the generic forwarders `Dry::Operation` prepends to find the method declaring the
real signature, which also lets a no-argument call into a keyword-only method pass its
validated input as kwargs rather than as a positional hash.

Note that `def call(input)` with no contract can no longer be invoked as `operation.call`;
it raises `ArgumentError`, as the signature says it should.
@aaronmallen aaronmallen added the bug Something isn't working label Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant