diff --git a/lib/hanami/action/params.rb b/lib/hanami/action/params.rb index 2e1dfe83..2130b54f 100644 --- a/lib/hanami/action/params.rb +++ b/lib/hanami/action/params.rb @@ -148,6 +148,35 @@ def self.params(&blk) validations(&blk || -> {}) end + # Define a validation rule. + # + # @param blk [Proc] the rule definition + # + # @since 2.x.x + # + # @example + # class Event < Hanami::Action + # params do + # required(:start_date).value(:date) + # required(:end_date).value(:date) + # end + # + # # Rules must be defined after the params schema. + # rule(:start_date, :end_date) do + # if start_date > end_date + # base.failure('event cannot end before it starts') + # end + # end + # + # def handle(req, *) + # halt 400 unless req.params.valid? + # # ... + # end + # end + def self.rule(...) + _validator.class.rule(...) + end + # Initialize the params and freeze them. # # @param env [Hash] a Rack env or an hash of params. diff --git a/lib/hanami/action/validatable.rb b/lib/hanami/action/validatable.rb index 6eaceb64..43436dc0 100644 --- a/lib/hanami/action/validatable.rb +++ b/lib/hanami/action/validatable.rb @@ -105,6 +105,40 @@ def params(klass = nil, &blk) @params_class = klass end + + # Define a validation rule. + # + # @param blk [Proc] the rule definition + # + # @since 2.x.x + # + # @example + # class Event < Hanami::Action + # params do + # required(:start_date).value(:date) + # required(:end_date).value(:date) + # end + # + # # Rules must be defined after the params schema. + # rule(:start_date, :end_date) do + # if start_date > end_date + # base.failure('event cannot end before it starts') + # end + # end + # + # def handle(req, *) + # halt 400 unless req.params.valid? + # # ... + # end + # end + def rule(...) + unless @params_class + raise ArgumentError.new( + "The params schema must be defined prior to adding rules." + ) + end + @params_class.class_eval { rule(...) } + end end end end diff --git a/spec/unit/hanami/action/rules_spec.rb b/spec/unit/hanami/action/rules_spec.rb new file mode 100644 index 00000000..49fcc7cd --- /dev/null +++ b/spec/unit/hanami/action/rules_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require "rack" + +RSpec.describe Hanami::Action do + describe ".params" do + let(:klass) do + Class.new(described_class) do + params do + required(:book).schema do + required(:code).filled(:str?) + end + end + + def handle(request, response) + response[:params] = request.params + end + end + end + + let(:action) { klass.new } + let(:response) { action.call(given_input) } + + context "given valid input" do + let(:given_input) { {book: {code: "abc"}} } + + it "is valid" do + expect(response[:params].valid?).to eq(true) + end + end + + context "given invalid input" do + let(:given_input) { {book: {code: nil}} } + + it "is not valid" do + expect(response[:params].valid?).to eq(false) + end + end + end + + describe ".rule" do + let(:klass) do + Class.new(described_class) do + params do + required(:book).schema do + required(:code).filled(:str?) + end + end + + rule("book.code") do + key.failure('must be "abc"') unless value == "abc" + end + + def handle(request, response) + response[:params] = request.params + end + end + end + + let(:action) { klass.new } + let(:response) { action.call(given_input) } + + context "given valid input" do + let(:given_input) { {book: {code: "abc"}} } + + it "is valid" do + expect(response[:params].valid?).to eq(true) + end + end + + context "given invalid input" do + let(:given_input) { {book: {code: nil}} } + + it "is not valid" do + expect(response[:params].valid?).to eq(false) + end + end + + context "given input which does not pass rules" do + let(:given_input) { {book: {code: "xyz"}} } + + it "is not valid" do + expect(response[:params].valid?).to eq(false) + end + end + end +end