├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .rspec ├── .ruby-gemset ├── .ruby-version ├── COPYING ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── UPGRADING.md ├── bin ├── autospec └── rspec ├── lib ├── rails_param.rb └── rails_param │ ├── coercion.rb │ ├── coercion │ ├── array_param.rb │ ├── big_decimal_param.rb │ ├── boolean_param.rb │ ├── float_param.rb │ ├── hash_param.rb │ ├── integer_param.rb │ ├── string_param.rb │ ├── time_param.rb │ └── virtual_param.rb │ ├── invalid_parameter_error.rb │ ├── param.rb │ ├── param_evaluator.rb │ ├── parameter.rb │ ├── validator.rb │ ├── validator │ ├── blank.rb │ ├── custom.rb │ ├── format.rb │ ├── in.rb │ ├── is.rb │ ├── max.rb │ ├── max_length.rb │ ├── min.rb │ ├── min_length.rb │ └── required.rb │ └── version.rb ├── rails_param.gemspec └── spec ├── fixtures ├── controllers.rb └── fake_rails_application.rb ├── rails_integration_spec.rb ├── rails_param ├── coercion │ ├── array_param_spec.rb │ ├── big_decimal_param_spec.rb │ ├── boolean_param_spec.rb │ ├── float_param_spec.rb │ ├── hash_param_spec.rb │ ├── integer_param_spec.rb │ ├── string_param_spec.rb │ └── time_param_spec.rb ├── coercion_spec.rb ├── param_spec.rb ├── parameter_spec.rb ├── validator │ ├── blank_spec.rb │ ├── custom_spec.rb │ ├── format_spec.rb │ ├── in_spec.rb │ ├── is_spec.rb │ ├── max_length_spec.rb │ ├── max_spec.rb │ ├── min_length_spec.rb │ ├── min_spec.rb │ ├── required_spec.rb │ └── shared_examples │ │ ├── does_not_raise_error.rb │ │ └── raises_invalid_parameter_error.rb └── validator_spec.rb ├── rails_param_spec.rb └── spec_helper.rb /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Basic Info 2 | 3 | * rails_param Version: 4 | * Ruby Version: 5 | * Rails Version: 6 | 7 | ## Issue description 8 | Please provide a description of the issue you're experiencing. 9 | Please also provide the exception message/stacktrace or any other useful detail. 10 | 11 | ## Steps to reproduce 12 | If possible, please provide the steps to reproduce the issue. 13 | 14 | ## CHECKLIST (delete before creating the issue) 15 | * If you're not reporting a bug/issue, you can ignore this whole template. 16 | * Are you using the latest rails_param version? If not, please check the [Releases](https://github.com/nicolasblanco/rails_param/releases) page to see if the issue has already been fixed. 17 | * Provide the rails_param, Ruby and Rails Versions you're using while experiencing the issue in `Basic Info`. 18 | * Fill the `Issue description` and `Steps to Reproduce` sections. 19 | * Delete this checklist before posting the issue. 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | A few sentences describing the overall goals of the pull request's commits. 3 | Link to related issues if any. (As `Fixes #XXX`) 4 | 5 | ## Todos 6 | List any remaining work that needs to be done, i.e: 7 | - [ ] Tests 8 | - [ ] Documentation 9 | 10 | ## Additional Notes 11 | Optional section 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | env: 12 | RAILS_VERSION: ${{ matrix.rails }} 13 | strategy: 14 | matrix: 15 | ruby: ['2.7', '3.0', '3.1', '3.2' ] 16 | rails: ['~> 5.2', '~> 6.0', '~> 6.1', '~> 7.0'] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Ruby 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.ruby }} 24 | bundler-cache: true 25 | - name: Run tests 26 | run: bundle exec rake 27 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | name: Publish to Rubygems 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@master 14 | 15 | - name: Set up Ruby 2.7 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: 2.7 19 | 20 | - name: Publish to RubyGems 21 | run: | 22 | mkdir -p $HOME/.gem 23 | touch $HOME/.gem/credentials 24 | chmod 0600 $HOME/.gem/credentials 25 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 26 | gem build rails_param.gemspec 27 | gem push rails_param-*.gem 28 | env: 29 | GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_AUTH_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | README.html 3 | .rvmrc 4 | .bundle 5 | Gemfile.lock 6 | *.gem 7 | .idea 8 | tmp/ 9 | coverage/ -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format=documentation 3 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | rails_param 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.0.1 2 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Everyone is permitted to copy and distribute verbatim or modified 5 | copies of this license document, and changing it is allowed as long 6 | as the name is changed. 7 | 8 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 9 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 10 | 11 | 0. You just DO WHAT THE FUCK YOU WANT TO. 12 | 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'pry' 6 | gem 'simplecov', require: false, group: :test 7 | 8 | install_if -> { ENV.fetch('RAILS_VERSION', nil) } do 9 | rails_version = ENV.fetch('RAILS_VERSION', nil) 10 | gem 'actionpack', rails_version 11 | gem 'activesupport', rails_version 12 | end 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nicolas Blanco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rails_param 2 | _Parameter Validation & Type Coercion for Rails_ 3 | 4 | [![Gem Version](https://badge.fury.io/rb/rails_param.svg)](https://rubygems.org/gems/rails_param) 5 | [![Build Status](https://travis-ci.org/nicolasblanco/rails_param.svg?branch=master)](https://travis-ci.org/nicolasblanco/rails_param) 6 | 7 | ## Introduction 8 | 9 | This library is handy if you want to validate a few numbers of parameters directly inside your controller. 10 | 11 | For example : you are building a search action and want to validate that the `sort` parameter is set and only set to something like `desc` or `asc`. 12 | 13 | ## Important 14 | 15 | This library should not be used to validate a large number of parameters or parameters sent via a form or namespaced (like `params[:user][:first_name]`). There is already a great framework included in Rails (ActiveModel::Model) which can be used to create virtual classes with all the validations you already know and love from Rails. Remember to always try to stay in the “thin controller” rule. 16 | 17 | See [this](http://blog.remarkablelabs.com/2012/12/activemodel-model-rails-4-countdown-to-2013) page to see an example on how to build a contact form using ActiveModel::Model. 18 | 19 | But sometimes, it’s not practical to create an external class just to validate and convert a few parameters. In this case, you may use this gem. It allows you to easily do validations and conversion of the parameters directly in your controller actions using a simple method call. 20 | 21 | ## Credits 22 | 23 | This is originally a port of the gem [sinatra-param](https://github.com/mattt/sinatra-param) for the Rails framework. 24 | 25 | All the credits go to [@mattt](https://twitter.com/mattt). 26 | 27 | It has all the features of the sinatra-param gem, I used bang methods (like param!) to indicate that they are destructive as they change the controller params object and may raise an exception. 28 | 29 | ## Upgrading 30 | 31 | Find a list of breaking changes in [UPGRADING](UPGRADING.md). 32 | 33 | ## Installation 34 | 35 | As usual, in your Gemfile... 36 | 37 | ``` ruby 38 | gem 'rails_param' 39 | ``` 40 | 41 | ## Example 42 | 43 | ``` ruby 44 | # GET /search?q=example 45 | # GET /search?q=example&categories=news 46 | # GET /search?q=example&sort=created_at&order=ASC 47 | def search 48 | param! :q, String, required: true 49 | param! :categories, Array 50 | param! :sort, String, default: "title" 51 | param! :order, String, in: %w(asc desc), transform: :downcase, default: "asc" 52 | param! :price, String, format: /[<\=>]\s*\$\d+/ 53 | 54 | # Access the parameters using the params object (like params[:q]) as you usually do... 55 | end 56 | end 57 | ``` 58 | 59 | ### Parameter Types 60 | 61 | By declaring parameter types, incoming parameters will automatically be transformed into an object of that type. For instance, if a param is `:boolean`, values of `'1'`, `'true'`, `'t'`, `'yes'`, and `'y'` will be automatically transformed into `true`. `BigDecimal` defaults to a precision of 14, but this can but changed by passing in the optional `precision:` argument. Any `$` and `,` are automatically stripped when converting to `BigDecimal`. 62 | 63 | - `String` 64 | - `Integer` 65 | - `Float` 66 | - `:boolean/TrueClass/FalseClass` _("1/0", "true/false", "t/f", "yes/no", "y/n")_ 67 | - `Array` _("1,2,3,4,5")_ 68 | - `Hash` _("key1:value1,key2:value2")_ 69 | - `Date`, `Time`, & `DateTime` 70 | - `BigDecimal` _("$1,000,000")_ 71 | 72 | ### Validations 73 | 74 | Encapsulate business logic in a consistent way with validations. If a parameter does not satisfy a particular condition, an exception (RailsParam::InvalidParameterError) is raised. 75 | You may use the [rescue_from](http://api.rubyonrails.org/classes/ActiveSupport/Rescuable/ClassMethods.html#method-i-rescue_from) method in your controller to catch this kind of exception. 76 | 77 | - `required` 78 | - `blank` 79 | - `is` 80 | - `in`, `within`, `range` 81 | - `min` / `max` 82 | - `min_length` / `max_length` 83 | - `format` 84 | 85 | Customize exception message with option `:message` 86 | 87 | ```ruby 88 | param! :q, String, required: true, message: "Query not specified" 89 | ``` 90 | 91 | ### Defaults and Transformations 92 | 93 | Passing a `default` option will provide a default value for a parameter if none is passed. A `default` can defined as either a default or as a `Proc`: 94 | 95 | ```ruby 96 | param! :attribution, String, default: "©" 97 | param! :year, Integer, default: lambda { Time.now.year } 98 | ``` 99 | 100 | Use the `transform` option to take even more of the business logic of parameter I/O out of your code. Anything that responds to `to_proc` (including `Proc` and symbols) will do. 101 | 102 | ```ruby 103 | param! :order, String, in: ["ASC", "DESC"], transform: :upcase, default: "ASC" 104 | param! :offset, Integer, min: 0, transform: lambda {|n| n - (n % 10)} 105 | ``` 106 | 107 | ### Nested Attributes 108 | 109 | rails_param allows you to apply any of the above mentioned validations to attributes nested in hashes: 110 | 111 | ```ruby 112 | param! :book, Hash do |b| 113 | b.param! :title, String, blank: false 114 | b.param! :price, BigDecimal, precision: 4, required: true 115 | b.param! :author, Hash, required: true do |a| 116 | a.param! :first_name, String 117 | a.param! :last_name, String, blank: false 118 | end 119 | end 120 | ``` 121 | 122 | ### Arrays 123 | 124 | Validate every element of your array, including nested hashes and arrays: 125 | 126 | ```ruby 127 | # primitive datatype syntax 128 | param! :integer_array, Array do |array,index| 129 | array.param! index, Integer, required: true 130 | end 131 | 132 | # complex array 133 | param! :books_array, Array, required: true do |b| 134 | b.param! :title, String, blank: false 135 | b.param! :author, Hash, required: true do |a| 136 | a.param! :first_name, String 137 | a.param! :last_name, String, required: true 138 | end 139 | b.param! :subjects, Array do |s,i| 140 | s.param! i, String, blank: false 141 | end 142 | end 143 | ``` 144 | 145 | ## Thank you 146 | 147 | Many thanks to: 148 | 149 | * [Mattt Thompson (@mattt)](https://twitter.com/mattt) 150 | * [Vincent Ollivier (@vinc686)](https://twitter.com/vinc686) 151 | 152 | ## Contact 153 | 154 | Nicolas Blanco 155 | 156 | - http://github.com/nicolasblanco 157 | - http://twitter.com/nblanco_fr 158 | - nicolas@nicolasblanco.fr 159 | 160 | ## License 161 | 162 | rails_param is available under the MIT license. See the LICENSE file for more info. 163 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | RSpec::Core::RakeTask.new(:spec) do |t| 4 | t.rspec_opts = ['--format progress'] 5 | end 6 | 7 | task :default => :spec 8 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # UPGRADING 2 | v0.x -> v1.0.0 3 | 4 | ## End of Support 5 | Support for the following versions has been removed: 6 | ### Ruby: 7 | - 2.4 8 | - 2.5 9 | 10 | ### Rails: 11 | - 5.0 12 | - 5.1 13 | 14 | ## Breaking Changes 15 | ### Error Namespace 16 | `RailsParam::InvalidParameter`: 17 | 18 | `RailsParam::Param::InvalidParameterError` has had the `Param` namespace removed. Please update error handling to use the new error `RailsParam::InvalidParameterError` 19 | -------------------------------------------------------------------------------- /bin/autospec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'autospec' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rspec-core', 'autospec') 17 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rspec' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rspec-core', 'rspec') 17 | -------------------------------------------------------------------------------- /lib/rails_param.rb: -------------------------------------------------------------------------------- 1 | require 'rails_param/param' 2 | Dir[File.join(__dir__, 'rails_param/validator', '*.rb')].sort.each { |file| require file } 3 | Dir[File.join(__dir__, 'rails_param/coercion', '*.rb')].sort.reverse.each { |file| require file } 4 | Dir[File.join(__dir__, 'rails_param', '*.rb')].sort.each { |file| require file } 5 | 6 | ActiveSupport.on_load(:action_controller) do 7 | include RailsParam 8 | end 9 | -------------------------------------------------------------------------------- /lib/rails_param/coercion.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Coercion 3 | attr_reader :coercion, :param 4 | 5 | PARAM_TYPE_MAPPING = { 6 | Integer => IntegerParam, 7 | Float => FloatParam, 8 | String => StringParam, 9 | Array => ArrayParam, 10 | Hash => HashParam, 11 | BigDecimal => BigDecimalParam, 12 | Date => TimeParam, 13 | DateTime => TimeParam, 14 | Time => TimeParam, 15 | TrueClass => BooleanParam, 16 | FalseClass => BooleanParam, 17 | boolean: BooleanParam 18 | }.freeze 19 | 20 | TIME_TYPES = [Date, DateTime, Time].freeze 21 | BOOLEAN_TYPES = [TrueClass, FalseClass, :boolean].freeze 22 | 23 | def initialize(param, type, options) 24 | @param = param 25 | @coercion = klass_for(type).new(param: param, options: options, type: type) 26 | end 27 | 28 | def klass_for(type) 29 | klass = PARAM_TYPE_MAPPING[type] 30 | return klass if klass 31 | 32 | raise TypeError 33 | end 34 | 35 | def coerce 36 | return nil if param.nil? 37 | 38 | coercion.coerce 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/rails_param/coercion/array_param.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Coercion 3 | class ArrayParam < VirtualParam 4 | def coerce 5 | return param if param.is_a?(Array) 6 | 7 | Array(param.split(options[:delimiter] || ",")) 8 | end 9 | 10 | private 11 | 12 | def argument_validation 13 | raise ArgumentError unless type == Array 14 | raise ArgumentError unless param.respond_to?(:split) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rails_param/coercion/big_decimal_param.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Coercion 3 | class BigDecimalParam < VirtualParam 4 | DEFAULT_PRECISION = 14 5 | 6 | def coerce 7 | stripped_param = if param.is_a?(String) 8 | param.delete('$,').strip.to_f 9 | else 10 | param 11 | end 12 | 13 | BigDecimal(stripped_param, options[:precision] || DEFAULT_PRECISION) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rails_param/coercion/boolean_param.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Coercion 3 | class BooleanParam < VirtualParam 4 | FALSEY = /^(false|f|no|n|0)$/i.freeze 5 | TRUTHY = /^(true|t|yes|y|1)$/i.freeze 6 | 7 | def coerce 8 | return false if FALSEY === param.to_s 9 | return true if TRUTHY === param.to_s 10 | 11 | raise ArgumentError 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rails_param/coercion/float_param.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Coercion 3 | class FloatParam < VirtualParam 4 | def coerce 5 | Float(param) 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/rails_param/coercion/hash_param.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Coercion 3 | class HashParam < VirtualParam 4 | def coerce 5 | return param if param.is_a?(ActionController::Parameters) 6 | raise ArgumentError unless param.respond_to?(:split) 7 | 8 | Hash[param.split(options[:delimiter] || ",").map { |c| c.split(options[:separator] || ":") }] 9 | end 10 | 11 | private 12 | 13 | def argument_validation 14 | raise ArgumentError unless type == Hash 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rails_param/coercion/integer_param.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Coercion 3 | class IntegerParam < VirtualParam 4 | def coerce 5 | Integer(param) 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/rails_param/coercion/string_param.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Coercion 3 | class StringParam < VirtualParam 4 | def coerce 5 | String(param) 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/rails_param/coercion/time_param.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Coercion 3 | class TimeParam < VirtualParam 4 | def coerce 5 | return type.strptime(param, options[:format]) if options[:format].present? 6 | 7 | type.parse(param) 8 | end 9 | 10 | private 11 | 12 | def argument_validation 13 | raise ArgumentError unless type.respond_to?(:parse) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rails_param/coercion/virtual_param.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Coercion 3 | class VirtualParam 4 | attr_reader :param, :options, :type 5 | 6 | def initialize(param:, options: nil, type: nil) 7 | @param = param 8 | @options = options 9 | @type = type 10 | argument_validation 11 | end 12 | 13 | def coerce 14 | nil 15 | end 16 | 17 | private 18 | 19 | def argument_validation 20 | nil 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/rails_param/invalid_parameter_error.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class InvalidParameterError < StandardError 3 | attr_accessor :param, :options 4 | 5 | def initialize(message, param: nil, options: {}) 6 | self.param = param 7 | self.options = options 8 | super(message) 9 | end 10 | 11 | def message 12 | return options[:message] if options.is_a?(Hash) && options.key?(:message) 13 | 14 | super 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rails_param/param.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | def param!(name, type, options = {}, &block) 3 | ParamEvaluator.new(params).param!(name, type, options, &block) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/rails_param/param_evaluator.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class ParamEvaluator 3 | attr_accessor :params 4 | 5 | def initialize(params, context = nil) 6 | @params = params 7 | @context = context 8 | end 9 | 10 | def param!(name, type, options = {}, &block) 11 | name = name.is_a?(Integer)? name : name.to_s 12 | return unless params.include?(name) || check_param_presence?(options[:default]) || options[:required] 13 | 14 | parameter_name = @context ? "#{@context}[#{name}]" : name 15 | coerced_value = coerce(parameter_name, params[name], type, options) 16 | 17 | parameter = RailsParam::Parameter.new( 18 | name: parameter_name, 19 | value: coerced_value, 20 | type: type, 21 | options: options, 22 | &block 23 | ) 24 | 25 | parameter.set_default if parameter.should_set_default? 26 | 27 | # validate presence 28 | if params[name].nil? && options[:required] 29 | raise InvalidParameterError.new( 30 | "Parameter #{parameter_name} is required", 31 | param: parameter_name, 32 | options: options 33 | ) 34 | end 35 | 36 | recurse_on_parameter(parameter, &block) if block_given? 37 | 38 | # apply transformation 39 | parameter.transform if options[:transform] 40 | 41 | # validate 42 | validate!(parameter) 43 | 44 | # set params value 45 | params[name] = parameter.value 46 | end 47 | 48 | private 49 | 50 | def recurse_on_parameter(parameter, &block) 51 | return if parameter.value.nil? 52 | 53 | if parameter.type == Array 54 | parameter.value.each_with_index do |element, i| 55 | if element.is_a?(Hash) || element.is_a?(ActionController::Parameters) 56 | recurse element, "#{parameter.name}[#{i}]", &block 57 | else 58 | parameter.value[i] = recurse({ i => element }, parameter.name, i, &block) # supply index as key unless value is hash 59 | end 60 | end 61 | else 62 | recurse parameter.value, parameter.name, &block 63 | end 64 | end 65 | 66 | def recurse(element, context, index = nil) 67 | raise InvalidParameterError, 'no block given' unless block_given? 68 | 69 | yield(ParamEvaluator.new(element, context), index) 70 | end 71 | 72 | def check_param_presence? param 73 | !param.nil? 74 | end 75 | 76 | def coerce(param_name, param, type, options = {}) 77 | begin 78 | return nil if param.nil? 79 | return param if (param.is_a?(type) rescue false) 80 | 81 | Coercion.new(param, type, options).coerce 82 | rescue ArgumentError, TypeError 83 | raise InvalidParameterError.new("'#{param}' is not a valid #{type}", param: param_name) 84 | end 85 | end 86 | 87 | def validate!(param) 88 | param.validate 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/rails_param/parameter.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Parameter 3 | attr_accessor :name, :value, :options, :type 4 | 5 | TIME_TYPES = [Date, DateTime, Time].freeze 6 | STRING_OR_TIME_TYPES = ([String] + TIME_TYPES).freeze 7 | 8 | def initialize(name:, value:, options: {}, type: nil) 9 | @name = name 10 | @value = value 11 | @options = options 12 | @type = type 13 | end 14 | 15 | def should_set_default? 16 | value.nil? && check_param_presence?(options[:default]) 17 | end 18 | 19 | def set_default 20 | self.value = options[:default].respond_to?(:call) ? options[:default].call : options[:default] 21 | end 22 | 23 | def transform 24 | self.value = options[:transform].to_proc.call(value) 25 | end 26 | 27 | def validate 28 | Validator.new(self).validate! 29 | end 30 | 31 | private 32 | 33 | def check_param_presence?(param) 34 | !param.nil? 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/rails_param/validator.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module RailsParam 4 | class Validator 5 | extend Forwardable 6 | 7 | attr_reader :parameter 8 | 9 | def_delegators :parameter, :name, :options, :value 10 | 11 | VALIDATABLE_OPTIONS = [ 12 | :blank, 13 | :custom, 14 | :format, 15 | :in, 16 | :is, 17 | :max_length, 18 | :max, 19 | :min_length, 20 | :min, 21 | :required 22 | ].freeze 23 | 24 | def initialize(parameter) 25 | @parameter = parameter 26 | end 27 | 28 | def validate! 29 | options.each_key do |key| 30 | next unless VALIDATABLE_OPTIONS.include? key 31 | 32 | class_name = camelize(key) 33 | Validator.const_get(class_name).new(parameter).valid! 34 | end 35 | end 36 | 37 | def valid! 38 | return if valid_value? 39 | 40 | raise InvalidParameterError.new( 41 | error_message, 42 | param: name, 43 | options: options 44 | ) 45 | end 46 | 47 | private 48 | 49 | def camelize(term) 50 | string = term.to_s 51 | string.split('_').collect(&:capitalize).join 52 | end 53 | 54 | def error_message 55 | nil 56 | end 57 | 58 | def valid_value? 59 | # Should be overwritten in subclass 60 | false 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/rails_param/validator/blank.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Validator 3 | class Blank < Validator 4 | def valid_value? 5 | return false if parameter.options[:blank] 6 | 7 | case value 8 | when String 9 | (/\S/ === value) 10 | when Array, Hash, ActionController::Parameters 11 | !value.empty? 12 | else 13 | !value.nil? 14 | end 15 | end 16 | 17 | private 18 | 19 | def error_message 20 | "Parameter #{name} cannot be blank" 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/rails_param/validator/custom.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Validator 3 | class Custom < Validator 4 | def valid_value? 5 | !options[:custom].call(value) 6 | end 7 | 8 | private 9 | 10 | def error_message; end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/rails_param/validator/format.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Validator 3 | class Format < Validator 4 | def valid_value? 5 | matches_time_types? || string_in_format? 6 | end 7 | 8 | private 9 | 10 | TIME_TYPES = [Date, DateTime, Time].freeze 11 | STRING_OR_TIME_TYPES = ([String] + TIME_TYPES).freeze 12 | 13 | def error_message 14 | "Parameter #{name} must be a string if using the format validation" unless matches_string_or_time_types? 15 | "Parameter #{name} must match format #{options[:format]}" unless string_in_format? 16 | end 17 | 18 | def matches_time_types? 19 | TIME_TYPES.any? { |cls| value.is_a? cls } 20 | end 21 | 22 | def matches_string_or_time_types? 23 | STRING_OR_TIME_TYPES.any? { |cls| value.is_a? cls } 24 | end 25 | 26 | def string_in_format? 27 | value =~ options[:format] && value.kind_of?(String) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/rails_param/validator/in.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Validator 3 | class In < Validator 4 | def valid_value? 5 | value.nil? || case options[:in] 6 | when Range 7 | options[:in].include?(value) 8 | else 9 | Array(options[:in]).include?(value) 10 | end 11 | end 12 | 13 | private 14 | 15 | def error_message 16 | "Parameter #{parameter.name} must be within #{parameter.options[:in]}" 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/rails_param/validator/is.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Validator 3 | class Is < Validator 4 | def valid_value? 5 | value === options[:is] 6 | end 7 | 8 | private 9 | 10 | def error_message 11 | "Parameter #{name} must be #{options[:is]}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rails_param/validator/max.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Validator 3 | class Max < Validator 4 | def valid_value? 5 | value.nil? || options[:max] >= value 6 | end 7 | 8 | private 9 | 10 | def error_message 11 | "Parameter #{name} cannot be greater than #{options[:max]}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rails_param/validator/max_length.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Validator 3 | class MaxLength < Validator 4 | def valid_value? 5 | value.nil? || options[:max_length] >= value.length 6 | end 7 | 8 | private 9 | 10 | def error_message 11 | "Parameter #{name} cannot have length greater than #{options[:max_length]}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rails_param/validator/min.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Validator 3 | class Min < Validator 4 | def valid_value? 5 | value.nil? || options[:min] <= value 6 | end 7 | 8 | private 9 | 10 | def error_message 11 | "Parameter #{name} cannot be less than #{options[:min]}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rails_param/validator/min_length.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Validator 3 | class MinLength < Validator 4 | def valid_value? 5 | value.nil? || options[:min_length] <= value.length 6 | end 7 | 8 | private 9 | 10 | def error_message 11 | "Parameter #{name} cannot have length less than #{options[:min_length]}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rails_param/validator/required.rb: -------------------------------------------------------------------------------- 1 | module RailsParam 2 | class Validator 3 | class Required < Validator 4 | private 5 | 6 | def valid_value? 7 | !(value.nil? && options[:required]) 8 | end 9 | 10 | def error_message 11 | "Parameter #{name} is required" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rails_param/version.rb: -------------------------------------------------------------------------------- 1 | module RailsParam #:nodoc 2 | VERSION = "1.3.1" 3 | end 4 | -------------------------------------------------------------------------------- /rails_param.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib/', __FILE__) 2 | $:.unshift lib unless $:.include?(lib) 3 | 4 | require 'rails_param/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'rails_param' 8 | s.version = RailsParam::VERSION 9 | s.authors = ["Nicolas Blanco"] 10 | s.email = 'nicolas@nicolasblanco.fr' 11 | s.homepage = 'http://github.com/nicolasblanco/rails_param' 12 | s.license = 'MIT' 13 | 14 | s.description = %q{ 15 | Parameter Validation and Type Coercion for Rails 16 | } 17 | 18 | s.summary = 'Parameter Validation and Type Coercion for Rails' 19 | 20 | s.required_rubygems_version = '>= 1.3.6' 21 | 22 | s.files = Dir.glob("lib/**/*.rb") + %w(README.md) 23 | 24 | s.rdoc_options = ["--charset=UTF-8"] 25 | s.require_paths = ["lib"] 26 | 27 | s.add_development_dependency 'rspec', '~> 3.4' 28 | s.add_development_dependency 'rspec-rails', '~> 3.4' 29 | 30 | s.add_dependency 'actionpack', '>= 3.2.0' 31 | s.add_dependency 'activesupport', '>= 3.2.0' 32 | end 33 | -------------------------------------------------------------------------------- /spec/fixtures/controllers.rb: -------------------------------------------------------------------------------- 1 | require 'fixtures/fake_rails_application' 2 | 3 | class FakeController < ActionController::Base 4 | include Rails.application.routes.url_helpers 5 | 6 | def show 7 | render plain: "Foo" 8 | end 9 | 10 | def index 11 | param! :sort, String, in: %w(asc desc), default: "asc", transform: :downcase 12 | param! :page, Integer, default: 1 13 | param! :tags, Array 14 | 15 | render plain: "index" 16 | end 17 | 18 | def new 19 | render plain: "new" 20 | end 21 | 22 | def edit 23 | param! :book, Hash, required: true do |b| 24 | b.param! :title, String, required: true 25 | b.param! :author, Hash do |a| 26 | a.param! :first_name, String, required: true 27 | a.param! :last_name, String, required: true 28 | a.param! :age, Integer, required: true 29 | end 30 | b.param! :price, BigDecimal, required: true 31 | end 32 | render plain: :book 33 | end 34 | 35 | def nested_array 36 | param! :filter, Hash, default: {} do |f| 37 | f.param! :state, Array 38 | end 39 | 40 | render plain: :nested_array 41 | end 42 | 43 | def nested_array_optional_element 44 | param! :filter, Hash, default: {} do |f| 45 | f.param! :state, Array do |s, idx| 46 | s.param! idx, String 47 | end 48 | end 49 | 50 | render plain: :nested_array_optional_element 51 | end 52 | 53 | def nested_array_required_element 54 | param! :filter, Hash, default: {} do |f| 55 | f.param! :state, Array do |s, idx| 56 | s.param! idx, String, required: true 57 | end 58 | end 59 | 60 | render plain: :nested_array_required_element 61 | end 62 | 63 | def array 64 | param! :my_array, Array 65 | 66 | render plain: :array 67 | end 68 | 69 | def array_optional_element 70 | param! :my_array, Array do |s, idx| 71 | s.param! idx, String 72 | end 73 | 74 | render plain: :array 75 | end 76 | 77 | def array_required_element 78 | param! :my_array, Array do |s, idx| 79 | s.param! idx, String, required: true 80 | end 81 | 82 | render plain: :array 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/fixtures/fake_rails_application.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/all' 2 | require 'action_controller' 3 | require 'action_dispatch' 4 | require 'rails' 5 | require 'rails_param' 6 | 7 | # Boilerplate 8 | module Rails 9 | class App 10 | def env_config; {} end 11 | def routes 12 | return @routes if defined?(@routes) 13 | @routes = ActionDispatch::Routing::RouteSet.new 14 | @routes.draw do 15 | get '/fake/new' => "fake#new" 16 | get '/fakes' => "fake#index" 17 | get '/fake/(:id)' => "fake#show" 18 | get '/fake/edit' => "fake#edit" 19 | get '/fake/nested_array' => "fake#nested_array" 20 | get '/fake/nested_array_optional_element' => "fake#nested_array_optional_element" 21 | get '/fake/nested_array_required_element' => "fake#nested_array_required_element" 22 | post '/fake/array' => "fake#array" 23 | post '/fake/array' => "fake#array_optional_element" 24 | post '/fake/array' => "fake#array_required_element" 25 | end 26 | @routes 27 | end 28 | end 29 | def self.application 30 | @app ||= App.new 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/rails_integration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe FakeController, type: :controller do 4 | # Needed to run tests against Rails 4 AND 5 5 | def prepare_params(params) 6 | return params if Rails.version[0].to_i <= 4 7 | { params: params } 8 | end 9 | 10 | describe "type coercion" do 11 | it "coerces to integer" do 12 | get :index, **prepare_params(page: "666") 13 | 14 | expect(controller.params[:page]).to eq 666 15 | end 16 | 17 | it "raises InvalidParameterError if supplied an array instead of other type (prevent TypeError)" do 18 | expect { get :index, **prepare_params(page: ["a", "b", "c"]) }.to raise_error( 19 | RailsParam::InvalidParameterError, %q('["a", "b", "c"]' is not a valid Integer)) 20 | end 21 | 22 | it "raises InvalidParameterError if supplied an hash instead of other type (prevent TypeError)" do 23 | expect { get :index, **prepare_params(page: {"a" => "b", "c" => "d"}) }.to raise_error( 24 | RailsParam::InvalidParameterError, %q('{"a"=>"b", "c"=>"d"}' is not a valid Integer)) 25 | end 26 | 27 | it "raises InvalidParameterError if supplied an hash instead of an array (prevent NoMethodError)" do 28 | expect { get :index, **prepare_params(tags: {"a" => "b", "c" => "d"}) }.to raise_error( 29 | RailsParam::InvalidParameterError, %q('{"a"=>"b", "c"=>"d"}' is not a valid Array)) 30 | end 31 | end 32 | 33 | describe "nested_hash" do 34 | it "validates nested properties" do 35 | params = { 36 | 'book' => { 37 | 'title' => 'One Hundred Years of Solitude', 38 | 'author' => { 39 | 'first_name' => 'Garbriel Garcia', 40 | 'last_name' => 'Marquez', 41 | 'age' => '70' 42 | }, 43 | 'price' => '$1,000.00' 44 | }} 45 | get :edit, **prepare_params(params) 46 | expect(controller.params[:book][:author][:age]).to eq 70 47 | expect(controller.params[:book][:author][:age]).to be_kind_of Integer 48 | expect(controller.params[:book][:price]).to eq 1000.0 49 | expect(controller.params[:book][:price]).to be_instance_of BigDecimal 50 | end 51 | 52 | it "raises error when required nested attribute missing" do 53 | params = { 54 | 'book' => { 55 | 'title' => 'One Hundred Years of Solitude', 56 | 'author' => { 57 | 'last_name' => 'Marquez', 58 | 'age' => '70' 59 | }, 60 | 'price' => '$1,000.00' 61 | }} 62 | expect { get :edit, **prepare_params(params) }.to raise_error { |error| 63 | expect(error).to be_a(RailsParam::InvalidParameterError) 64 | expect(error.param).to eq "book[author][first_name]" 65 | expect(error.options).to eq({:required => true}) 66 | } 67 | end 68 | 69 | it "passes when hash that's not required but has required attributes is missing" do 70 | params = { 71 | 'book' => { 72 | 'title' => 'One Hundred Years of Solitude', 73 | 'price' => '$1,000.00' 74 | }} 75 | get :edit, **prepare_params(params) 76 | expect(controller.params[:book][:price]).to eq 1000.0 77 | expect(controller.params[:book][:price]).to be_instance_of BigDecimal 78 | end 79 | end 80 | 81 | describe "InvalidParameterError" do 82 | it "raises an exception with params attributes" do 83 | expect { get :index, **prepare_params(sort: "foo") }.to raise_error { |error| 84 | expect(error).to be_a(RailsParam::InvalidParameterError) 85 | expect(error.param).to eq "sort" 86 | expect(error.options).to eq({:in => ["asc", "desc"], :default => "asc", :transform => :downcase}) 87 | } 88 | end 89 | end 90 | 91 | describe ":transform parameter" do 92 | it "applies transformations" do 93 | get :index, **prepare_params(sort: "ASC") 94 | 95 | expect(controller.params[:sort]).to eq "asc" 96 | end 97 | end 98 | 99 | describe "default values" do 100 | it "applies default values" do 101 | get :index 102 | 103 | expect(controller.params[:page]).to eq 1 104 | expect(controller.params[:sort]).to eq "asc" 105 | end 106 | end 107 | 108 | describe "nested_array" do 109 | it "responds with a 200 when the hash is supplied as a string" do 110 | expect { get :nested_array, **prepare_params({ filter: 'state' }) } 111 | .not_to raise_error 112 | end 113 | 114 | it "responds with a 200 when the nested array is provided as nil" do 115 | expect { get :nested_array, **prepare_params({ filter: { state: nil } }) } 116 | .not_to raise_error 117 | end 118 | end 119 | 120 | describe "nested_array_optional_element" do 121 | it "responds with a 200 when the hash is supplied as a string" do 122 | expect { get :nested_array_optional_element, **prepare_params({ filter: 'state' }) } 123 | .not_to raise_error 124 | end 125 | 126 | it "responds with a 200 when the nested array is provided as nil" do 127 | expect { get :nested_array_optional_element, **prepare_params({ filter: { state: nil } }) } 128 | .not_to raise_error 129 | end 130 | end 131 | 132 | describe "nested_array_required_element" do 133 | it "responds with a 200 when the hash is supplied as a string" do 134 | expect { get :nested_array_required_element, **prepare_params({ filter: 'state' }) } 135 | .not_to raise_error 136 | end 137 | 138 | it "responds with a 200 when the nested array is provided as nil" do 139 | expect { get :nested_array_required_element, **prepare_params({ filter: { state: nil } }) } 140 | .not_to raise_error 141 | end 142 | end 143 | 144 | describe "array" do 145 | before { request.headers['Content-Type'] = 'application/json' } 146 | 147 | it "responds with a 200 when array is not provided" do 148 | expect { post :array, **prepare_params({}) } 149 | .not_to raise_error 150 | end 151 | 152 | it "responds with a 200 when when nil is provided" do 153 | expect { post :array, **prepare_params({ my_array: nil }) } 154 | .not_to raise_error 155 | end 156 | end 157 | 158 | describe "array_optional_element" do 159 | before { request.headers['Content-Type'] = 'application/json' } 160 | 161 | it "responds with a 200 when array is not provided" do 162 | expect { post :array_optional_element, **prepare_params({}) } 163 | .not_to raise_error 164 | end 165 | 166 | it "responds with a 200 when when nil is provided" do 167 | expect { post :array_optional_element, **prepare_params({ my_array: nil }) } 168 | .not_to raise_error 169 | end 170 | end 171 | 172 | describe "array_required_element" do 173 | before { request.headers['Content-Type'] = 'application/json' } 174 | 175 | it "responds with a 200 when array is not provided" do 176 | expect { post :array_required_element, **prepare_params({}) } 177 | .not_to raise_error 178 | end 179 | 180 | it "responds with a 200 when when nil is provided" do 181 | expect { post :array_required_element, **prepare_params({ my_array: nil }) } 182 | .not_to raise_error 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /spec/rails_param/coercion/array_param_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Coercion::ArrayParam do 4 | let(:options) { { fizz: :buzz } } 5 | 6 | subject { described_class } 7 | 8 | shared_examples "does not raise an error" do 9 | it "does not raise an error" do 10 | expect { subject.new(param: param, type: type, options: options) }.to_not raise_error 11 | end 12 | end 13 | 14 | shared_examples "raises ArgumentError" do 15 | it "raises ArgumentError" do 16 | expect { subject.new(param: param, type: type, options: options) }.to raise_error ArgumentError 17 | end 18 | end 19 | 20 | shared_examples "returns an array" do 21 | it "returns the param as an array" do 22 | expect(subject.coerce).to eq ["foo", "bar"] 23 | end 24 | end 25 | 26 | describe ".new" do 27 | context "type is Array" do 28 | let(:type) { Array } 29 | 30 | context "param responds to #split" do 31 | let(:param) { "foo,bar" } 32 | 33 | it_behaves_like "does not raise an error" 34 | end 35 | 36 | context "param does not respond to split" do 37 | let(:param) { 1 } 38 | 39 | it_behaves_like "raises ArgumentError" 40 | end 41 | end 42 | 43 | context "type is not Array" do 44 | let(:param) { "foo" } 45 | let(:type) { String } 46 | 47 | it_behaves_like "raises ArgumentError" 48 | end 49 | end 50 | 51 | describe "#coerce" do 52 | let(:type) { Array } 53 | let(:options) { {} } 54 | subject { described_class.new(param: param, type: type, options: options) } 55 | 56 | context "param is an array" do 57 | let(:param) { ["foo", "bar"] } 58 | 59 | it_behaves_like "returns an array" 60 | end 61 | 62 | context "param is delimited by ','" do 63 | let(:param) { "foo,bar" } 64 | 65 | it_behaves_like "returns an array" 66 | end 67 | 68 | context "options delimiter provided" do 69 | let(:options) { {delimiter: "::"} } 70 | let(:param) { "foo::bar" } 71 | 72 | it_behaves_like "returns an array" 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/rails_param/coercion/big_decimal_param_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Coercion::BigDecimalParam do 4 | describe "#coerce" do 5 | let(:param) { 1.234567890123456 } 6 | let(:type) { BigDecimal } 7 | let(:options) { {} } 8 | subject { described_class.new(param: param, type: type, options: options) } 9 | 10 | shared_examples "returns BigDecimal with default precision" do 11 | it "returns a BigDecimal with the default precision" do 12 | expect(subject.coerce).to eq 1.2345678901235 13 | end 14 | end 15 | 16 | it_behaves_like "returns BigDecimal with default precision" 17 | 18 | context "given a precision option" do 19 | let(:options) { {precision: 10} } 20 | 21 | it "returns BigDecimal using precision option" do 22 | expect(subject.coerce).to eq 1.234567890 23 | end 24 | end 25 | 26 | context "param is a String" do 27 | let(:param) { "1.234567890123456"} 28 | 29 | it_behaves_like "returns BigDecimal with default precision" 30 | 31 | context "param is currency String" do 32 | let(:param) { "$1.50" } 33 | 34 | it "returns the param as BigDecimal" do 35 | expect(subject.coerce).to eq 1.50 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/rails_param/coercion/boolean_param_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Coercion::BooleanParam do 4 | describe "#coerce" do 5 | let(:type) { TrueClass } 6 | let(:options) { {} } 7 | subject { described_class.new(param: param, type: type, options: options) } 8 | 9 | shared_examples "coerces to true" do 10 | it "returns true" do 11 | expect(subject.coerce).to eq true 12 | end 13 | end 14 | 15 | shared_examples "coerces to false" do 16 | it "returns false" do 17 | expect(subject.coerce).to eq false 18 | end 19 | end 20 | 21 | context "given 'true'" do 22 | let(:param) { "true" } 23 | 24 | it_behaves_like "coerces to true" 25 | end 26 | 27 | context "given 'false'" do 28 | let(:param) { "false" } 29 | 30 | it_behaves_like "coerces to false" 31 | end 32 | 33 | context "given 't'" do 34 | let(:param) { "t" } 35 | 36 | it_behaves_like "coerces to true" 37 | end 38 | 39 | context "given 'f'" do 40 | let(:param) { "f" } 41 | 42 | it_behaves_like "coerces to false" 43 | end 44 | 45 | context "given 'yes'" do 46 | let(:param) { "yes" } 47 | 48 | it_behaves_like "coerces to true" 49 | end 50 | 51 | context "given 'no'" do 52 | let(:param) { "no" } 53 | 54 | it_behaves_like "coerces to false" 55 | end 56 | 57 | context "given 'y'" do 58 | let(:param) { "y" } 59 | 60 | it_behaves_like "coerces to true" 61 | end 62 | 63 | context "given 'n'" do 64 | let(:param) { "n" } 65 | 66 | it_behaves_like "coerces to false" 67 | end 68 | 69 | context "given '1'" do 70 | let(:param) { "1" } 71 | 72 | it_behaves_like "coerces to true" 73 | end 74 | 75 | context "given '0'" do 76 | let(:param) { "0" } 77 | 78 | it_behaves_like "coerces to false" 79 | end 80 | 81 | context "param not TRUTHY or FALSEY" do 82 | let(:param) { "foo" } 83 | 84 | it "raises ArgumentError" do 85 | expect { subject.coerce }.to raise_error ArgumentError 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/rails_param/coercion/float_param_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Coercion::FloatParam do 4 | describe "#coerce" do 5 | let(:type) { Float } 6 | let(:options) { {} } 7 | subject { described_class.new(param: param, type: type, options: options) } 8 | 9 | context "value is valid" do 10 | let(:param) { "12.34" } 11 | 12 | it "returns a Float" do 13 | expect(subject.coerce).to eq 12.34 14 | end 15 | end 16 | 17 | context "value is invalid" do 18 | let(:param) { "foo" } 19 | 20 | it "raises ArgumentError" do 21 | expect { subject.coerce }.to raise_error ArgumentError 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/rails_param/coercion/hash_param_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Coercion::HashParam do 4 | let(:options) { {} } 5 | 6 | subject { described_class } 7 | 8 | shared_examples "does not raise an error" do 9 | it "does not raise an error" do 10 | expect { subject.new(param: param, type: type, options: options) }.to_not raise_error 11 | end 12 | end 13 | 14 | shared_examples "raises ArgumentError" do 15 | it "raises ArgumentError" do 16 | expect { subject.new(param: param, type: type, options: options) }.to raise_error ArgumentError 17 | end 18 | end 19 | 20 | shared_examples "returns a hash" do 21 | it "returns the param as a hash" do 22 | expect(subject.coerce).to eq({ "foo" => "bar", "fizz" => "buzz" }) 23 | end 24 | end 25 | 26 | describe ".new" do 27 | let(:param) { "foo,bar" } 28 | 29 | context "type is Hash" do 30 | let(:type) { Hash } 31 | 32 | context "param responds to #split" do 33 | 34 | it_behaves_like "does not raise an error" 35 | end 36 | end 37 | 38 | context "type is not Hash" do 39 | let(:type) { String } 40 | 41 | it_behaves_like "raises ArgumentError" 42 | end 43 | end 44 | 45 | describe "#coerce" do 46 | let(:type) { Hash } 47 | let(:options) { {} } 48 | subject { described_class.new(param: param, type: type, options: options) } 49 | 50 | context "param is delimited by ','" do 51 | let(:param) { "foo:bar,fizz:buzz" } 52 | 53 | it_behaves_like "returns a hash" 54 | end 55 | 56 | context "options delimiter provided" do 57 | let(:options) { {delimiter: "::"} } 58 | let(:param) { "foo:bar::fizz:buzz" } 59 | 60 | it_behaves_like "returns a hash" 61 | end 62 | 63 | context "options delimiter provided" do 64 | let(:options) { {separator: "!"} } 65 | let(:param) { "foo!bar,fizz!buzz" } 66 | 67 | it_behaves_like "returns a hash" 68 | end 69 | 70 | context "param does not respond to split" do 71 | let(:param) { 1 } 72 | 73 | it "raises an ArgumentError" do 74 | expect { subject.coerce }.to raise_error ArgumentError 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/rails_param/coercion/integer_param_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Coercion::IntegerParam do 4 | shared_examples "does not raise an error" do 5 | it "does not raise an error" do 6 | expect { subject.coerce }.to_not raise_error 7 | end 8 | end 9 | 10 | shared_examples "raises ArgumentError" do 11 | it "raises ArgumentError" do 12 | expect { subject.coerce }.to raise_error ArgumentError 13 | end 14 | end 15 | 16 | shared_examples "returns an Integer" do 17 | it "returns the param as an Integer" do 18 | expect(subject.coerce).to eq 19 19 | end 20 | end 21 | 22 | describe "#coerce" do 23 | let(:type) { Integer } 24 | let(:options) { {} } 25 | subject { described_class.new(param: param, type: type, options: options) } 26 | 27 | context "param is a valid value" do 28 | let(:param) { "19" } 29 | 30 | it_behaves_like "does not raise an error" 31 | it_behaves_like "returns an Integer" 32 | end 33 | 34 | context "param is invalid value" do 35 | let(:param) { "notInteger" } 36 | 37 | it_behaves_like "raises ArgumentError" 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/rails_param/coercion/string_param_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Coercion::StringParam do 4 | shared_examples "does not raise an error" do 5 | it "does not raise an error" do 6 | expect { subject.coerce }.to_not raise_error 7 | end 8 | end 9 | 10 | shared_examples "returns an String" do 11 | it "returns the param as an String" do 12 | expect(subject.coerce).to eq "bar" 13 | end 14 | end 15 | 16 | describe "#coerce" do 17 | let(:type) { String } 18 | let(:options) { {} } 19 | subject { described_class.new(param: param, type: type, options: options) } 20 | 21 | context "param is a valid value" do 22 | let(:param) { :bar } 23 | 24 | it_behaves_like "does not raise an error" 25 | it_behaves_like "returns an String" 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/rails_param/coercion/time_param_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Coercion::TimeParam do 4 | describe ".new" do 5 | let(:param) { "foo" } 6 | let(:options) { {} } 7 | subject { described_class } 8 | 9 | shared_examples "does not raise an error" do 10 | it "does not raise an error" do 11 | expect { subject.new(param: param, type: type, options: options) }.to_not raise_error 12 | end 13 | end 14 | 15 | shared_examples "raises ArgumentError" do 16 | it "raises ArgumentError" do 17 | expect { subject.new(param: param, type: type, options: options) }.to raise_error ArgumentError 18 | end 19 | end 20 | 21 | context "type is Date" do 22 | let(:type) { Date } 23 | 24 | it_behaves_like "does not raise an error" 25 | end 26 | 27 | context "type is Time" do 28 | let(:type) { Time } 29 | 30 | it_behaves_like "does not raise an error" 31 | end 32 | 33 | context "type is DateTime" do 34 | let(:type) { DateTime } 35 | 36 | it_behaves_like "does not raise an error" 37 | end 38 | 39 | context "type does not respond to parse" do 40 | let(:type) { Array } 41 | 42 | it_behaves_like "raises ArgumentError" 43 | end 44 | end 45 | 46 | describe "#coerce" do 47 | shared_examples "does not raise an error" do 48 | it "does not raise an error" do 49 | expect { subject.coerce }.to_not raise_error 50 | end 51 | end 52 | 53 | shared_examples "raises ArgumentError" do 54 | it "raises ArgumentError" do 55 | expect { subject.coerce }.to raise_error ArgumentError 56 | end 57 | end 58 | 59 | let(:options) { {} } 60 | subject { described_class.new(param: param, type: type, options: options) } 61 | 62 | context "type is Date" do 63 | let(:type) { Date } 64 | 65 | shared_examples "returns a date" do 66 | it "returns a Date" do 67 | expect(subject.coerce).to eq(Date.new(2015, 10, 21)) 68 | end 69 | end 70 | 71 | context "param is a valid value" do 72 | let(:param) { "2015-10-21" } 73 | 74 | it_behaves_like "does not raise an error" 75 | it_behaves_like "returns a date" 76 | end 77 | 78 | context "param is an invalid value" do 79 | let(:param) { "notDate" } 80 | 81 | it_behaves_like "raises ArgumentError" 82 | end 83 | 84 | context "with format" do 85 | let(:options) { { format: "%F" } } 86 | 87 | context "param is a valid value" do 88 | let(:param) { "2015-10-21T11:11:00.000+06:00" } 89 | 90 | it_behaves_like "does not raise an error" 91 | it_behaves_like "returns a date" 92 | end 93 | 94 | context "param is an invalid value" do 95 | let(:param) { "notDate" } 96 | 97 | it_behaves_like "raises ArgumentError" 98 | end 99 | 100 | context "format is an invalid value" do 101 | let(:param) { "2015-10-21T11:11:00.000+06:00" } 102 | let(:options) { { format: "%x" } } 103 | 104 | it_behaves_like "raises ArgumentError" 105 | end 106 | end 107 | end 108 | 109 | context "type is DateTime" do 110 | let(:type) { DateTime } 111 | 112 | context "param is valid value" do 113 | let(:param) { "2015-10-21T11:11:00.000+06:00" } 114 | 115 | it_behaves_like "does not raise an error" 116 | it "returns a DateTime" do 117 | expect(subject.coerce).to eq(DateTime.new(2015, 10, 21, 11, 11, 0, '+6')) 118 | end 119 | end 120 | 121 | context "param is an invalid value" do 122 | let(:param) { "notDateTime" } 123 | 124 | it_behaves_like "raises ArgumentError" 125 | end 126 | 127 | context "with format" do 128 | let(:options) { { format: "%F" } } 129 | 130 | context "param is a valid value" do 131 | let(:param) { "2015-10-21T11:11:00.000+06:00" } 132 | 133 | it_behaves_like "does not raise an error" 134 | it "returns a Time" do 135 | expect(subject.coerce).to eq(DateTime.new(2015, 10, 21)) 136 | end 137 | end 138 | 139 | context "param is an invalid value" do 140 | let(:param) { "notDateTime" } 141 | 142 | it_behaves_like "raises ArgumentError" 143 | end 144 | 145 | context "format is an invalid value" do 146 | let(:param) { "2015-10-21T11:11:00.000+06:00" } 147 | let(:options) { { format: "%x" } } 148 | 149 | it_behaves_like "raises ArgumentError" 150 | end 151 | end 152 | end 153 | 154 | context "type is Time" do 155 | let(:type) { Time } 156 | 157 | context "param is valid value" do 158 | let(:param) { "2015-10-21T11:11:00.000+06:00" } 159 | 160 | it_behaves_like "does not raise an error" 161 | it "returns a Time" do 162 | expect(subject.coerce).to eq(Time.new(2015, 10, 21, 11, 11, 0, 21600)) 163 | end 164 | end 165 | 166 | context "param is an invalid value" do 167 | let(:param) { "notTime" } 168 | 169 | it_behaves_like "raises ArgumentError" 170 | end 171 | 172 | context "with format" do 173 | let(:options) { { format: "%F" } } 174 | 175 | context "param is a valid value" do 176 | let(:param) { "2015-10-21T11:11:00.000+06:00" } 177 | 178 | it_behaves_like "does not raise an error" 179 | it "returns a Time" do 180 | expect(subject.coerce).to eq(Time.new(2015, 10, 21)) 181 | end 182 | end 183 | 184 | context "param is an invalid value" do 185 | let(:param) { "notTime" } 186 | 187 | it_behaves_like "raises ArgumentError" 188 | end 189 | 190 | context "format is an invalid value" do 191 | let(:param) { "2015-10-21T11:11:00.000+06:00" } 192 | let(:options) { { format: "%x" } } 193 | 194 | it_behaves_like "raises ArgumentError" 195 | end 196 | end 197 | end 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /spec/rails_param/coercion_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Coercion do 4 | describe ".new" do 5 | let(:param) { "foo" } 6 | let(:type) { String } 7 | let(:options) { { fizz: :buzz } } 8 | 9 | subject { described_class } 10 | 11 | let(:coercion_class) { RailsParam::Coercion::StringParam } 12 | 13 | before { allow(coercion_class).to receive(:new) } 14 | 15 | it "initializes a coercion class based on the provided type" do 16 | subject.new(param, type, options) 17 | 18 | expect(coercion_class).to have_received(:new).with( 19 | param: param, 20 | type: type, 21 | options: options 22 | ) 23 | end 24 | 25 | context "when there is no mapping for the provided type" do 26 | let(:type) { :foobar } 27 | 28 | it "raises a type error" do 29 | expect { subject.new(param, type, options) }.to raise_error(TypeError) 30 | end 31 | end 32 | end 33 | 34 | describe "#coerce" do 35 | let(:param) { rand(0...1000) } 36 | let(:type) { Integer } 37 | let(:options) { {} } 38 | 39 | subject { described_class.new(param, type, options) } 40 | 41 | let(:coercion_class) { RailsParam::Coercion::IntegerParam } 42 | let(:coerced_param) { rand(-1000...-1) } 43 | 44 | before { allow_any_instance_of(coercion_class).to receive(:coerce).and_return(coerced_param) } 45 | 46 | it "delegates to the coercion instance" do 47 | expect(subject.coerce).to eq(coerced_param) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/rails_param/param_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if RUBY_VERSION >= '2.6.0' and Rails.version < '5' 4 | class ActionController::TestResponse < ActionDispatch::TestResponse 5 | def recycle! 6 | # hack to avoid MonitorMixin double-initialize error: 7 | @mon_mutex_owner_object_id = nil 8 | @mon_mutex = nil 9 | initialize 10 | end 11 | end 12 | end 13 | 14 | class MyController < ActionController::Base 15 | include RailsParam 16 | 17 | def params; 18 | end 19 | end 20 | 21 | describe RailsParam do 22 | describe ".param!" do 23 | let(:controller) { MyController.new } 24 | it "defines the method" do 25 | expect(controller).to respond_to(:param!) 26 | end 27 | 28 | describe "transform" do 29 | context "with a method" do 30 | it "transforms the value" do 31 | allow(controller).to receive(:params).and_return({ "word" => "foo" }) 32 | controller.param! :word, String, transform: :upcase 33 | expect(controller.params["word"]).to eq "FOO" 34 | end 35 | 36 | it "transforms default value" do 37 | allow(controller).to receive(:params).and_return({}) 38 | controller.param! :word, String, default: "foo", transform: :upcase 39 | expect(controller.params["word"]).to eq "FOO" 40 | end 41 | end 42 | 43 | context "with a block" do 44 | it "transforms the value" do 45 | allow(controller).to receive(:params).and_return({ "word" => "FOO" }) 46 | controller.param! :word, String, transform: lambda { |n| n.downcase } 47 | expect(controller.params["word"]).to eq "foo" 48 | end 49 | 50 | it "transforms default value" do 51 | allow(controller).to receive(:params).and_return({}) 52 | controller.param! :word, String, default: "foo", transform: lambda { |n| n.upcase } 53 | expect(controller.params["word"]).to eq "FOO" 54 | end 55 | 56 | it "transforms falsey value" do 57 | allow(controller).to receive(:params).and_return({ "foo" => "0" }) 58 | controller.param! :foo, :boolean, transform: lambda { |n| n ? "bar" : "no bar" } 59 | expect(controller.params["foo"]).to eq "no bar" 60 | end 61 | end 62 | 63 | context "when param is required & not present" do 64 | it "doesn't transform the value" do 65 | allow(controller).to receive(:params).and_return({ "foo" => nil }) 66 | expect { controller.param! :foo, String, required: true, transform: :upcase }.to( 67 | raise_error(RailsParam::InvalidParameterError, "Parameter foo is required") do |error| 68 | expect(error.param).to eq "foo" 69 | end 70 | ) 71 | end 72 | end 73 | 74 | context "when param is optional & not present" do 75 | it "doesn't transform the value" do 76 | allow(controller).to receive(:params).and_return({ }) 77 | expect { controller.param! :foo, String, transform: :upcase }.not_to raise_error 78 | end 79 | end 80 | end 81 | 82 | describe "default" do 83 | context "with a value" do 84 | it "defaults to the value" do 85 | allow(controller).to receive(:params).and_return({}) 86 | controller.param! :word, String, default: "foo" 87 | expect(controller.params["word"]).to eq "foo" 88 | end 89 | 90 | it "does not default to the value if value already provided" do 91 | allow(controller).to receive(:params).and_return({ "word" => "bar" }) 92 | controller.param! :word, String, default: "foo" 93 | expect(controller.params["word"]).to eq "bar" 94 | end 95 | end 96 | 97 | context "with a block" do 98 | it "defaults to the block value" do 99 | allow(controller).to receive(:params).and_return({}) 100 | controller.param! :foo, :boolean, default: lambda { false } 101 | expect(controller.params["foo"]).to eq false 102 | end 103 | 104 | it "does not default to the value if value already provided" do 105 | allow(controller).to receive(:params).and_return({ "foo" => "bar" }) 106 | controller.param! :foo, String, default: lambda { 'not bar' } 107 | expect(controller.params["foo"]).to eq "bar" 108 | end 109 | end 110 | end 111 | 112 | describe "coerce" do 113 | describe "String" do 114 | it "will convert to String" do 115 | allow(controller).to receive(:params).and_return({ "foo" => :bar }) 116 | controller.param! :foo, String 117 | expect(controller.params["foo"]).to eq "bar" 118 | end 119 | end 120 | 121 | describe "Integer" do 122 | it "will convert to Integer if the value is valid" do 123 | allow(controller).to receive(:params).and_return({ "foo" => "42" }) 124 | controller.param! :foo, Integer 125 | expect(controller.params["foo"]).to eq 42 126 | end 127 | 128 | it "will raise InvalidParameterError if the value is not valid" do 129 | allow(controller).to receive(:params).and_return({ "foo" => "notInteger" }) 130 | expect { controller.param! :foo, Integer }.to( 131 | raise_error(RailsParam::InvalidParameterError, "'notInteger' is not a valid Integer") do |error| 132 | expect(error.param).to eq "foo" 133 | end 134 | ) 135 | end 136 | 137 | it "will raise InvalidParameterError if the value is a boolean" do 138 | allow(controller).to receive(:params).and_return({ "foo" => true }) 139 | expect { controller.param! :foo, Integer }.to( 140 | raise_error(RailsParam::InvalidParameterError, "'true' is not a valid Integer") do |error| 141 | expect(error.param).to eq "foo" 142 | end 143 | ) 144 | end 145 | end 146 | 147 | describe "Float" do 148 | it "will convert to Float" do 149 | allow(controller).to receive(:params).and_return({ "foo" => "42.22" }) 150 | controller.param! :foo, Float 151 | expect(controller.params["foo"]).to eq 42.22 152 | end 153 | 154 | it "will raise InvalidParameterError if the value is not valid" do 155 | allow(controller).to receive(:params).and_return({ "foo" => "notFloat" }) 156 | expect { controller.param! :foo, Float }.to( 157 | raise_error(RailsParam::InvalidParameterError, "'notFloat' is not a valid Float") do |error| 158 | expect(error.param).to eq "foo" 159 | end 160 | ) 161 | end 162 | 163 | it "will raise InvalidParameterError if the value is a boolean" do 164 | allow(controller).to receive(:params).and_return({ "foo" => true }) 165 | expect { controller.param! :foo, Float }.to( 166 | raise_error(RailsParam::InvalidParameterError, "'true' is not a valid Float") do |error| 167 | expect(error.param).to eq "foo" 168 | end 169 | ) 170 | end 171 | end 172 | 173 | describe "Array" do 174 | it "will convert to Array" do 175 | allow(controller).to receive(:params).and_return({ "foo" => "2,3,4,5" }) 176 | controller.param! :foo, Array 177 | expect(controller.params["foo"]).to eq ["2", "3", "4", "5"] 178 | end 179 | 180 | it "will raise InvalidParameterError if the value is a boolean" do 181 | allow(controller).to receive(:params).and_return({ "foo" => true }) 182 | expect { controller.param! :foo, Array }.to( 183 | raise_error(RailsParam::InvalidParameterError, "'true' is not a valid Array") do |error| 184 | expect(error.param).to eq "foo" 185 | end 186 | ) 187 | end 188 | end 189 | 190 | describe "Hash" do 191 | it "will convert to Hash" do 192 | allow(controller).to receive(:params).and_return({ "foo" => "key1:foo,key2:bar" }) 193 | controller.param! :foo, Hash 194 | expect(controller.params["foo"]).to eq({ "key1" => "foo", "key2" => "bar" }) 195 | end 196 | 197 | it "will raise InvalidParameterError if the value is a boolean" do 198 | allow(controller).to receive(:params).and_return({ "foo" => true }) 199 | expect { controller.param! :foo, Hash }.to( 200 | raise_error(RailsParam::InvalidParameterError, "'true' is not a valid Hash") do |error| 201 | expect(error.param).to eq "foo" 202 | end 203 | ) 204 | end 205 | end 206 | 207 | describe "Date" do 208 | context "default condition" do 209 | it "will convert to DateTime" do 210 | allow(controller).to receive(:params).and_return({ "foo" => "1984-01-10" }) 211 | controller.param! :foo, Date 212 | expect(controller.params["foo"]).to eq Date.new(1984, 1, 10) 213 | end 214 | 215 | it "will raise InvalidParameterError if the value is not valid" do 216 | allow(controller).to receive(:params).and_return({ "foo" => "notDate" }) 217 | expect { controller.param! :foo, Date }.to( 218 | raise_error(RailsParam::InvalidParameterError, "'notDate' is not a valid Date") do |error| 219 | expect(error.param).to eq "foo" 220 | end 221 | ) 222 | end 223 | end 224 | 225 | context "with format" do 226 | it "will convert to DateTime" do 227 | allow(controller).to receive(:params).and_return({ "foo" => "1984-01-10T12:25:00.000+02:00" }) 228 | controller.param! :foo, Date, format: "%F" 229 | expect(controller.params["foo"]).to eq Date.new(1984, 1, 10) 230 | end 231 | 232 | it "will raise InvalidParameterError if the value is not valid" do 233 | allow(controller).to receive(:params).and_return({ "foo" => "notDate" }) 234 | expect { controller.param! :foo, DateTime, format: "%F" }.to( 235 | raise_error(RailsParam::InvalidParameterError, "'notDate' is not a valid DateTime") do |error| 236 | expect(error.param).to eq "foo" 237 | end 238 | ) 239 | end 240 | 241 | it "will raise InvalidParameterError if the format is not valid" do 242 | allow(controller).to receive(:params).and_return({ "foo" => "1984-01-10" }) 243 | expect { controller.param! :foo, DateTime, format: "%x" }.to( 244 | raise_error(RailsParam::InvalidParameterError, "'1984-01-10' is not a valid DateTime") do |error| 245 | expect(error.param).to eq "foo" 246 | end 247 | ) 248 | end 249 | end 250 | end 251 | 252 | describe "Time" do 253 | context "default condition" do 254 | it "will convert to Time" do 255 | allow(controller).to receive(:params).and_return({ "foo" => "2014-08-07T12:25:00.000+02:00" }) 256 | controller.param! :foo, Time 257 | expect(controller.params["foo"]).to eq Time.new(2014, 8, 7, 12, 25, 0, 7200) 258 | end 259 | 260 | it "will raise InvalidParameterError if the value is not valid" do 261 | allow(controller).to receive(:params).and_return({ "foo" => "notTime" }) 262 | expect { controller.param! :foo, Time }.to( 263 | raise_error(RailsParam::InvalidParameterError, "'notTime' is not a valid Time") do |error| 264 | expect(error.param).to eq "foo" 265 | end 266 | ) 267 | end 268 | end 269 | 270 | context "with format" do 271 | it "will convert to Time" do 272 | allow(controller).to receive(:params).and_return({ "foo" => "2014-08-07T12:25:00.000+02:00" }) 273 | controller.param! :foo, Time, format: "%F" 274 | expect(controller.params["foo"]).to eq Time.new(2014, 8, 7) 275 | end 276 | 277 | it "will raise InvalidParameterError if the value is not valid" do 278 | allow(controller).to receive(:params).and_return({ "foo" => "notDate" }) 279 | expect { controller.param! :foo, Time, format: "%F" }.to( 280 | raise_error(RailsParam::InvalidParameterError, "'notDate' is not a valid Time") do |error| 281 | expect(error.param).to eq "foo" 282 | end 283 | ) 284 | end 285 | 286 | it "will raise InvalidParameterError if the format is not valid" do 287 | allow(controller).to receive(:params).and_return({ "foo" => "2014-08-07T12:25:00.000+02:00" }) 288 | expect { controller.param! :foo, Time, format: "%x" }.to( 289 | raise_error(RailsParam::InvalidParameterError, "'2014-08-07T12:25:00.000+02:00' is not a valid Time") do |error| 290 | expect(error.param).to eq "foo" 291 | end 292 | ) 293 | end 294 | end 295 | end 296 | 297 | describe "DateTime" do 298 | context "default condition" do 299 | it "will convert to DateTime" do 300 | allow(controller).to receive(:params).and_return({ "foo" => "2014-08-07T12:25:00.000+02:00" }) 301 | controller.param! :foo, DateTime 302 | expect(controller.params["foo"]).to eq DateTime.new(2014, 8, 7, 12, 25, 0, '+2') 303 | end 304 | 305 | it "will raise InvalidParameterError if the value is not valid" do 306 | allow(controller).to receive(:params).and_return({ "foo" => "notTime" }) 307 | expect { controller.param! :foo, DateTime }.to( 308 | raise_error(RailsParam::InvalidParameterError, "'notTime' is not a valid DateTime") do |error| 309 | expect(error.param).to eq "foo" 310 | end 311 | ) 312 | end 313 | end 314 | 315 | context "with format" do 316 | it "will convert to DateTime" do 317 | allow(controller).to receive(:params).and_return({ "foo" => "2014-08-07T12:25:00.000+02:00" }) 318 | controller.param! :foo, DateTime, format: "%F" 319 | expect(controller.params["foo"]).to eq DateTime.new(2014, 8, 7) 320 | end 321 | 322 | it "will raise InvalidParameterError if the value is not valid" do 323 | allow(controller).to receive(:params).and_return({ "foo" => "notDate" }) 324 | expect { controller.param! :foo, DateTime, format: "%F" }.to( 325 | raise_error(RailsParam::InvalidParameterError, "'notDate' is not a valid DateTime") do |error| 326 | expect(error.param).to eq "foo" 327 | end 328 | ) 329 | end 330 | 331 | it "will raise InvalidParameterError if the format is not valid" do 332 | allow(controller).to receive(:params).and_return({ "foo" => "2014-08-07T12:25:00.000+02:00" }) 333 | expect { controller.param! :foo, DateTime, format: "%x" }.to( 334 | raise_error(RailsParam::InvalidParameterError, "'2014-08-07T12:25:00.000+02:00' is not a valid DateTime") do |error| 335 | expect(error.param).to eq "foo" 336 | end 337 | ) 338 | end 339 | end 340 | end 341 | 342 | describe "BigDecimals" do 343 | it "converts to BigDecimal using default precision" do 344 | allow(controller).to receive(:params).and_return({ "foo" => 12345.67890123456 }) 345 | controller.param! :foo, BigDecimal 346 | expect(controller.params["foo"]).to eq 12345.678901235 347 | end 348 | 349 | it "converts to BigDecimal using precision option" do 350 | allow(controller).to receive(:params).and_return({ "foo" => 12345.6789 }) 351 | controller.param! :foo, BigDecimal, precision: 6 352 | expect(controller.params["foo"]).to eq 12345.7 353 | end 354 | 355 | it "converts formatted currency string to big decimal" do 356 | allow(controller).to receive(:params).and_return({ "foo" => "$100,000" }) 357 | controller.param! :foo, BigDecimal 358 | expect(controller.params["foo"]).to eq 100000.0 359 | end 360 | end 361 | 362 | describe "booleans" do 363 | it "converts 1/0" do 364 | allow(controller).to receive(:params).and_return({ "foo" => "1" }) 365 | controller.param! :foo, TrueClass 366 | expect(controller.params["foo"]).to eq true 367 | 368 | allow(controller).to receive(:params).and_return({ "foo" => "0" }) 369 | controller.param! :foo, TrueClass 370 | expect(controller.params["foo"]).to eq false 371 | end 372 | 373 | it "converts true/false" do 374 | allow(controller).to receive(:params).and_return({ "foo" => "true" }) 375 | controller.param! :foo, TrueClass 376 | expect(controller.params["foo"]).to eq true 377 | 378 | allow(controller).to receive(:params).and_return({ "foo" => "false" }) 379 | controller.param! :foo, TrueClass 380 | expect(controller.params["foo"]).to eq false 381 | end 382 | 383 | it "converts t/f" do 384 | allow(controller).to receive(:params).and_return({ "foo" => "t" }) 385 | controller.param! :foo, TrueClass 386 | expect(controller.params["foo"]).to eq true 387 | 388 | allow(controller).to receive(:params).and_return({ "foo" => "f" }) 389 | controller.param! :foo, TrueClass 390 | expect(controller.params["foo"]).to eq false 391 | end 392 | 393 | it "converts yes/no" do 394 | allow(controller).to receive(:params).and_return({ "foo" => "yes" }) 395 | controller.param! :foo, TrueClass 396 | expect(controller.params["foo"]).to eq true 397 | 398 | allow(controller).to receive(:params).and_return({ "foo" => "no" }) 399 | controller.param! :foo, TrueClass 400 | expect(controller.params["foo"]).to eq false 401 | end 402 | 403 | it "converts y/n" do 404 | allow(controller).to receive(:params).and_return({ "foo" => "y" }) 405 | controller.param! :foo, TrueClass 406 | expect(controller.params["foo"]).to eq true 407 | 408 | allow(controller).to receive(:params).and_return({ "foo" => "n" }) 409 | controller.param! :foo, TrueClass 410 | expect(controller.params["foo"]).to eq false 411 | end 412 | 413 | it "return InvalidParameterError if value not boolean" do 414 | allow(controller).to receive(:params).and_return({ "foo" => "1111" }) 415 | expect { controller.param! :foo, :boolean }.to( 416 | raise_error(RailsParam::InvalidParameterError, "'1111' is not a valid boolean") do |error| 417 | expect(error.param).to eq "foo" 418 | end 419 | ) 420 | end 421 | 422 | it "set default boolean" do 423 | allow(controller).to receive(:params).and_return({}) 424 | controller.param! :foo, :boolean, default: false 425 | expect(controller.params["foo"]).to eq false 426 | end 427 | end 428 | 429 | describe "Arrays" do 430 | it "will handle nil" do 431 | allow(controller).to receive(:params).and_return({ "foo" => nil }) 432 | expect { controller.param! :foo, Array }.not_to raise_error 433 | end 434 | end 435 | 436 | describe "UploadedFiles" do 437 | it "will handle nil" do 438 | allow(controller).to receive(:params).and_return({ "foo" => nil }) 439 | expect { controller.param! :foo, ActionDispatch::Http::UploadedFile }.not_to raise_error 440 | end 441 | end 442 | end 443 | 444 | describe 'validating nested hash' do 445 | it 'typecasts nested attributes' do 446 | allow(controller).to receive(:params).and_return({ 'foo' => { 'bar' => 1, 'baz' => 2 } }) 447 | controller.param! :foo, Hash do |p| 448 | p.param! :bar, BigDecimal 449 | p.param! :baz, Float 450 | end 451 | expect(controller.params['foo']['bar']).to be_instance_of BigDecimal 452 | expect(controller.params['foo']['baz']).to be_instance_of Float 453 | end 454 | 455 | it 'does not raise exception if hash is not required but nested attributes are, and no hash is provided' do 456 | allow(controller).to receive(:params).and_return(foo: nil) 457 | controller.param! :foo, Hash do |p| 458 | p.param! :bar, BigDecimal, required: true 459 | p.param! :baz, Float, required: true 460 | end 461 | expect(controller.params['foo']).to be_nil 462 | end 463 | 464 | it 'raises exception if hash is required, nested attributes are not required, and no hash is provided' do 465 | allow(controller).to receive(:params).and_return(foo: nil) 466 | 467 | expect { 468 | controller.param! :foo, Hash, required: true do |p| 469 | p.param! :bar, BigDecimal 470 | p.param! :baz, Float 471 | end 472 | }.to raise_error(RailsParam::InvalidParameterError, "Parameter foo is required") do |error| 473 | expect(error.param).to eq "foo" 474 | end 475 | end 476 | 477 | it 'raises exception if hash is not required but nested attributes are, and hash has missing attributes' do 478 | allow(controller).to receive(:params).and_return({ 'foo' => { 'bar' => 1, 'baz' => nil } }) 479 | expect { 480 | controller.param! :foo, Hash do |p| 481 | p.param! :bar, BigDecimal, required: true 482 | p.param! :baz, Float, required: true 483 | end 484 | }.to raise_error(RailsParam::InvalidParameterError, "Parameter foo[baz] is required") do |error| 485 | expect(error.param).to eq "foo[baz]" 486 | end 487 | end 488 | end 489 | 490 | describe 'validating arrays' do 491 | it 'typecasts array of primitive elements' do 492 | allow(controller).to receive(:params).and_return({ 'array' => ['1', '2'] }) 493 | controller.param! :array, Array do |a, i| 494 | a.param! i, Integer, required: true 495 | end 496 | expect(controller.params['array'][0]).to be_a Integer 497 | expect(controller.params['array'][1]).to be_a Integer 498 | end 499 | 500 | it 'validates array of hashes' do 501 | params = { 'array' => [{ 'object' => { 'num' => '1', 'float' => '1.5' } }, { 'object' => { 'num' => '2', 'float' => '2.3' } }] } 502 | allow(controller).to receive(:params).and_return(params) 503 | controller.param! :array, Array do |a| 504 | a.param! :object, Hash do |h| 505 | h.param! :num, Integer, required: true 506 | h.param! :float, Float, required: true 507 | end 508 | end 509 | expect(controller.params['array'][0]['object']['num']).to be_a Integer 510 | expect(controller.params['array'][0]['object']['float']).to be_instance_of Float 511 | expect(controller.params['array'][1]['object']['num']).to be_a Integer 512 | expect(controller.params['array'][1]['object']['float']).to be_instance_of Float 513 | end 514 | 515 | it 'validates an array of arrays' do 516 | params = { 'array' => [['1', '2'], ['3', '4']] } 517 | allow(controller).to receive(:params).and_return(params) 518 | controller.param! :array, Array do |a, i| 519 | a.param! i, Array do |b, e| 520 | b.param! e, Integer, required: true 521 | end 522 | end 523 | expect(controller.params['array'][0][0]).to be_a Integer 524 | expect(controller.params['array'][0][1]).to be_a Integer 525 | expect(controller.params['array'][1][0]).to be_a Integer 526 | expect(controller.params['array'][1][1]).to be_a Integer 527 | end 528 | 529 | it 'raises exception when primitive element missing' do 530 | allow(controller).to receive(:params).and_return({ 'array' => ['1', nil] }) 531 | expect { 532 | controller.param! :array, Array do |a, i| 533 | a.param! i, Integer, required: true 534 | end 535 | }.to raise_error(RailsParam::InvalidParameterError, "Parameter array[1] is required") do |error| 536 | expect(error.param).to eq "array[1]" 537 | end 538 | end 539 | 540 | it 'raises exception when nested hash element missing' do 541 | params = { 'array' => [{ 'object' => { 'num' => '1', 'float' => nil } }, { 'object' => { 'num' => '2', 'float' => '2.3' } }] } 542 | allow(controller).to receive(:params).and_return(params) 543 | expect { 544 | controller.param! :array, Array do |a| 545 | a.param! :object, Hash do |h| 546 | h.param! :num, Integer, required: true 547 | h.param! :float, Float, required: true 548 | end 549 | end 550 | }.to raise_error(RailsParam::InvalidParameterError, "Parameter array[0][object][float] is required") do |error| 551 | expect(error.param).to eq "array[0][object][float]" 552 | end 553 | end 554 | 555 | it 'raises exception when nested array element missing' do 556 | params = { 'array' => [['1', '2'], ['3', nil]] } 557 | allow(controller).to receive(:params).and_return(params) 558 | expect { 559 | controller.param! :array, Array do |a, i| 560 | a.param! i, Array do |b, e| 561 | b.param! e, Integer, required: true 562 | end 563 | end 564 | }.to raise_error(RailsParam::InvalidParameterError, 'Parameter array[1][1] is required') do |error| 565 | expect(error.param).to eq "array[1][1]" 566 | end 567 | end 568 | 569 | it 'does not raise exception if array is not required but nested attributes are, and no array is provided' do 570 | allow(controller).to receive(:params).and_return(foo: nil) 571 | controller.param! :foo, Array do |p| 572 | p.param! :bar, BigDecimal, required: true 573 | p.param! :baz, Float, required: true 574 | end 575 | expect(controller.params['foo']).to be_nil 576 | end 577 | 578 | it 'raises exception if array is required, nested attributes are not required, and no array is provided' do 579 | allow(controller).to receive(:params).and_return(foo: nil) 580 | expect { 581 | controller.param! :foo, Array, required: true do |p| 582 | p.param! :bar, BigDecimal 583 | p.param! :baz, Float 584 | end 585 | }.to raise_error(RailsParam::InvalidParameterError, "Parameter foo is required") do |error| 586 | expect(error.param).to eq "foo" 587 | end 588 | end 589 | end 590 | 591 | describe "validation" do 592 | describe "required parameter" do 593 | it "succeeds" do 594 | allow(controller).to receive(:params).and_return({ "price" => "50" }) 595 | expect { controller.param! :price, Integer, required: true }.to_not raise_error 596 | end 597 | 598 | it "raises" do 599 | allow(controller).to receive(:params).and_return({}) 600 | expect { controller.param! :price, Integer, required: true }.to( 601 | raise_error(RailsParam::InvalidParameterError, "Parameter price is required") do |error| 602 | expect(error.param).to eq "price" 603 | end 604 | ) 605 | end 606 | 607 | it "raises custom message" do 608 | allow(controller).to receive(:params).and_return({}) 609 | expect { controller.param! :price, Integer, required: true, message: "No price specified" }.to( 610 | raise_error(RailsParam::InvalidParameterError, "No price specified") do |error| 611 | expect(error.param).to eq "price" 612 | end 613 | ) 614 | end 615 | end 616 | 617 | describe "blank parameter" do 618 | it "succeeds with not empty String" do 619 | allow(controller).to receive(:params).and_return({ "price" => "50" }) 620 | expect { controller.param! :price, String, blank: false }.to_not raise_error 621 | end 622 | 623 | it "raises with empty String" do 624 | allow(controller).to receive(:params).and_return({ "price" => "" }) 625 | expect { controller.param! :price, String, blank: false }.to( 626 | raise_error(RailsParam::InvalidParameterError, "Parameter price cannot be blank") do |error| 627 | expect(error.param).to eq "price" 628 | end 629 | ) 630 | end 631 | 632 | it "succeeds with not empty Hash" do 633 | allow(controller).to receive(:params).and_return({ "hash" => { "price" => "50" } }) 634 | expect { controller.param! :hash, Hash, blank: false }.to_not raise_error 635 | end 636 | 637 | it "raises with empty Hash" do 638 | allow(controller).to receive(:params).and_return({ "hash" => {} }) 639 | expect { controller.param! :hash, Hash, blank: false }.to( 640 | raise_error(RailsParam::InvalidParameterError, "Parameter hash cannot be blank") do |error| 641 | expect(error.param).to eq "hash" 642 | end 643 | ) 644 | end 645 | 646 | it "succeeds with not empty Array" do 647 | allow(controller).to receive(:params).and_return({ "array" => [50] }) 648 | expect { controller.param! :array, Array, blank: false }.to_not raise_error 649 | end 650 | 651 | it "raises with empty Array" do 652 | allow(controller).to receive(:params).and_return({ "array" => [] }) 653 | expect { controller.param! :array, Array, blank: false }.to( 654 | raise_error(RailsParam::InvalidParameterError, "Parameter array cannot be blank") do |error| 655 | expect(error.param).to eq "array" 656 | end 657 | ) 658 | end 659 | 660 | it "succeeds with not empty ActiveController::Parameters" do 661 | allow(controller).to receive(:params).and_return({ "hash" => ActionController::Parameters.new({ "price" => "50" }) }) 662 | expect { controller.param! :hash, Hash, blank: false }.to_not raise_error 663 | end 664 | 665 | it "raises with empty ActiveController::Parameters" do 666 | allow(controller).to receive(:params).and_return({ "hash" => ActionController::Parameters.new }) 667 | expect { controller.param! :hash, Hash, blank: false }.to( 668 | raise_error(RailsParam::InvalidParameterError, "Parameter hash cannot be blank") do |error| 669 | expect(error.param).to eq "hash" 670 | end 671 | ) 672 | end 673 | end 674 | 675 | describe "format parameter" do 676 | it "succeeds" do 677 | allow(controller).to receive(:params).and_return({ "price" => "50$" }) 678 | expect { controller.param! :price, String, format: /[0-9]+\$/ }.to_not raise_error 679 | end 680 | 681 | it "raises" do 682 | allow(controller).to receive(:params).and_return({ "price" => "50" }) 683 | expect { controller.param! :price, String, format: /[0-9]+\$/ }.to( 684 | raise_error(RailsParam::InvalidParameterError, "Parameter price must match format #{/[0-9]+\$/}") do |error| 685 | expect(error.param).to eq "price" 686 | end 687 | ) 688 | end 689 | end 690 | 691 | describe "is parameter" do 692 | it "succeeds" do 693 | allow(controller).to receive(:params).and_return({ "price" => "50" }) 694 | expect { controller.param! :price, String, is: "50" }.to_not raise_error 695 | end 696 | 697 | it "raises" do 698 | allow(controller).to receive(:params).and_return({ "price" => "51" }) 699 | expect { controller.param! :price, String, is: "50" }.to( 700 | raise_error(RailsParam::InvalidParameterError, "Parameter price must be 50") do |error| 701 | expect(error.param).to eq "price" 702 | end 703 | ) 704 | end 705 | end 706 | 707 | describe "min parameter" do 708 | it "succeeds" do 709 | allow(controller).to receive(:params).and_return({ "price" => "50" }) 710 | expect { controller.param! :price, Integer, min: 50 }.to_not raise_error 711 | end 712 | 713 | it "raises" do 714 | allow(controller).to receive(:params).and_return({ "price" => "50" }) 715 | expect { controller.param! :price, Integer, min: 51 }.to( 716 | raise_error(RailsParam::InvalidParameterError, "Parameter price cannot be less than 51") do |error| 717 | expect(error.param).to eq "price" 718 | end 719 | ) 720 | end 721 | end 722 | 723 | describe "max parameter" do 724 | it "succeeds" do 725 | allow(controller).to receive(:params).and_return({ "price" => "50" }) 726 | expect { controller.param! :price, Integer, max: 50 }.to_not raise_error 727 | end 728 | 729 | it "raises" do 730 | allow(controller).to receive(:params).and_return({ "price" => "50" }) 731 | expect { controller.param! :price, Integer, max: 49 }.to( 732 | raise_error(RailsParam::InvalidParameterError, "Parameter price cannot be greater than 49") do |error| 733 | expect(error.param).to eq "price" 734 | end 735 | ) 736 | end 737 | end 738 | 739 | describe "min_length parameter" do 740 | it "succeeds" do 741 | allow(controller).to receive(:params).and_return({ "word" => "foo" }) 742 | expect { controller.param! :word, String, min_length: 3 }.to_not raise_error 743 | end 744 | 745 | it "raises" do 746 | allow(controller).to receive(:params).and_return({ "word" => "foo" }) 747 | expect { controller.param! :word, String, min_length: 4 }.to( 748 | raise_error(RailsParam::InvalidParameterError, "Parameter word cannot have length less than 4") do |error| 749 | expect(error.param).to eq "word" 750 | end 751 | ) 752 | end 753 | end 754 | 755 | describe "max_length parameter" do 756 | it "succeeds" do 757 | allow(controller).to receive(:params).and_return({ "word" => "foo" }) 758 | expect { controller.param! :word, String, max_length: 3 }.to_not raise_error 759 | end 760 | 761 | it "raises" do 762 | allow(controller).to receive(:params).and_return({ "word" => "foo" }) 763 | expect { controller.param! :word, String, max_length: 2 }.to( 764 | raise_error(RailsParam::InvalidParameterError, "Parameter word cannot have length greater than 2") do |error| 765 | expect(error.param).to eq "word" 766 | end 767 | ) 768 | end 769 | end 770 | 771 | describe "in, within, range parameters" do 772 | before(:each) { allow(controller).to receive(:params).and_return({ "price" => "50" }) } 773 | 774 | it "succeeds in the range" do 775 | controller.param! :price, Integer, in: 1..100 776 | expect(controller.params["price"]).to eq 50 777 | end 778 | 779 | it "raises outside the range" do 780 | expect { controller.param! :price, Integer, in: 51..100 }.to( 781 | raise_error(RailsParam::InvalidParameterError, "Parameter price must be within 51..100") do |error| 782 | expect(error.param).to eq "price" 783 | end 784 | ) 785 | end 786 | end 787 | 788 | describe "custom validator" do 789 | let(:custom_validation) { lambda { |v| raise RailsParam::InvalidParameterError, 'Number is not even' if v % 2 != 0 } } 790 | 791 | it "succeeds when valid" do 792 | allow(controller).to receive(:params).and_return({ "number" => "50" }) 793 | controller.param! :number, Integer, custom: custom_validation 794 | expect(controller.params["number"]).to eq 50 795 | end 796 | 797 | it "raises when invalid" do 798 | allow(controller).to receive(:params).and_return({ "number" => "51" }) 799 | expect { controller.param! :number, Integer, custom: custom_validation }.to( 800 | raise_error(RailsParam::InvalidParameterError, 'Number is not even') do |error| 801 | expect(error.param).to be_nil 802 | end 803 | ) 804 | end 805 | end 806 | end 807 | end 808 | end 809 | -------------------------------------------------------------------------------- /spec/rails_param/parameter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Parameter do 4 | let(:name) { "foo" } 5 | let(:value) { "bar" } 6 | let(:options) { { fizz: :buzz } } 7 | let(:type) { String } 8 | 9 | subject { described_class.new(name: name, value: value, options: options, type: type) } 10 | 11 | describe "#should_set_default?" do 12 | context "when value is nil" do 13 | let(:value) { nil } 14 | 15 | context "and default options present" do 16 | let(:options) { { default: "foobar" } } 17 | 18 | it "returns true" do 19 | expect(subject.should_set_default?).to eq true 20 | end 21 | end 22 | 23 | context "and default options are not present" do 24 | it "returns false" do 25 | expect(subject.should_set_default?).to eq false 26 | end 27 | end 28 | end 29 | 30 | context "when value is present" do 31 | it "returns false" do 32 | expect(subject.should_set_default?).to eq false 33 | end 34 | end 35 | end 36 | 37 | describe "#set_default" do 38 | context "default options respond to .call" do 39 | let(:default_option) do 40 | double.tap do |dbl| 41 | allow(dbl).to receive(:call).and_return("foobar") 42 | end 43 | end 44 | let(:options) { { default: default_option } } 45 | 46 | it "sets the value" do 47 | subject.set_default 48 | expect(subject.value).to eq "foobar" 49 | end 50 | end 51 | 52 | context "default options does not respond to .call" do 53 | let(:options) { {default: "fizzbuzz" } } 54 | 55 | it "sets the value" do 56 | subject.set_default 57 | expect(subject.value).to eq "fizzbuzz" 58 | end 59 | end 60 | end 61 | 62 | describe "#transform" do 63 | context "with a method" do 64 | let(:options) { { transform: :upcase } } 65 | 66 | it "transforms the value" do 67 | subject.transform 68 | expect(subject.value).to eq "BAR" 69 | end 70 | end 71 | 72 | context "with a block" do 73 | let(:value) { "BAR" } 74 | let(:options) { { transform: lambda { |n| n.downcase } } } 75 | 76 | it "transforms the value" do 77 | subject.transform 78 | expect(subject.value).to eq "bar" 79 | end 80 | 81 | context "conditional block" do 82 | let(:value) { false } 83 | let(:options) { { transform: lambda { |n| n ? "foo" : "no foo" } } } 84 | 85 | it "transforms the value" do 86 | subject.transform 87 | expect(subject.value).to eq "no foo" 88 | end 89 | end 90 | end 91 | end 92 | 93 | describe "#validate" do 94 | let(:validator) { double } 95 | before :each do 96 | allow(RailsParam::Validator).to receive(:new).and_return(validator) 97 | allow(validator).to receive(:validate!) 98 | end 99 | 100 | it "passes self to Validator" do 101 | subject.validate 102 | expect(RailsParam::Validator).to have_received(:new).with(subject) 103 | end 104 | 105 | it "calls #validate!" do 106 | subject.validate 107 | expect(validator).to have_received(:validate!) 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/rails_param/validator/blank_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Validator::Required do 4 | let(:name) { "foo" } 5 | let(:options) { { blank: false } } 6 | let(:type) { String } 7 | let(:error_message) { "Parameter foo cannot be blank" } 8 | let(:parameter) do 9 | RailsParam::Parameter.new( 10 | name: name, 11 | value: value, 12 | options: options, 13 | type: type 14 | ) 15 | end 16 | 17 | subject { described_class.new(parameter) } 18 | 19 | describe "#validate!" do 20 | context "String" do 21 | context "is not empty" do 22 | let(:value) { "bar" } 23 | 24 | it_behaves_like "does not raise error" 25 | end 26 | 27 | context "is empty" do 28 | let(:value) { "" } 29 | 30 | it_behaves_like "raises InvalidParameterError" 31 | end 32 | end 33 | 34 | context "Hash" do 35 | context "is not empty" do 36 | let(:value) { { foo: :bar } } 37 | 38 | it_behaves_like "does not raise error" 39 | end 40 | 41 | context "is empty" do 42 | let(:value) { {} } 43 | 44 | it_behaves_like "raises InvalidParameterError" 45 | end 46 | end 47 | 48 | context "Array" do 49 | context "is not empty" do 50 | let(:value) { [50] } 51 | 52 | it_behaves_like "does not raise error" 53 | end 54 | 55 | context "is empty" do 56 | let(:value) { [] } 57 | 58 | it_behaves_like "raises InvalidParameterError" 59 | end 60 | end 61 | 62 | context "ActiveController::Parameters" do 63 | context "is not empty" do 64 | let(:value) do 65 | ActionController::Parameters.new({ "price" => "50" }) 66 | end 67 | 68 | it_behaves_like "does not raise error" 69 | end 70 | 71 | context "is empty" do 72 | let(:value) { ActionController::Parameters.new() } 73 | 74 | it_behaves_like "raises InvalidParameterError" 75 | end 76 | end 77 | 78 | context "Integer" do 79 | context "is not empty" do 80 | let(:value) { 50 } 81 | 82 | it_behaves_like "does not raise error" 83 | end 84 | 85 | context "is empty" do 86 | let(:value) { nil } 87 | 88 | it_behaves_like "raises InvalidParameterError" 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/rails_param/validator/custom_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Validator::Custom do 4 | let(:custom_validation) { lambda { |v| raise RailsParam::InvalidParameterError, 'Number is not even' if v % 2 != 0 } } 5 | let(:name) { "foo" } 6 | let(:options) { { custom: custom_validation } } 7 | let(:type) { String } 8 | let(:parameter) do 9 | RailsParam::Parameter.new( 10 | name: name, 11 | value: value, 12 | options: options, 13 | type: type 14 | ) 15 | end 16 | 17 | subject { described_class.new(parameter) } 18 | 19 | describe "#validate!" do 20 | context "value given is valid" do 21 | let(:value) { 50 } 22 | 23 | it_behaves_like "does not raise error" 24 | end 25 | 26 | context "value given is invalid" do 27 | let(:value) { 51 } 28 | let(:error_message) { "Number is not even" } 29 | 30 | it_behaves_like "raises InvalidParameterError" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/rails_param/validator/format_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Validator::Format do 4 | let(:format_validation) { /[0-9]+\$/ } 5 | let(:name) { "foo" } 6 | let(:options) { { format: format_validation } } 7 | let(:type) { String } 8 | let(:parameter) do 9 | RailsParam::Parameter.new( 10 | name: name, 11 | value: value, 12 | options: options, 13 | type: type 14 | ) 15 | end 16 | 17 | subject { described_class.new(parameter) } 18 | 19 | describe "#validate!" do 20 | context "value given is valid" do 21 | let(:value) { "50$" } 22 | 23 | it_behaves_like "does not raise error" 24 | end 25 | 26 | context "value given is invalid" do 27 | let(:value) { "50" } 28 | let(:error_message) { "Parameter foo must match format #{format_validation}" } 29 | 30 | it_behaves_like "raises InvalidParameterError" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/rails_param/validator/in_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Validator::In do 4 | let(:value) { 50 } 5 | let(:name) { "foo" } 6 | let(:options) { { in: in_validation } } 7 | let(:type) { Integer } 8 | let(:parameter) do 9 | RailsParam::Parameter.new( 10 | name: name, 11 | value: value, 12 | options: options, 13 | type: type 14 | ) 15 | end 16 | 17 | subject { described_class.new(parameter) } 18 | 19 | describe "#validate!" do 20 | context "value given is valid" do 21 | let(:in_validation) { 1..100 } 22 | 23 | it_behaves_like "does not raise error" 24 | end 25 | 26 | context "value given is invalid" do 27 | let(:in_validation) { 51..100 } 28 | let(:error_message) { "Parameter foo must be within 51..100" } 29 | 30 | it_behaves_like "raises InvalidParameterError" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/rails_param/validator/is_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Validator::Is do 4 | let(:name) { "foo" } 5 | let(:options) { { is: "50" } } 6 | let(:type) { String } 7 | let(:parameter) do 8 | RailsParam::Parameter.new( 9 | name: name, 10 | value: value, 11 | options: options, 12 | type: type 13 | ) 14 | end 15 | 16 | subject { described_class.new(parameter) } 17 | 18 | describe "#validate!" do 19 | context "value given is valid" do 20 | let(:value) { "50" } 21 | 22 | it_behaves_like "does not raise error" 23 | end 24 | 25 | context "value given is invalid" do 26 | let(:value) { "51" } 27 | let(:error_message) { "Parameter foo must be 50" } 28 | 29 | it_behaves_like "raises InvalidParameterError" 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/rails_param/validator/max_length_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Validator::MaxLength do 4 | let(:name) { "foo" } 5 | let(:value) { "bar" } 6 | let(:options) { { max_length: max_length } } 7 | let(:type) { String } 8 | let(:parameter) do 9 | RailsParam::Parameter.new( 10 | name: name, 11 | value: value, 12 | options: options, 13 | type: type 14 | ) 15 | end 16 | 17 | subject { described_class.new(parameter) } 18 | 19 | describe "#validate!" do 20 | context "value given is valid" do 21 | let(:max_length) { 3 } 22 | 23 | it_behaves_like "does not raise error" 24 | end 25 | 26 | context "value given is invalid" do 27 | let(:max_length) { 2 } 28 | let(:error_message) { "Parameter foo cannot have length greater than #{max_length}" } 29 | 30 | it_behaves_like "raises InvalidParameterError" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/rails_param/validator/max_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Validator::Max do 4 | let(:name) { "foo" } 5 | let(:value) { 50 } 6 | let(:options) { { max: max } } 7 | let(:type) { Integer } 8 | let(:parameter) do 9 | RailsParam::Parameter.new( 10 | name: name, 11 | value: value, 12 | options: options, 13 | type: type 14 | ) 15 | end 16 | 17 | subject { described_class.new(parameter) } 18 | 19 | describe "#validate!" do 20 | context "value given is valid" do 21 | let(:max) { 50 } 22 | 23 | it_behaves_like "does not raise error" 24 | end 25 | 26 | context "value given is invalid" do 27 | let(:max) { 49 } 28 | let(:error_message) { "Parameter foo cannot be greater than #{max}" } 29 | 30 | it_behaves_like "raises InvalidParameterError" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/rails_param/validator/min_length_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Validator::MinLength do 4 | let(:name) { "foo" } 5 | let(:value) { "bar" } 6 | let(:options) { { min_length: min_length } } 7 | let(:type) { String } 8 | let(:parameter) do 9 | RailsParam::Parameter.new( 10 | name: name, 11 | value: value, 12 | options: options, 13 | type: type 14 | ) 15 | end 16 | 17 | subject { described_class.new(parameter) } 18 | 19 | describe "#validate!" do 20 | context "value given is valid" do 21 | let(:min_length) { 3 } 22 | 23 | it_behaves_like "does not raise error" 24 | end 25 | 26 | context "value given is invalid" do 27 | let(:min_length) { 44 } 28 | let(:error_message) { "Parameter foo cannot have length less than #{min_length}" } 29 | 30 | it_behaves_like "raises InvalidParameterError" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/rails_param/validator/min_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Validator::Min do 4 | let(:name) { "foo" } 5 | let(:value) { 50 } 6 | let(:options) { { min: min } } 7 | let(:type) { Integer } 8 | let(:parameter) do 9 | RailsParam::Parameter.new( 10 | name: name, 11 | value: value, 12 | options: options, 13 | type: type 14 | ) 15 | end 16 | 17 | subject { described_class.new(parameter) } 18 | 19 | describe "#validate!" do 20 | context "value given is valid" do 21 | let(:min) { 50 } 22 | 23 | it_behaves_like "does not raise error" 24 | end 25 | 26 | context "value given is invalid" do 27 | let(:min) { 51 } 28 | let(:error_message) { "Parameter foo cannot be less than #{min}" } 29 | 30 | it_behaves_like "raises InvalidParameterError" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/rails_param/validator/required_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Validator::Required do 4 | let(:name) { "foo" } 5 | let(:options) { { required: true } } 6 | let(:type) { String } 7 | let(:parameter) do 8 | RailsParam::Parameter.new( 9 | name: name, 10 | value: value, 11 | options: options, 12 | type: type 13 | ) 14 | end 15 | 16 | subject { described_class.new(parameter) } 17 | 18 | describe "#validate!" do 19 | context "value given is present" do 20 | let(:value) { "bar" } 21 | 22 | it_behaves_like "does not raise error" 23 | end 24 | 25 | context "value given is not nil but is also not present" do 26 | let(:type) { Hash } 27 | let(:value) { {} } 28 | 29 | it_behaves_like "does not raise error" 30 | end 31 | 32 | context "value is not present" do 33 | let(:error_message) { "Parameter foo is required" } 34 | let(:value) { nil } 35 | 36 | it_behaves_like "raises InvalidParameterError" 37 | 38 | context "with a custom message" do 39 | let(:error_message) { "No price specified." } 40 | let(:options) { { required: true, message: error_message } } 41 | 42 | it_behaves_like "raises InvalidParameterError" 43 | end 44 | end 45 | 46 | context "parameter is not required" do 47 | let(:options) { { required: false } } 48 | 49 | context "value is not present" do 50 | let(:value) { nil } 51 | 52 | it_behaves_like "does not raise error" 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/rails_param/validator/shared_examples/does_not_raise_error.rb: -------------------------------------------------------------------------------- 1 | RSpec.shared_examples "does not raise error" do 2 | it "does not raise error" do 3 | expect { subject.validate! }.to_not raise_error 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/rails_param/validator/shared_examples/raises_invalid_parameter_error.rb: -------------------------------------------------------------------------------- 1 | RSpec.shared_examples "raises InvalidParameterError" do 2 | it "raises error with message" do 3 | expect { subject.validate! }.to raise_error(RailsParam::InvalidParameterError, error_message) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/rails_param/validator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RailsParam::Validator do 4 | let(:name) { "foo" } 5 | let(:value) { "bar" } 6 | let(:options) { { required: true } } 7 | let(:type) { String } 8 | let(:parameter) do 9 | RailsParam::Parameter.new( 10 | name: name, 11 | value: value, 12 | options: options, 13 | type: type 14 | ) 15 | end 16 | 17 | subject { described_class.new(parameter) } 18 | 19 | let(:validator_class) { RailsParam::Validator::Required } 20 | let(:validator_double) { double } 21 | 22 | before :each do 23 | allow(validator_double).to receive(:valid!) 24 | allow(validator_class).to receive(:new).and_return(validator_double) 25 | end 26 | 27 | describe "#validate!" do 28 | it "initializes a validator class based on the provided option" do 29 | subject.validate! 30 | 31 | expect(validator_class).to have_received(:new).with(parameter) 32 | end 33 | end 34 | 35 | describe "#valid!" do 36 | it "raises an InvalidParameterError if not subclassed" do 37 | expect { subject.valid! }.to raise_error RailsParam::InvalidParameterError 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/rails_param_spec.rb: -------------------------------------------------------------------------------- 1 | describe RailsParam do 2 | end 3 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | require 'active_support' 5 | require 'bigdecimal' 6 | require 'date' 7 | 8 | require 'action_controller' 9 | require 'fixtures/controllers' 10 | require 'rails_param' 11 | require 'rspec/rails' 12 | Dir["./spec/rails_param/validator/shared_examples/**/*.rb"].sort.each { |f| require f } 13 | --------------------------------------------------------------------------------