parameters_schema 0.42

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 699ec439cfd577bce7294f3718e6a45b4e9d4652
4
+ data.tar.gz: 6bc716b44313cc86c2b10ab393c6c9ad01c6dd9a
5
+ SHA512:
6
+ metadata.gz: f538951c2a1732b9cfa90412069a53d5ad5e5907667ae0b262fdd25c018082fabfbe3b409c7c660663166145eb5e9eb88254eba89f169e8337bd35ff49226aad
7
+ data.tar.gz: 2820f485ea953e8c629b6262b42db91ed6774f94509db1417eb85d5d537025c3eea1f1ccaae645d478ef3faaf86ed1746df579da52e33c4cc5e61a7bb70afad6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'minitest'
4
+ gem 'activesupport'
data/README.md ADDED
@@ -0,0 +1,286 @@
1
+ # Parameters Schema
2
+
3
+ ## *"A strict API is the best kind of API."*
4
+
5
+ In this line of thought, **[strong_parameters](https://github.com/rails/strong_parameters)** lacks some cool validations that this gem provide.
6
+
7
+ For example, let's say you want your operation `create` to require a `Fixnum` parameter between 1 and 99. With strong_parameters, you're out of luck. With this gem, you simply write in your controller:
8
+ ``` ruby
9
+ class Api::PeopleController < Api::BaseController
10
+ def create
11
+ validated_params = validate_params params do
12
+ param :age, type: Fixnum, allow: (1..99)
13
+ end
14
+
15
+ # Do something with the validated parameters.
16
+ end
17
+ end
18
+ ```
19
+
20
+ So when you use this controller:
21
+ ``` ruby
22
+ > app.post 'api/people', age: 12 # validated_params = { age: 12 }
23
+ > app.post 'api/people', age: 100 # throws a ParameterSchema::InvalidParameters
24
+ ```
25
+
26
+ ## Why use this gem instead of strong_parameters
27
+
28
+ * You prefer a procedural approach (via a DSL) over a declarative one.
29
+ * You want more control over the parameters of your API, at the type and format level.
30
+ * You want to do things differently, you fucking hipster.
31
+
32
+ ## Installation
33
+
34
+ Add in your Gemfile:
35
+ ``` ruby
36
+ gem 'parameters_schema'
37
+ ```
38
+
39
+ Add in your project:
40
+ ``` ruby
41
+ require 'parameters_schema'
42
+ ```
43
+
44
+ ## Schema
45
+
46
+ The schema is the heart of this gem. It provides a simple DSL to express an operation's parameters.
47
+
48
+ Creating a schema:
49
+ ``` ruby
50
+ schema = ParametersSchema::Schema.new do
51
+ # Define parameters here...
52
+ # ... but an empty schema is also valid.
53
+ end
54
+ ```
55
+ Validating parameters against a schema:
56
+ ``` ruby
57
+ params = { potatoe: 'Eramosa' }
58
+ schema.validate!(params)
59
+ ```
60
+
61
+ The minimal representation of a parameter is:
62
+ ``` ruby
63
+ param :potatoe
64
+ ```
65
+ This represents a `required` parameter of type `String` accepting any characters and which doesn't allow nil or empty values.
66
+
67
+ The valid options for a parameter are:
68
+ ``` ruby
69
+ * required # Whether the parameter is required. Default: true.
70
+ * type # The type of the parameter. Default: String.
71
+ * allow # The allowed values of the parameter. Default: :any.
72
+ * deny # The denied values of the parameter. Default: :none.
73
+ * array # Whether the parameter is an array. Default: false.
74
+ ```
75
+
76
+ ### Parameter types
77
+
78
+ The available types are:
79
+ ``` ruby
80
+ * String
81
+ * Symbol
82
+ * Fixnum
83
+ * Float
84
+ * Date
85
+ * DateTime
86
+ * Array # An array of :any types.
87
+ * Hash # An object which members are not validated further.
88
+ * :boolean # See options for accepted values.
89
+ * :any # Accepts any value type.
90
+ ```
91
+
92
+ To accept more than one type, you can do:
93
+ ``` ruby
94
+ param :potatoe, type: [Boolean, String] # Accepts a boolean or string value.
95
+ ```
96
+
97
+ To accept an array of a specific type, you can do:
98
+ ``` ruby
99
+ param :potatoes, type: { Array => String } # Accepts an array of strings.
100
+ ```
101
+
102
+ To deeper refine the schema of an object, you pass a block to the parameter:
103
+ ```
104
+ param :potatoe do # Implicitly of type Hash
105
+ param :variety
106
+ param :origin
107
+ end
108
+ ```
109
+
110
+ As you have seen above, a parameter can be of type `Array` but can also have the option `array`. Confusing, right? This option was introduced to simplify the `type` syntax. For example:
111
+ ```
112
+ param :potatoes, type: String, array: true # This is equivalent...
113
+ param :potatoes, type: { Array => String } # ... to this.
114
+ ```
115
+
116
+ But this parameter truly shine with an array of objects:
117
+ ```
118
+ param :potatoes, array: true do
119
+ param :variety
120
+ param :origin
121
+ end
122
+
123
+ # This syntax is also valid but less sexy:
124
+ param :potatoes, type: { Array => Hash } do
125
+ param :variety
126
+ param :origin
127
+ end
128
+ ```
129
+
130
+ #### Gotchas
131
+ * A `Float` value can be passed to a `Fixnum` parameter but will loose its precision.
132
+ * Some types accepts more than one representation. Example: `Symbol` accepts any type that respond to `:to_sym`.
133
+ * If you define multiple types (ex: `[Symbol, String]`), values are interpreted in this order. So the value `'a'` will be cast to `:a`.
134
+ * Defining the type `{ Fixnum => Date }` doesn't make sense so it falls back to `Fixnum` (the key).
135
+ * `{ Array => Array }` is accepted. It means a 2D array of `:any`.
136
+ * `{ Array => Array => ... }` is not yet supported. Did I hear pull request?
137
+
138
+ ### The `allow` and `deny` options
139
+
140
+ By default, the value of a parameter can be any one in the spectrum of a type, with the exception of nil and empty. The `allow` and `deny` options can be used to further refine the accepted values.
141
+
142
+ To accept nil or empty values:
143
+ ``` ruby
144
+ param :potatoe, allow: :nil
145
+ # => accepts nil, 'Kennebec' but not ''.
146
+
147
+ param :potatoe, allow: :empty
148
+ # => accepts '', 'Kennebec' but not nil.
149
+
150
+ param :potatoe, allow: [:nil, :empty]
151
+ # => accepts nil, '' and 'Kennebec'
152
+ ```
153
+ Of course, this *nil* or *empty* restriction doesn't make sense for all the types so it will only be applied when it does.
154
+
155
+ To accept predefined values:
156
+ ``` ruby
157
+ param :potatoe, allow: ['Superior', 'Ac Belmont', 'Eramosa'] # this is case-sensitive.
158
+
159
+ # Gotcha: this will allow empty values even if you wanted to accept the value 'empty'. You can redefine keywords in the options.
160
+ param :potatoe, type: Symbol, allow: [:superior, :ac_belmont, :empty]
161
+ ```
162
+
163
+ To accept a value matching a regex:
164
+ ``` ruby
165
+ param :potatoe, allow: /^[a-zA-Z]*$/
166
+
167
+ # Gotcha: even though the regex above allows empty values, it must be explicitly stated:
168
+ param :potatoe, allow: [:empty, /^[a-zA-Z]*$/]
169
+ ```
170
+
171
+ To accept a value in a range:
172
+ ``` ruby
173
+ param :potatoe, type: Fixnum, allow: (1..3)
174
+ # => accepts 1, 2, 3 but will fail on any other value.
175
+ ```
176
+
177
+ The `deny` option is conceptually identical to `allow` but a value will fail the validation if a match is found:
178
+ ``` ruby
179
+ param :potatoe, type: Fixnum, deny: (1..3)
180
+ # => accepts any value except 1, 2, 3.
181
+ ```
182
+
183
+ The options `allow` and `deny` are validated independently. So beware to not define `allow` and `deny` options that encompass all the possible values of the parameter!
184
+
185
+ ## Exceptions
186
+
187
+ When the validation fails, an instance of `ParametersSchema::InvalidParameters` is raised. This exception contains the attribute `errors` which is an hash of `{ key: error_code }` that you can work with.
188
+
189
+ Simple case:
190
+ ``` ruby
191
+ ParametersSchema::Schema.new do
192
+ param :potatoe
193
+ end.validate!({})
194
+
195
+ # => ParametersSchema::InvalidParameters
196
+ # @errors = { potatoe: :missing }
197
+ ```
198
+
199
+ The validation process tries to accumulate as many errors as possible before raising the exception, so you can have a precise picture of what went wrong:
200
+ ``` ruby
201
+ ParametersSchema::Schema.new do
202
+ param :potatoe do
203
+ param :name
204
+ param :type, allow: ['Atlantic']
205
+ end
206
+ end.validate!(potatoe: { type: 'Conestoga' })
207
+
208
+ # => ParametersSchema::InvalidParameters
209
+ # @errors = { potatoe: { name: :missing, type: :disallowed } }
210
+ ```
211
+
212
+ The possible error codes are (in the order the are validated):
213
+ ``` ruby
214
+ * :unknown # The parameter is provided but not defined in the schema.
215
+ * :missing # The parameter is required but is missing.
216
+ * :nil # The value cannot be nil but is nil.
217
+ * :empty # The value cannot be empty but is empty.
218
+ * :disallowed # The value has an invalid format (type/allow) other than nil/empty.
219
+ ```
220
+
221
+ ## Integrate with Rails
222
+
223
+ This gem can be used outside of Rails but was created with Rails in mind. For example, the parameters `controller, action, format` are skipped by default (see Options section to override this behavior) and the parameters are defined in a `Hash`. However, this gem doesn't insinuate itself in your project so you must manually add it in your controllers or anywhere else that make sense to you. Here is a little recipe to add validation in your API pipeline:
224
+
225
+ In the base controller of your API, add this **helper**:
226
+ ``` ruby
227
+ # Validate the parameters of an action, using a schema.
228
+ # Returns the validated parameters and throw exceptions on invalid input.
229
+ def validate_params(&parameters_schema)
230
+ schema = ParametersSchema::Schema.new(parameters_schema)
231
+ schema.validate!(params)
232
+ end
233
+ ```
234
+ In the base controller of your API, add this **exception handler**:
235
+ ``` ruby
236
+ # Handle errors related to invalid parameters.
237
+ rescue_from ParametersSchema::InvalidParameters do |e|
238
+ # Do something with the exception (ex: log it).
239
+
240
+ # Render the response.
241
+ render json: ..., status: :bad_request
242
+ end
243
+ ```
244
+
245
+ Now in any controller where you want to validate the parameters, you can do:
246
+ ``` ruby
247
+ def operation
248
+ validated_params = validate_parameters do
249
+ # ...
250
+ end
251
+ # ...
252
+ end
253
+ ```
254
+
255
+ ## Options
256
+
257
+ Options can be specified on the module `ParametersSchema::Options`. Example:
258
+
259
+ ``` ruby
260
+ ParametersSchema::Options.skip_parameters = [:internal_stuff]
261
+ ```
262
+
263
+ Available options:
264
+ * `skip_parameters` an array of first-level parameters to skip. Default: `[:controller, :action, :format]`.
265
+ * `empty_keyword` the keyword used to represent an empty value. Default: `:empty`.
266
+ * `any_keyword` the keyword used to represent any value. Default: `:any`.
267
+ * `none_keyword` the keyword used to represent no value. Default: `:none`.
268
+ * `boolean_keyword` the keyword used to represent a boolean value. Default: `:boolean`.
269
+ * `nil_keyword` the keyword used to represent a nil value. Default: `:nil`.
270
+ * `boolean_true_values` the accepted boolean true values. Not case-sensitive. Default: `true`, `'t'`, `'true'`, `'1'`, `1`, `1.0`.
271
+ * `boolean_false_values` the accepted boolean false values. Not case-sensitive. Default: `false`, `'f'`, `'false'`, `'0'`, `0`, `0.0`.
272
+
273
+ ## Contribute
274
+
275
+ Yes, please. Bug fixes, new features, refactoring, unit tests. Send your precious pull requests.
276
+
277
+ ### Ideas
278
+
279
+ * Array of arrays of ...
280
+ * Min/Max for numeric values
281
+ * More `allow` options
282
+ * Better refine error codes
283
+
284
+ ## License
285
+
286
+ Parameters Schema is released under the [MIT License](http://www.opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ end
6
+
7
+ desc 'Run tests'
8
+ task default: :test
@@ -0,0 +1,21 @@
1
+ class Object
2
+ #
3
+ # Check if object is numeric.
4
+ # From http://stackoverflow.com/questions/5661466/test-if-string-is-a-number-in-ruby-on-rails
5
+ #
6
+ # p "1".numeric? # => true
7
+ # p "1.2".numeric? # => true
8
+ # p "5.4e-29".numeric? # => true
9
+ # p "12e20".numeric? # => true
10
+ # p "1a".numeric? # => false
11
+ # p "1.2.3.4".numeric? # => false
12
+ #
13
+ def numeric?
14
+ return true if self.kind_of?(Numeric)
15
+ return true if self.to_s =~ /^\d+$/
16
+ Float(self)
17
+ true
18
+ rescue
19
+ false
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module ParametersSchema
2
+ module ErrorCode
3
+ unless defined? UNKNOWN # Make sure we don't redefine the constants twice.
4
+ UNKNOWN = :unknown
5
+ MISSING = :missing
6
+ NIL = :nil
7
+ EMPTY = :empty
8
+ DISALLOWED = :disallowed
9
+ end
10
+ end
11
+
12
+ class InvalidParameters < StandardError
13
+ attr_reader :errors
14
+
15
+ def initialize(errors)
16
+ @errors = errors
17
+ end
18
+
19
+ def message
20
+ @errors.to_s
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,80 @@
1
+ module ParametersSchema
2
+ module Options
3
+ def self.reset_defaults
4
+ @@skip_parameters = [:controller, :action, :format]
5
+ @@empty_keyword = :empty
6
+ @@any_keyword = :any
7
+ @@none_keyword = :none
8
+ @@boolean_keyword = :boolean
9
+ @@nil_keyword = :nil
10
+ @@boolean_true_values = [true, 't', 'true', '1', 1, 1.0]
11
+ @@boolean_false_values = [false, 'f', 'false', '0', 0, 0.0]
12
+ end
13
+
14
+ def self.skip_parameters
15
+ @@skip_parameters
16
+ end
17
+
18
+ def self.skip_parameters=(new_value)
19
+ @@skip_parameters = new_value
20
+ end
21
+
22
+ def self.empty_keyword
23
+ @@empty_keyword
24
+ end
25
+
26
+ def self.empty_keyword=(new_value)
27
+ @@empty_keyword = new_value
28
+ end
29
+
30
+ def self.any_keyword
31
+ @@any_keyword
32
+ end
33
+
34
+ def self.any_keyword=(new_value)
35
+ @@any_keyword = new_value
36
+ end
37
+
38
+ def self.none_keyword
39
+ @@none_keyword
40
+ end
41
+
42
+ def self.none_keyword=(new_value)
43
+ @@none_keyword = new_value
44
+ end
45
+
46
+ def self.boolean_keyword
47
+ @@boolean_keyword
48
+ end
49
+
50
+ def self.boolean_keyword=(new_value)
51
+ @@boolean_keyword = new_value
52
+ end
53
+
54
+ def self.nil_keyword
55
+ @@nil_keyword
56
+ end
57
+
58
+ def self.nil_keyword=(new_value)
59
+ @@nil_keyword = new_value
60
+ end
61
+
62
+ def self.boolean_true_values
63
+ @@boolean_true_values
64
+ end
65
+
66
+ def self.boolean_true_values=(new_value)
67
+ @@boolean_true_values = new_value
68
+ end
69
+
70
+ def self.boolean_false_values
71
+ @@boolean_false_values
72
+ end
73
+
74
+ def self.boolean_false_values=(new_value)
75
+ @@boolean_false_values = new_value
76
+ end
77
+
78
+ self.reset_defaults
79
+ end
80
+ end
@@ -0,0 +1,296 @@
1
+ module ParametersSchema
2
+ class Schema
3
+ def initialize(&schema)
4
+ @schema = schema
5
+ end
6
+
7
+ def validate!(params)
8
+ # Make sure we have params we can work with.
9
+ @params = __prepare_params(params)
10
+
11
+ # Parse and validate each param.
12
+ @sanitized_params = []
13
+ instance_eval(&@schema)
14
+ # Serve the params if valid, otherwise throw exception.
15
+ __handle_errors
16
+ __serve
17
+ end
18
+
19
+ private
20
+
21
+ def param(name, options = {}, &inner_params)
22
+ options[:required] = !options.has_key?(:required) || options[:required].present?
23
+
24
+ options[:type] = [options[:type] || String].flatten
25
+ options[:allow] = [options[:allow].present? ? options[:allow] : ParametersSchema::Options.any_keyword].flatten
26
+ options[:deny] = [options[:deny].present? ? options[:deny] : ParametersSchema::Options.none_keyword].flatten
27
+
28
+ [ParametersSchema::Options.any_keyword, ParametersSchema::Options.none_keyword].each do |dominant_value|
29
+ [:allow, :deny, :type].each do |key|
30
+ options[key] = [dominant_value] if options[key].include?(dominant_value)
31
+ end
32
+ end
33
+
34
+ options[:array] = options[:array] || false
35
+ options[:parent] = options[:parent] || @params
36
+
37
+ options[:type].map! do |type|
38
+ # Limit to { key => value }
39
+ if type.kind_of?(Hash) && type.count > 1
40
+ type = { type.first[0] => type.first[1] }
41
+ end
42
+
43
+ # Limit to { Array => value }
44
+ if type.kind_of?(Hash) && type.first[0] != Array
45
+ type = type.first[0]
46
+ end
47
+
48
+ # Apply :array keyword if not already in the format { Array => value }
49
+ if options.delete(:array) && !type.kind_of?(Hash)
50
+ type = { Array => type }
51
+ end
52
+
53
+ # The format...
54
+ #
55
+ # param :potatoe do
56
+ # ...
57
+ # end
58
+ #
59
+ # ... is always an Hash.
60
+ if type.kind_of?(Hash) && inner_params.present?
61
+ type = { Array => Hash }
62
+ elsif inner_params.present?
63
+ type = Hash
64
+ end
65
+
66
+ type
67
+ end
68
+
69
+ @sanitized_params.push(__validate_param(name, options, inner_params))
70
+ end
71
+
72
+ def __prepare_params(params)
73
+ params ||= {}
74
+ params = {} unless params.kind_of?(Hash)
75
+ params = params.clone.with_indifferent_access
76
+ ParametersSchema::Options.skip_parameters.each{ |param| params.delete(param) }
77
+ params
78
+ end
79
+
80
+ def __validate_param(name, options, inner_params)
81
+ # Validate the presence of the parameter.
82
+ value, error = __validate_param_presence(name, options)
83
+ return __stop_validation(name, value, error, options) if error || (!options[:required] && value.nil?)
84
+
85
+ # Validate nil value.
86
+ value, error = __validate_param_value_nil(value, options)
87
+ return __stop_validation(name, value, error, options) if error || value.nil?
88
+
89
+ # Validate empty value (except hash).
90
+ value, error = __validate_param_value_empty(value, options)
91
+ return __stop_validation(name, value, error, options) if error || value.nil?
92
+
93
+ # Validate the type of the parameter.
94
+ [options[:type]].flatten.each do |type|
95
+ value, error = __validate_type_and_cast(value, type, options, inner_params)
96
+ break if error.blank?
97
+ end
98
+ return __stop_validation(name, value, error, options) if error || value.nil?
99
+
100
+ # Validate the allowed and denied values of the parameter
101
+ unless value.kind_of?(Array) || value.kind_of?(Hash)
102
+ [:allow, :deny].each do |allow_or_deny|
103
+ value, error = __validate_param_value_format(value, options, allow_or_deny)
104
+ return __stop_validation(name, value, error, options) if error || value.nil?
105
+ end
106
+ end
107
+
108
+ # Validate empty value for hash.
109
+ # This is done at this point to let the validation emit errors when inner parameters are missing.
110
+ # It is preferable that { key: {} } emit { key: { name: :missing } } than { key: :empty }.
111
+ value, error = __validate_param_value_hash_empty(value, options)
112
+ return __stop_validation(name, value, error, options) if error || value.nil?
113
+
114
+ __stop_validation(name, value, error, options)
115
+ end
116
+
117
+ def __validate_param_presence(name, options)
118
+ error = nil
119
+
120
+ if options[:required] && !options[:parent].has_key?(name)
121
+ error = ParametersSchema::ErrorCode::MISSING
122
+ elsif options[:parent].has_key?(name)
123
+ value = options[:parent][name]
124
+ end
125
+
126
+ [value, error]
127
+ end
128
+
129
+ def __validate_param_value_nil(value, options)
130
+ error = nil
131
+
132
+ if !options[:allow].include?(ParametersSchema::Options.nil_keyword) && value.nil?
133
+ error = ParametersSchema::ErrorCode::NIL
134
+ end
135
+
136
+ [value, error]
137
+ end
138
+
139
+ def __validate_param_value_empty(value, options)
140
+ error = nil
141
+
142
+ if !options[:allow].include?(ParametersSchema::Options.empty_keyword) && !value.kind_of?(Hash) && value.respond_to?(:empty?) && value.empty?
143
+ error = ParametersSchema::ErrorCode::EMPTY
144
+ end
145
+
146
+ [value, error]
147
+ end
148
+
149
+ def __validate_param_value_hash_empty(value, options)
150
+ error = nil
151
+
152
+ if !options[:allow].include?(ParametersSchema::Options.empty_keyword) && value.kind_of?(Hash) && value.empty?
153
+ error = ParametersSchema::ErrorCode::EMPTY
154
+ end
155
+
156
+ [value, error]
157
+ end
158
+
159
+ def __validate_param_value_format(value, options, allow_or_deny)
160
+ conditions = options[allow_or_deny] - [ParametersSchema::Options.empty_keyword, ParametersSchema::Options.nil_keyword]
161
+ inverse = allow_or_deny == :deny
162
+ accept_all_keyword = inverse ? ParametersSchema::Options.none_keyword : ParametersSchema::Options.any_keyword
163
+ refuse_all_keyword = inverse ? ParametersSchema::Options.any_keyword : ParametersSchema::Options.none_keyword
164
+
165
+ return [value, nil] if conditions.include?(accept_all_keyword)
166
+ return [value, ParametersSchema::ErrorCode::DISALLOWED] if conditions.include?(refuse_all_keyword)
167
+
168
+ error = nil
169
+
170
+ conditions.each do |condition|
171
+ error = nil
172
+
173
+ if condition.kind_of?(Range)
174
+ condition_passed = condition.include?(value)
175
+ condition_passed = !condition_passed if inverse
176
+ error = ParametersSchema::ErrorCode::DISALLOWED unless condition_passed
177
+ elsif condition.kind_of?(Regexp) && !value.kind_of?(String)
178
+ error = ParametersSchema::ErrorCode::DISALLOWED
179
+ elsif condition.kind_of?(Regexp) && value.kind_of?(String)
180
+ condition_passed = (condition =~ value).present?
181
+ condition_passed = !condition_passed if inverse
182
+ error = ParametersSchema::ErrorCode::DISALLOWED unless condition_passed
183
+ else
184
+ condition_passed = condition == value
185
+ condition_passed = !condition_passed if inverse
186
+ error = ParametersSchema::ErrorCode::DISALLOWED unless condition_passed
187
+ end
188
+
189
+ break if inverse ? error.present? : error.blank?
190
+ end
191
+
192
+ [value, error]
193
+ end
194
+
195
+ def __validate_type_and_cast(value, type, options, inner_params)
196
+ if type.kind_of?(Hash)
197
+ error = ParametersSchema::ErrorCode::DISALLOWED if !value.kind_of?(Array)
198
+ value, error = __validate_array(value, type.values.first, options, inner_params) unless error
199
+ elsif inner_params.present?
200
+ begin
201
+ inner_schema = ParametersSchema::Schema.new(&inner_params)
202
+ value = inner_schema.validate!(value)
203
+ rescue ParametersSchema::InvalidParameters => e
204
+ error = e.errors
205
+ end
206
+ elsif type == ParametersSchema::Options.boolean_keyword
207
+ value = true if ParametersSchema::Options.boolean_true_values.include?(value.kind_of?(String) ? value.downcase : value)
208
+ value = false if ParametersSchema::Options.boolean_false_values.include?(value.kind_of?(String) ? value.downcase : value)
209
+ error = ParametersSchema::ErrorCode::DISALLOWED if !value.kind_of?(TrueClass) && !value.kind_of?(FalseClass)
210
+ elsif type == Fixnum
211
+ error = ParametersSchema::ErrorCode::DISALLOWED if !value.numeric?
212
+ value = value.to_i if error.blank? # cast to right type.
213
+ elsif type == Float
214
+ error = ParametersSchema::ErrorCode::DISALLOWED if !value.numeric?
215
+ value = value.to_f if error.blank? # cast to right type.
216
+ elsif type == Regexp
217
+ error = ParametersSchema::ErrorCode::DISALLOWED unless value =~ options[:regex]
218
+ elsif type == ParametersSchema::Options.any_keyword
219
+ # No validation required.
220
+ elsif type == ParametersSchema::Options.none_keyword
221
+ # Always fail. Why would you want to do that?
222
+ error = ParametersSchema::ErrorCode::DISALLOWED
223
+ elsif type == String
224
+ error = ParametersSchema::ErrorCode::DISALLOWED unless value.kind_of?(String) || value.kind_of?(Symbol)
225
+ value = value.to_s if error.blank? # cast to right type.
226
+ elsif type == Symbol
227
+ error = ParametersSchema::ErrorCode::DISALLOWED unless value.respond_to?(:to_sym)
228
+ value = value.to_sym if error.blank? # cast to right type.
229
+ elsif type == Date
230
+ begin
231
+ value = value.kind_of?(String) ? Date.parse(value) : value.to_date
232
+ rescue
233
+ error = ParametersSchema::ErrorCode::DISALLOWED
234
+ end
235
+ elsif type == DateTime
236
+ begin
237
+ value = value.kind_of?(String) ? DateTime.parse(value) : value.to_datetime
238
+ rescue
239
+ error = ParametersSchema::ErrorCode::DISALLOWED
240
+ end
241
+ else
242
+ error = ParametersSchema::ErrorCode::DISALLOWED if !value.kind_of?(type)
243
+ end
244
+
245
+ [value, error]
246
+ end
247
+
248
+ def __validate_array(value, type, options, inner_params)
249
+ if !value.kind_of?(Array)
250
+ return [value, ParametersSchema::ErrorCode::DISALLOWED]
251
+ end
252
+
253
+ value_opts = {
254
+ required: true,
255
+ type: type,
256
+ parent: { value: nil },
257
+ allow: options[:allow],
258
+ deny: options[:deny]
259
+ }
260
+
261
+ value.map! do |v|
262
+ value_opts[:parent][:value] = v
263
+ __validate_param(:value, value_opts, inner_params)
264
+ end
265
+
266
+ # For now, take the first error.
267
+ [value.map{ |v| v[:value] }, value.find{ |v| v[:error].present? }.try(:[], :error)]
268
+ end
269
+
270
+ def __stop_validation(name, value, error, options)
271
+ { param: name, error: error, value: value, keep_if_nil: options[:allow].include?(ParametersSchema::Options.nil_keyword) }
272
+ end
273
+
274
+ def __handle_errors
275
+ errors = @sanitized_params
276
+ .select{ |p| p[:error].present? }
277
+ .each_with_object({}.with_indifferent_access) do |p, h|
278
+ h[p[:param]] = p[:error] == :nested_errors ? p[:value] : p[:error]
279
+ end
280
+
281
+ (@params.keys.map(&:to_sym) - @sanitized_params.map{ |p| p[:param] }).each do |extra_param|
282
+ errors[extra_param] = ParametersSchema::ErrorCode::UNKNOWN
283
+ end
284
+
285
+ raise ParametersSchema::InvalidParameters.new(errors) if errors.any?
286
+ end
287
+
288
+ def __serve
289
+ @sanitized_params
290
+ .reject{ |p| p[:value].nil? && !p[:keep_if_nil] }
291
+ .each_with_object({}.with_indifferent_access) do |p, h|
292
+ h[p[:param]] = p[:value]
293
+ end
294
+ end
295
+ end
296
+ end