parameters_schema 0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/README.md +286 -0
- data/Rakefile +8 -0
- data/lib/parameters_schema/core_ext.rb +21 -0
- data/lib/parameters_schema/exceptions.rb +23 -0
- data/lib/parameters_schema/options.rb +80 -0
- data/lib/parameters_schema/schema.rb +296 -0
- data/lib/parameters_schema.rb +8 -0
- data/test/helpers.rb +56 -0
- data/test/test_options.rb +74 -0
- data/test/test_schema_allow.rb +114 -0
- data/test/test_schema_allow_empty.rb +30 -0
- data/test/test_schema_allow_nil.rb +34 -0
- data/test/test_schema_hash.rb +90 -0
- data/test/test_schema_required.rb +74 -0
- data/test/test_schema_simple.rb +58 -0
- data/test/test_schema_types.rb +452 -0
- data/test/test_schema_types_complex.rb +126 -0
- metadata +77 -0
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
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(¶meters_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,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
|