├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── explicit-parameters.gemspec ├── gemfiles ├── Gemfile.rails-6.0 └── Gemfile.rails-6.0.lock ├── lib ├── explicit-parameters.rb ├── explicit_parameters.rb └── explicit_parameters │ ├── controller.rb │ ├── parameters.rb │ ├── railtie.rb │ └── version.rb └── spec ├── controller_spec.rb ├── parameters_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | cache: bundler 3 | rvm: 4 | 2.6.4 5 | gemfile: 6 | - gemfiles/Gemfile.rails-6.0 7 | before_install: 8 | - gem install bundler -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in explicit_parameters.gemspec 4 | gemspec 5 | 6 | gem 'byebug' 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jean Boussier 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExplicitParameters 2 | 3 | [![Build Status](https://secure.travis-ci.org/byroot/explicit-parameters.png)](http://travis-ci.org/byroot/explicit-parameters) 4 | [![Gem Version](https://badge.fury.io/rb/explicit-parameters.png)](http://badge.fury.io/rb/explicit-parameters) 5 | 6 | 7 | Explicit parameters validation and casting for Rails APIs. 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'explicit-parameters' 15 | ``` 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | ## Usage 22 | 23 | Example: 24 | 25 | ```ruby 26 | class DummyController < ApiController 27 | params do 28 | requires :search, String 29 | accepts :limit, Integer, default: 30 30 | 31 | validates :limit, :numericality: {greater_than: 0, less_than_or_equal_to: 100} 32 | end 33 | def index 34 | Dummy.search(params.search).limit(params.limit) 35 | end 36 | end 37 | ``` 38 | 39 | ## TODO 40 | 41 | - Real README 42 | 43 | ## Contributing 44 | 45 | 1. Fork it ( https://github.com/byroot/explicit_parameters/fork ) 46 | 2. Create your feature branch (`git checkout -b my-new-feature`) 47 | 3. Commit your changes (`git commit -am 'Add some feature'`) 48 | 4. Push to the branch (`git push origin my-new-feature`) 49 | 5. Create a new Pull Request 50 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | require 'rspec/core/rake_task' 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task default: :spec 8 | 9 | namespace :spec do 10 | task :all do 11 | %w(4.2 5.0).each do |rails_version| 12 | command = %W{ 13 | BUNDLE_GEMFILE=gemfiles/Gemfile.rails-#{rails_version} 14 | rspec 15 | }.join(' ') 16 | puts command 17 | system(command) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /explicit-parameters.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'explicit_parameters/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'explicit-parameters' 8 | spec.version = ExplicitParameters::VERSION 9 | spec.authors = ['Jean Boussier'] 10 | spec.email = ['jean.boussier@gmail.com'] 11 | spec.summary = %q{Explicit parameters validation and casting for Rails APIs} 12 | spec.homepage = 'https://github.com/byroot/explicit-parameters' 13 | spec.license = 'MIT' 14 | 15 | spec.files = `git ls-files -z`.split(?\0) 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ['lib'] 19 | 20 | spec.add_dependency 'actionpack', '>= 6.0' 21 | spec.add_dependency 'activemodel', '>= 6.0' 22 | spec.add_dependency 'virtus', '~> 1.0' 23 | 24 | spec.add_development_dependency 'bundler' 25 | spec.add_development_dependency 'rake', '~> 10.0' 26 | spec.add_development_dependency 'rspec', '~> 3.0' 27 | end 28 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-6.0: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'actionpack', '~> 5.1' 4 | gem 'activemodel', '~> 5.1' 5 | gem 'virtus', '~> 1.0' 6 | gem 'rake', '~> 10.0' 7 | gem 'rspec', '~> 3.0' 8 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-6.0.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionpack (5.2.3) 5 | actionview (= 5.2.3) 6 | activesupport (= 5.2.3) 7 | rack (~> 2.0) 8 | rack-test (>= 0.6.3) 9 | rails-dom-testing (~> 2.0) 10 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 11 | actionview (5.2.3) 12 | activesupport (= 5.2.3) 13 | builder (~> 3.1) 14 | erubi (~> 1.4) 15 | rails-dom-testing (~> 2.0) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 17 | activemodel (5.2.3) 18 | activesupport (= 5.2.3) 19 | activesupport (5.2.3) 20 | concurrent-ruby (~> 1.0, >= 1.0.2) 21 | i18n (>= 0.7, < 2) 22 | minitest (~> 5.1) 23 | tzinfo (~> 1.1) 24 | axiom-types (0.1.1) 25 | descendants_tracker (~> 0.0.4) 26 | ice_nine (~> 0.11.0) 27 | thread_safe (~> 0.3, >= 0.3.1) 28 | builder (3.2.3) 29 | coercible (1.0.0) 30 | descendants_tracker (~> 0.0.1) 31 | concurrent-ruby (1.1.5) 32 | crass (1.0.4) 33 | descendants_tracker (0.0.4) 34 | thread_safe (~> 0.3, >= 0.3.1) 35 | diff-lcs (1.3) 36 | equalizer (0.0.11) 37 | erubi (1.8.0) 38 | i18n (1.6.0) 39 | concurrent-ruby (~> 1.0) 40 | ice_nine (0.11.2) 41 | loofah (2.2.3) 42 | crass (~> 1.0.2) 43 | nokogiri (>= 1.5.9) 44 | mini_portile2 (2.4.0) 45 | minitest (5.11.3) 46 | nokogiri (1.10.4) 47 | mini_portile2 (~> 2.4.0) 48 | rack (2.0.7) 49 | rack-test (1.1.0) 50 | rack (>= 1.0, < 3) 51 | rails-dom-testing (2.0.3) 52 | activesupport (>= 4.2.0) 53 | nokogiri (>= 1.6) 54 | rails-html-sanitizer (1.2.0) 55 | loofah (~> 2.2, >= 2.2.2) 56 | rake (10.5.0) 57 | rspec (3.8.0) 58 | rspec-core (~> 3.8.0) 59 | rspec-expectations (~> 3.8.0) 60 | rspec-mocks (~> 3.8.0) 61 | rspec-core (3.8.2) 62 | rspec-support (~> 3.8.0) 63 | rspec-expectations (3.8.4) 64 | diff-lcs (>= 1.2.0, < 2.0) 65 | rspec-support (~> 3.8.0) 66 | rspec-mocks (3.8.1) 67 | diff-lcs (>= 1.2.0, < 2.0) 68 | rspec-support (~> 3.8.0) 69 | rspec-support (3.8.2) 70 | thread_safe (0.3.6) 71 | tzinfo (1.2.5) 72 | thread_safe (~> 0.1) 73 | virtus (1.0.5) 74 | axiom-types (~> 0.1) 75 | coercible (~> 1.0) 76 | descendants_tracker (~> 0.0, >= 0.0.3) 77 | equalizer (~> 0.0, >= 0.0.9) 78 | 79 | PLATFORMS 80 | ruby 81 | 82 | DEPENDENCIES 83 | actionpack (~> 5.1) 84 | activemodel (~> 5.1) 85 | rake (~> 10.0) 86 | rspec (~> 3.0) 87 | virtus (~> 1.0) 88 | 89 | BUNDLED WITH 90 | 2.0.2 91 | -------------------------------------------------------------------------------- /lib/explicit-parameters.rb: -------------------------------------------------------------------------------- 1 | require 'explicit_parameters' 2 | -------------------------------------------------------------------------------- /lib/explicit_parameters.rb: -------------------------------------------------------------------------------- 1 | require 'action_pack/version' 2 | 3 | module ExplicitParameters 4 | IS_RAILS5 = ActionPack.version >= Gem::Version.new('5.0.0') 5 | BaseError = Class.new(StandardError) 6 | InvalidParameters = Class.new(BaseError) 7 | end 8 | 9 | require 'explicit_parameters/version' 10 | require 'explicit_parameters/parameters' 11 | require 'explicit_parameters/controller' 12 | 13 | require 'explicit_parameters/railtie' if defined? Rails 14 | -------------------------------------------------------------------------------- /lib/explicit_parameters/controller.rb: -------------------------------------------------------------------------------- 1 | module ExplicitParameters 2 | module Controller 3 | extend ActiveSupport::Concern 4 | 5 | Boolean = Axiom::Types::Boolean 6 | 7 | class << self 8 | attr_accessor :last_parameters 9 | end 10 | 11 | included do 12 | rescue_from ExplicitParameters::InvalidParameters, with: :render_parameters_error 13 | end 14 | 15 | module ClassMethods 16 | attr_accessor :parameters 17 | 18 | def method_added(action) 19 | return unless Controller.last_parameters 20 | self.parameters ||= {} 21 | parameters[action.to_s] = Controller.last_parameters 22 | const_set("#{action.to_s.camelize}Parameters", Controller.last_parameters) 23 | Controller.last_parameters = nil 24 | end 25 | 26 | def params(&block) 27 | Controller.last_parameters = ExplicitParameters::Parameters.define(&block) 28 | end 29 | 30 | def parse_parameters_for(action_name, params) 31 | if declaration = parameters.try!(:[], action_name) 32 | declaration.parse!(params) 33 | else 34 | params 35 | end 36 | end 37 | end 38 | 39 | def params 40 | @validated_params ||= self.class.parse_parameters_for(action_name, super) 41 | end 42 | 43 | private 44 | 45 | def param_error!(parameter, error) 46 | raise ExplicitParameters::InvalidParameters.new({errors: {parameter => [error]}}.to_json) 47 | end 48 | 49 | def render_param_error(parameter, error) 50 | render json: {errors: {parameter => [error]}}, status: :unprocessable_entity 51 | end 52 | 53 | def render_parameters_error(error) 54 | render json: error.message, status: :unprocessable_entity 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/explicit_parameters/parameters.rb: -------------------------------------------------------------------------------- 1 | require 'virtus' 2 | require 'active_model' 3 | 4 | module ExplicitParameters 5 | class Parameters 6 | include Virtus.model 7 | include ActiveModel::Validations 8 | include Enumerable 9 | 10 | class CoercionValidator < ActiveModel::EachValidator 11 | def validate_each(record, attribute, value) 12 | record.validate_attribute_coercion!(attribute, value) 13 | end 14 | end 15 | 16 | class RequiredValidator < ActiveModel::EachValidator 17 | def validate_each(record, attribute, value) 18 | record.validate_attribute_provided!(attribute, value) 19 | end 20 | end 21 | 22 | class << self 23 | def parse!(params) 24 | new(params).validate! 25 | end 26 | 27 | def define(name = nil, &block) 28 | name_class(Class.new(self, &block), name) 29 | end 30 | 31 | def requires(name, type = nil, options = {}, &block) 32 | accepts(name, type, options.merge(required: true), &block) 33 | end 34 | 35 | def accepts(name, type = nil, options = {}, &block) 36 | if block_given? 37 | subtype = define(name, &block) 38 | if type == Array 39 | type = Array[subtype] 40 | elsif type == nil 41 | type = subtype 42 | else 43 | raise ArgumentError, "`type` argument can only be `nil` or `Array` when a block is provided" 44 | end 45 | end 46 | attribute(name, type, options.slice(:default, :required)) 47 | validations = options.except(:default) 48 | validations[:coercion] = true 49 | validates(name, validations) 50 | end 51 | 52 | def optional_attributes 53 | @optional_attributes ||= [] 54 | end 55 | 56 | private 57 | 58 | def name_class(klass, name) 59 | if name.present? 60 | name = name.to_s.camelize 61 | klass.singleton_class.send(:define_method, :name) { name } 62 | end 63 | klass 64 | end 65 | end 66 | 67 | def initialize(attributes = {}) 68 | attributes = attributes.to_unsafe_h if IS_RAILS5 && attributes.respond_to?(:to_unsafe_h) 69 | @original_attributes = attributes.stringify_keys 70 | super(attributes) 71 | end 72 | 73 | def validate! 74 | raise InvalidParameters.new({errors: errors}.to_json) unless valid? 75 | self 76 | end 77 | 78 | def validate_attribute_provided!(attribute_name, value) 79 | if !@original_attributes.key?(attribute_name.to_s) 80 | errors.add attribute_name, 'is required' 81 | elsif attribute_set[attribute_name].type.primitive == Array && value == [].freeze 82 | errors.add attribute_name, 'is required' 83 | end 84 | end 85 | 86 | def validate_attribute_coercion!(attribute_name, value) 87 | return unless @original_attributes.key?(attribute_name.to_s) 88 | attribute = attribute_set[attribute_name] 89 | return if value.nil? && !attribute.required? 90 | return if attribute.value_coerced?(value) 91 | errors.add attribute_name, "#{@original_attributes[attribute_name].inspect} is not a valid #{attribute.type.name.demodulize}" 92 | end 93 | 94 | def to_hash 95 | super.except(*missing_attributes) 96 | end 97 | 98 | delegate :each, :each_pair, :empty?, :stringify_keys, :reject, :select, to: :to_hash 99 | delegate :[], to: :@original_attributes 100 | 101 | private 102 | 103 | def missing_attributes 104 | @missing_attributes ||= (attribute_set.map(&:name).map(&:to_s) - @original_attributes.keys).map(&:to_sym) 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/explicit_parameters/railtie.rb: -------------------------------------------------------------------------------- 1 | module ExplicitParameters 2 | class Railtie < ::Rails::Railtie 3 | initializer 'explicit_parameters.controller' do 4 | ActiveSupport.on_load(:action_controller) do 5 | include ExplicitParameters::Controller 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/explicit_parameters/version.rb: -------------------------------------------------------------------------------- 1 | module ExplicitParameters 2 | VERSION = '0.4.1' 3 | end 4 | -------------------------------------------------------------------------------- /spec/controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class DummyController < ActionController::Base 4 | include ExplicitParameters::Controller 5 | 6 | params do 7 | accepts :page_size, Integer 8 | accepts :published, Boolean, default: false 9 | end 10 | def index 11 | render json: {value: params.page_size, type: params.page_size.class.name} 12 | end 13 | 14 | def error 15 | param_error!(:page_size, "Out of bound") 16 | render body: 'OK' 17 | end 18 | 19 | def no_declaration 20 | render body: 'OK' 21 | end 22 | 23 | end 24 | 25 | RSpec.describe DummyController do 26 | it 'is optional' do 27 | get :no_declaration 28 | expect(response.code).to be == '200' 29 | end 30 | 31 | it 'coerce parameters to the required type' do 32 | get :index, page_size: '42' 33 | expect(json_response).to be == {value: 42, type: 'Integer'} 34 | end 35 | 36 | it 'returns a 422 if parameters are invalid' do 37 | get :index, page_size: 'foobar' 38 | expect(response.code).to be == '422' 39 | end 40 | 41 | it 'returns the list of errors if parameters are invalid' do 42 | get :index, page_size: 'foobar' 43 | expect(json_response).to be == {errors: {page_size: ['"foobar" is not a valid Integer']}} 44 | end 45 | 46 | it 'param_error! interupt the request immediately' do 47 | get :error 48 | expect(json_response).to be == {errors: {page_size: ['Out of bound']}} 49 | end 50 | 51 | private 52 | 53 | attr_reader :response 54 | 55 | def json_response 56 | JSON.load(response.body).deep_symbolize_keys 57 | end 58 | 59 | def get(action, parameters = {}) 60 | request(action, 'GET', query: parameters) 61 | end 62 | 63 | if ActionController::Base.instance_method(:dispatch).arity > 2 64 | def request(action, method, query: {}, body: '') 65 | request = ActionDispatch::Request.new( 66 | 'REQUEST_METHOD' => method, 67 | 'QUERY_STRING' => query.to_query, 68 | 'rack.input' => StringIO.new(body) 69 | ) 70 | @response = ActionDispatch::Response.create.tap do |res| 71 | res.request = request 72 | end 73 | subject.dispatch(action, request, @response) 74 | end 75 | else 76 | def request(action, method, query: {}, body: '') 77 | rack_response = subject.dispatch(action, ActionDispatch::Request.new( 78 | 'REQUEST_METHOD' => method, 79 | 'QUERY_STRING' => query.to_query, 80 | 'rack.input' => StringIO.new(body) 81 | )) 82 | @response = ActionDispatch::Response.new(*rack_response) 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/parameters_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ExplicitParameters::Parameters do 4 | let :definition do 5 | ExplicitParameters::Parameters.define(:test) do 6 | requires :id, Integer, numericality: {greater_than: 0} 7 | accepts :name, String 8 | accepts :title, String, default: 'Untitled' 9 | end 10 | end 11 | 12 | let :parameters do 13 | { 14 | 'id' => '42', 15 | 'name' => 'George', 16 | 'unexpected' => 'parameter', 17 | } 18 | end 19 | 20 | let(:params) { definition.parse!(parameters.with_indifferent_access) } 21 | 22 | it 'casts parameters to the declared type' do 23 | expect(params.id).to be == 42 24 | end 25 | 26 | it 'provides the default if the parameter is missing' do 27 | expect(params.title).to be == 'Untitled' 28 | end 29 | 30 | it 'ignores unexpected parameters during iteration' do 31 | params.each do |name, value| 32 | expect(name).to_not be == 'unexpected' 33 | end 34 | end 35 | 36 | it 'ignores unexpected parameters when converted to hash' do 37 | expect(params.to_hash.keys).to be == %i(id name) 38 | end 39 | 40 | it 'ignores unexpected parameters when converted to hash with string keys' do 41 | expect(params.stringify_keys.keys).to be == %w(id name) 42 | end 43 | 44 | it '#reject returns a hash' do 45 | expect(params.reject { false }).to be == {id: 42, name: 'George'} 46 | end 47 | 48 | it 'allows access to raw parameters when accessed like a Hash' do 49 | expect(params[:id]).to be == '42' 50 | expect(params[:unexpected]).to be == 'parameter' 51 | end 52 | 53 | it 'can perform any type of active model validations' do 54 | message = {errors: {id: ['must be greater than 0']}}.to_json 55 | expect { 56 | definition.parse!('id' => -1) 57 | }.to raise_error(ExplicitParameters::InvalidParameters, message) 58 | end 59 | 60 | context 'with nested parameters' do 61 | let :definition do 62 | ExplicitParameters::Parameters.define(:nested) do 63 | requires :address do 64 | requires :street, String 65 | requires :city, String 66 | end 67 | end 68 | end 69 | 70 | let :parameters do 71 | { 72 | 'address' => { 73 | 'street' => '3575 St-Laurent', 74 | 'city' => 'Montréal', 75 | } 76 | } 77 | end 78 | 79 | it 'parses expose the nested hash as a `Parameters` instance' do 80 | expect(params.address).to be_an ExplicitParameters::Parameters 81 | end 82 | 83 | it 'parses nested hashes' do 84 | expect(params.address.street).to be == '3575 St-Laurent' 85 | expect(params.address.city).to be == 'Montréal' 86 | end 87 | 88 | it 'reports missing attributes' do 89 | message = {errors: {address: ['is required']}}.to_json 90 | expect { 91 | definition.parse!({}) 92 | }.to raise_error(ExplicitParameters::InvalidParameters, message) 93 | end 94 | end 95 | 96 | context 'with list of parameters' do 97 | let :definition do 98 | ExplicitParameters::Parameters.define(:nested) do 99 | accepts :addresses, Array do 100 | requires :street, String 101 | requires :city, String 102 | end 103 | end 104 | end 105 | 106 | let :parameters do 107 | { 108 | 'addresses' => [ 109 | { 110 | 'street' => '3575 St-Laurent', 111 | 'city' => 'Montréal', 112 | } 113 | ] 114 | } 115 | end 116 | 117 | it 'the exposed parameter is an Array' do 118 | expect(params.addresses).to be_an Array 119 | expect(params.addresses.size).to be == 1 120 | end 121 | 122 | it 'parses nested hashes' do 123 | expect(params.addresses.first.street).to be == '3575 St-Laurent' 124 | expect(params.addresses.first.city).to be == 'Montréal' 125 | end 126 | 127 | context 'when required' do 128 | let :definition do 129 | ExplicitParameters::Parameters.define(:nested) do 130 | requires :addresses, Array do 131 | requires :street, String 132 | requires :city, String 133 | end 134 | end 135 | end 136 | 137 | it 'reports missing attributes' do 138 | message = {errors: {addresses: ['is required']}}.to_json 139 | expect { 140 | definition.parse!({}) 141 | }.to raise_error(ExplicitParameters::InvalidParameters, message) 142 | end 143 | 144 | it 'considers empty arrays as missing' do 145 | message = {errors: {addresses: ['is required']}}.to_json 146 | expect { 147 | params = definition.parse!({'addresses' => []}) 148 | p params.addresses 149 | }.to raise_error(ExplicitParameters::InvalidParameters, message) 150 | end 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'action_controller' 2 | require 'explicit_parameters' 3 | 4 | RSpec.configure do |config| 5 | config.expect_with :rspec do |expectations| 6 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 7 | end 8 | config.mock_with :rspec do |mocks| 9 | mocks.verify_partial_doubles = true 10 | end 11 | config.disable_monkey_patching! 12 | config.order = :random 13 | Kernel.srand config.seed 14 | end 15 | --------------------------------------------------------------------------------