Commit 5799d1a7 by Nicolas Blanco

Merge pull request #7 from stantoncbradley/master

Hash, array, nested and BigDecimal feature
parents c6d98432 d594af0f
...@@ -47,7 +47,7 @@ end ...@@ -47,7 +47,7 @@ end
### Parameter Types ### Parameter Types
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`. 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`.
- `String` - `String`
- `Integer` - `Integer`
...@@ -56,6 +56,7 @@ By declaring parameter types, incoming parameters will automatically be transfor ...@@ -56,6 +56,7 @@ By declaring parameter types, incoming parameters will automatically be transfor
- `Array` _("1,2,3,4,5")_ - `Array` _("1,2,3,4,5")_
- `Hash` _("key1:value1,key2:value2")_ - `Hash` _("key1:value1,key2:value2")_
- `Date`, `Time`, & `DateTime` - `Date`, `Time`, & `DateTime`
- `BigDecimal` _("$1,000,000")_
### Validations ### Validations
...@@ -85,6 +86,44 @@ param! :order, String, in: ["ASC", "DESC"], transform: :upcase, default: "ASC" ...@@ -85,6 +86,44 @@ param! :order, String, in: ["ASC", "DESC"], transform: :upcase, default: "ASC"
param! :offset, Integer, min: 0, transform: lambda {|n| n - (n % 10)} param! :offset, Integer, min: 0, transform: lambda {|n| n - (n % 10)}
``` ```
### Nested Attributes
rails_param allows you to apply any of the above mentioned validations to attributes nested in hashes:
```ruby
param! :book, Hash do |b|
b.param! :title, String, blank: false
b.param! :price, BigDecimal, precision: 4, required: true
b.param! :author, Hash, required: true do |a|
a.param! :first_name, String
a.param! :last_name, String, blank: false
end
end
```
### Arrays
Validate every element of your array, including nested hashes and arrays:
```ruby
# primitive datatype syntax
param! :integer_array, Array do |array,index|
array.param! index, Integer, required: true
end
# complex array
param! :books_array, Array, required: true do |b|
b.param! :title, String, blank: false
b.param! :author, Hash, required: true, do |a|
a.param! :first_name, String
a.param! :last_name, String, required: true
end
b.param! :subjects, Array do |s,i|
s.param! i, String, blank: false
end
end
```
## Thank you ## Thank you
Many thanks to: Many thanks to:
......
module RailsParam module RailsParam
module Param module Param
DEFAULT_PRECISION = 14
class InvalidParameterError < StandardError class InvalidParameterError < StandardError
attr_accessor :param, :options attr_accessor :param, :options
end end
def param!(name, type, options = {}) class MockController
name = name.to_s include RailsParam::Param
attr_accessor :params
end
def param!(name, type, options = {}, &block)
name = name.to_s unless name.is_a? Integer # keep index for validating elements
return unless params.member?(name) || options[:default].present? || options[:required] return unless params.member?(name) || options[:default].present? || options[:required]
...@@ -15,8 +22,25 @@ module RailsParam ...@@ -15,8 +22,25 @@ module RailsParam
params[name] = (options[:default].call if options[:default].respond_to?(:call)) || options[:default] if params[name].nil? and options[:default] params[name] = (options[:default].call if options[:default].respond_to?(:call)) || options[:default] if params[name].nil? and options[:default]
params[name] = options[:transform].to_proc.call(params[name]) if params[name] and options[:transform] params[name] = options[:transform].to_proc.call(params[name]) if params[name] and options[:transform]
validate!(params[name], options) validate!(params[name], options)
if block_given?
if type == Array
params[name].each_with_index do |element, i|
if element.is_a?(Hash)
recurse element, &block
else
params[name][i] = recurse({ i => element }, i, &block) # supply index as key unless value is hash
end
end
else
recurse params[name], &block
end
end
params[name]
rescue InvalidParameterError => exception rescue InvalidParameterError => exception
exception.param, exception.options = name, options exception.param ||= name
exception.options ||= options
raise exception raise exception
end end
end end
...@@ -41,6 +65,13 @@ module RailsParam ...@@ -41,6 +65,13 @@ module RailsParam
private private
def recurse(params, index = nil)
raise InvalidParameterError, 'no block given' unless block_given?
controller = RailsParam::Param::MockController.new
controller.params = params
yield(controller, index)
end
def coerce(param, type, options = {}) def coerce(param, type, options = {})
begin begin
return nil if param.nil? return nil if param.nil?
...@@ -52,8 +83,12 @@ module RailsParam ...@@ -52,8 +83,12 @@ module RailsParam
return Time.parse(param) if type == Time return Time.parse(param) if type == Time
return DateTime.parse(param) if type == DateTime return DateTime.parse(param) if type == DateTime
return Array(param.split(options[:delimiter] || ",")) if type == Array return Array(param.split(options[:delimiter] || ",")) if type == Array
return Hash[param.split(options[:delimiter] || ",").map{|c| c.split(options[:separator] || ":")}] if type == Hash return Hash[param.split(options[:delimiter] || ",").map { |c| c.split(options[:separator] || ":") }] if type == Hash
return (/(false|f|no|n|0)$/i === param.to_s ? false : (/(true|t|yes|y|1)$/i === param.to_s ? true : nil)) if type == TrueClass || type == FalseClass || type == :boolean return (/(false|f|no|n|0)$/i === param.to_s ? false : (/(true|t|yes|y|1)$/i === param.to_s ? true : nil)) if type == TrueClass || type == FalseClass || type == :boolean
if type == BigDecimal
param = param.delete('$,').strip.to_f if param.is_a?(String)
return BigDecimal.new(param, (options[:precision] || DEFAULT_PRECISION))
end
return nil return nil
rescue ArgumentError rescue ArgumentError
raise InvalidParameterError, "'#{param}' is not a valid #{type}" raise InvalidParameterError, "'#{param}' is not a valid #{type}"
...@@ -63,37 +98,37 @@ module RailsParam ...@@ -63,37 +98,37 @@ module RailsParam
def validate!(param, options) def validate!(param, options)
options.each do |key, value| options.each do |key, value|
case key case key
when :required when :required
raise InvalidParameterError, "Parameter is required" if value && param.nil? raise InvalidParameterError, "Parameter is required" if value && param.nil?
when :blank when :blank
raise InvalidParameterError, "Parameter cannot be blank" if !value && case param raise InvalidParameterError, "Parameter cannot be blank" if !value && case param
when String when String
!(/\S/ === param) !(/\S/ === param)
when Array, Hash when Array, Hash
param.empty? param.empty?
else else
param.nil? param.nil?
end end
when :format when :format
raise InvalidParameterError, "Parameter must be a string if using the format validation" unless param.kind_of?(String) raise InvalidParameterError, "Parameter must be a string if using the format validation" unless param.kind_of?(String)
raise InvalidParameterError, "Parameter must match format #{value}" unless param =~ value raise InvalidParameterError, "Parameter must match format #{value}" unless param =~ value
when :is when :is
raise InvalidParameterError, "Parameter must be #{value}" unless param === value raise InvalidParameterError, "Parameter must be #{value}" unless param === value
when :in, :within, :range when :in, :within, :range
raise InvalidParameterError, "Parameter must be within #{value}" unless param.nil? || case value raise InvalidParameterError, "Parameter must be within #{value}" unless param.nil? || case value
when Range when Range
value.include?(param) value.include?(param)
else else
Array(value).include?(param) Array(value).include?(param)
end end
when :min when :min
raise InvalidParameterError, "Parameter cannot be less than #{value}" unless param.nil? || value <= param raise InvalidParameterError, "Parameter cannot be less than #{value}" unless param.nil? || value <= param
when :max when :max
raise InvalidParameterError, "Parameter cannot be greater than #{value}" unless param.nil? || value >= param raise InvalidParameterError, "Parameter cannot be greater than #{value}" unless param.nil? || value >= param
when :min_length when :min_length
raise InvalidParameterError, "Parameter cannot have length less than #{value}" unless param.nil? || value <= param.length raise InvalidParameterError, "Parameter cannot have length less than #{value}" unless param.nil? || value <= param.length
when :max_length when :max_length
raise InvalidParameterError, "Parameter cannot have length greater than #{value}" unless param.nil? || value >= param.length raise InvalidParameterError, "Parameter cannot have length greater than #{value}" unless param.nil? || value >= param.length
end end
end end
end end
......
...@@ -17,4 +17,18 @@ class FakeController < ActionController::Base ...@@ -17,4 +17,18 @@ class FakeController < ActionController::Base
def new def new
render text: "new" render text: "new"
end end
def edit
param! :book, Hash, required: true do |b|
b.param! :title, String, required: true
b.param! :author, Hash do |a|
a.param! :first_name, String, required: true
a.param! :last_name, String, required: true
a.param! :age, Integer, required: true
end
b.param! :price, BigDecimal, required: true
end
render text: :book
end
end end
...@@ -15,6 +15,7 @@ module Rails ...@@ -15,6 +15,7 @@ module Rails
get '/fake/new' => "fake#new" get '/fake/new' => "fake#new"
get '/fakes' => "fake#index" get '/fakes' => "fake#index"
get '/fake/(:id)' => "fake#show" get '/fake/(:id)' => "fake#show"
get '/fake/edit' => "fake#edit"
end end
@routes @routes
end end
......
...@@ -10,12 +10,60 @@ describe FakeController, type: :controller do ...@@ -10,12 +10,60 @@ describe FakeController, type: :controller do
end end
end end
describe "nested_hash" do
it "validates nested properties" do
params = {
'book' => {
'title' => 'One Hundred Years of Solitude',
'author' => {
'first_name' => 'Garbriel Garcia',
'last_name' => 'Marquez',
'age' => '70'
},
'price' => '$1,000.00'
}}
get :edit, params
expect(controller.params[:book][:author][:age]).to eql 70
expect(controller.params[:book][:author][:age]).to be_kind_of Integer
expect(controller.params[:book][:price]).to eql 1000.0
expect(controller.params[:book][:price]).to be_instance_of BigDecimal
end
it "raises error when required nested attribute missing" do
params = {
'book' => {
'title' => 'One Hundred Years of Solitude',
'author' => {
'last_name' => 'Marquez',
'age' => '70'
},
'price' => '$1,000.00'
}}
expect { get :edit, params }.to raise_error { |error|
expect(error).to be_a(RailsParam::Param::InvalidParameterError)
expect(error.param).to eql("first_name")
expect(error.options).to eql({:required => true})
}
end
it "passes when hash that's not required but has required attributes is missing" do
params = {
'book' => {
'title' => 'One Hundred Years of Solitude',
'price' => '$1,000.00'
}}
get :edit, params
expect(controller.params[:book][:price]).to eql 1000.0
expect(controller.params[:book][:price]).to be_instance_of BigDecimal
end
end
describe "InvalidParameterError" do describe "InvalidParameterError" do
it "raises an exception with params attributes" do it "raises an exception with params attributes" do
expect { get :index, sort: "foo" }.to raise_error { |error| expect { get :index, sort: "foo" }.to raise_error { |error|
expect(error).to be_a(RailsParam::Param::InvalidParameterError) expect(error).to be_a(RailsParam::Param::InvalidParameterError)
expect(error.param).to eql("sort") expect(error.param).to eql("sort")
expect(error.options).to eql({ :in => ["asc", "desc"], :default => "asc", :transform => :downcase }) expect(error.options).to eql({:in => ["asc", "desc"], :default => "asc", :transform => :downcase})
} }
end end
end end
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment