Skip to content

Commit

Permalink
Support receiving dry-validation contracts directly
Browse files Browse the repository at this point in the history
  • Loading branch information
timriley committed Sep 3, 2024
1 parent 951f8cc commit 9f6ab88
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 109 deletions.
7 changes: 5 additions & 2 deletions lib/hanami/action.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def self.gem_loader
loader.ignore(
"#{root}/hanami-controller.rb",
"#{root}/hanami/controller/version.rb",
"#{root}/hanami/action/{constants,errors,params,validatable}.rb"
"#{root}/hanami/action/{constants,errors,validatable}.rb"
)
loader.inflector.inflect("csrf_protection" => "CSRFProtection")
end
Expand Down Expand Up @@ -72,6 +72,7 @@ def self.gem_loader
setting :public_directory, default: Config::DEFAULT_PUBLIC_DIRECTORY
setting :before_callbacks, default: Utils::Callbacks::Chain.new, mutable: true
setting :after_callbacks, default: Utils::Callbacks::Chain.new, mutable: true
setting :contract_class

# @!scope class

Expand Down Expand Up @@ -316,7 +317,9 @@ def call(env)
response = nil

halted = catch :halt do
params = self.class.params_class.new(env)
# TODO: call contract_class.new at #initialize and capture it as an ivar (better perf for
# long-lived actions)
params = Params.new(env: env, validator: config.contract_class&.new)
request = build_request(
env: env,
params: params,
Expand Down
37 changes: 22 additions & 15 deletions lib/hanami/action/params.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "hanami/utils/hash"

module Hanami
class Action
# A set of params requested by the client
Expand Down Expand Up @@ -103,6 +105,11 @@ def _nested_attribute(keys, key)
end
end

# @api private
# @since 2.2.0
DEFAULT_VALIDATOR = Utils::Hash.method(:deep_symbolize)
private_constant :DEFAULT_VALIDATOR

# Defines validations for the params, using the `params` schema of a dry-validation contract.
#
# @param block [Proc] the schema definition
Expand All @@ -112,19 +119,14 @@ def _nested_attribute(keys, key)
# @api public
# @since 0.7.0
def self.params(&block)
@_validator = Class.new(Dry::Validation::Contract) { params(&block || -> {}) }.new
end
# TODO: add tests for this case
unless defined?(Dry::Validation::Contract)
message = %(To use `.params`, please add the "hanami-validations" gem to your Gemfile)
raise NoMethodError, message
end

# Defines validations for the params, using a dry-validation contract.
#
# @param block [Proc] the contract definition
#
# @see https://dry-rb.org/gems/dry-validation/
#
# @api public
# @since 2.2.0
def self.contract(&block)
@_validator = Class.new(Dry::Validation::Contract, &block).new
# TODO: deprecation warning
@_validator = Class.new(Dry::Validation::Contract) { params(&block || -> {}) }.new
end

class << self
Expand All @@ -143,13 +145,18 @@ class << self
#
# @since 0.1.0
# @api private
def initialize(env)
def initialize(env:, validator: nil)
@env = env
@raw = _extract_params

validation = self.class._validator.call(raw)
# Fall back to the default validator here rather than in `._validator` itself. This allows
# `._validator` to return nil when there is no user-defined validator, which is important
# for the backwards compatibility behavior in `Validatable::ClassMethods#params`.
validator ||= self.class._validator || DEFAULT_VALIDATOR
validation = validator.call(raw)

@params = validation.to_h
@errors = Errors.new(validation.errors.to_h)
@errors = Errors.new(validation.respond_to?(:errors) ? validation.errors.to_h : {})

freeze
end
Expand Down
25 changes: 13 additions & 12 deletions lib/hanami/action/validatable.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# frozen_string_literal: true

require_relative "params"

module Hanami
class Action
# Support for validating params when calling actions.
Expand Down Expand Up @@ -98,12 +96,18 @@ module ClassMethods
# @api public
# @since 0.3.0
def params(klass = nil, &block)
if klass.nil?
klass = const_set(PARAMS_CLASS_NAME, Class.new(Params))
klass.params(&block)
end
contract_class =
if klass.nil?
Class.new(Dry::Validation::Contract) { params(&block) }
elsif klass < Params
# Handle deprecated behavior of providing custom Hanami::Action::Params subclasses.
# TODO: deprecation warning here
klass._validator.class
else
klass
end

@params_class = klass
config.contract_class = contract_class
end

# Defines a validation contract for the params passed to {Hanami::Action#call}.
Expand Down Expand Up @@ -187,12 +191,9 @@ def params(klass = nil, &block)
# @api public
# @since 2.2.0
def contract(klass = nil, &block)
if klass.nil?
klass = const_set(PARAMS_CLASS_NAME, Class.new(Params))
klass.contract(&block)
end
contract_class = klass || Class.new(Dry::Validation::Contract, &block)

@params_class = klass
config.contract_class = contract_class
end
end
end
Expand Down
15 changes: 0 additions & 15 deletions spec/isolation/without_hanami_validations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
expect(defined?(Hanami::Action::Validatable)).to be(nil)
end

it "doesn't load Hanami::Action::Params" do
expect(defined?(Hanami::Action::Params)).to be(nil)
end

it "doesn't have params DSL" do
expect do
Class.new(Hanami::Action) do
Expand Down Expand Up @@ -53,17 +49,6 @@ def handle(req, res)
response = action.new.call({})
expect(response.body).to eq(["[true, true]"])
end

it "has params that don't respond to .errors" do
action = Class.new(Hanami::Action) do
def handle(req, res)
res.body = req.params.respond_to?(:errors)
end
end

response = action.new.call({})
expect(response.body).to eq(["false"])
end
end

RSpec::Support::Runner.run
20 changes: 9 additions & 11 deletions spec/support/fixtures.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1930,23 +1930,21 @@ def handle(request, response)
end
end

class ExternalContractParams < Hanami::Action::Params
contract do
params do
required(:birth_date).filled(:date)
required(:book).schema do
required(:title).filled(:str?)
end
class ExternalContract < Dry::Validation::Contract
params do
required(:birth_date).filled(:date)
required(:book).schema do
required(:title).filled(:str?)
end
end

rule(:birth_date) do
key.failure("you must be 18 years or older") if value < Date.today << (12 * 18)
end
rule(:birth_date) do
key.failure("you must be 18 years or older") if value < Date.today << (12 * 18)
end
end

class ExternalContractAction < ContractAction
contract ExternalContractParams
contract ExternalContract
end

class WhitelistedUploadDslContractAction < Hanami::Action
Expand Down
22 changes: 1 addition & 21 deletions spec/unit/hanami/action/contract_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
end
end

describe "provided by a standlone params class using a contract" do
describe "provided by a standlone contract class" do
let(:action) { ExternalContractAction.new }

context "when it has errors" do
Expand All @@ -47,26 +47,6 @@
end
end

describe "standalone class" do
it "validates the input" do
params_class = Class.new(Hanami::Action::Params) {
contract do
params do
required(:start_date).value(:date)
end

rule(:start_date) do
key.failure("must be in the future") if value <= Date.today
end
end
}

params = params_class.new(start_date: "2000-01-01")

expect(params.errors.to_h).to eq(start_date: ["must be in the future"])
end
end

describe "#raw" do
context "without a contract" do
let(:action) { RawContractAction.new }
Expand Down
Loading

0 comments on commit 9f6ab88

Please sign in to comment.