roda 3.2.0 → 3.3.0
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 +4 -4
- data/CHANGELOG +4 -0
- data/README.rdoc +29 -1
- data/doc/release_notes/3.3.0.txt +291 -0
- data/lib/roda/plugins/class_matchers.rb +1 -1
- data/lib/roda/plugins/indifferent_params.rb +8 -1
- data/lib/roda/plugins/param_matchers.rb +11 -4
- data/lib/roda/plugins/path.rb +5 -2
- data/lib/roda/plugins/symbol_status.rb +2 -2
- data/lib/roda/plugins/typecast_params.rb +968 -0
- data/lib/roda/version.rb +1 -1
- data/spec/plugin/public_spec.rb +18 -0
- data/spec/plugin/typecast_params_spec.rb +1215 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 992b12eaa4153e29ad448ac21c6afafe12f9a89a
|
4
|
+
data.tar.gz: 3ce43ca69c2faeb4a335968959365f33309460ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af0baab222fcd78a7b49e5d8d2e204139ab7b1ec25cf75d001c2715777d31effa1527dda7ecfe9e1f18082978f77ad419d5c0b020667c6265d9273038d441483
|
7
|
+
data.tar.gz: 4b7da58d7c976ed77f7da407090c14f38f912ac0dbead739cc676a16b10665d09866a95f739c852139919a260805e839a80ea0c9797a3da8413aaef632109046
|
data/CHANGELOG
CHANGED
data/README.rdoc
CHANGED
@@ -443,7 +443,7 @@ treat it as one route with an optional segment. One simple way to do that is to
|
|
443
443
|
use a parameter instead of an optional segment (e.g. +/items/123?opt=456+).
|
444
444
|
|
445
445
|
r.is "items", Integer do |item_id|
|
446
|
-
optional_data = r.params['opt']
|
446
|
+
optional_data = r.params['opt'].to_s
|
447
447
|
end
|
448
448
|
|
449
449
|
However, if you really do want to use a optional segment, there are a couple different
|
@@ -812,6 +812,34 @@ are not escaping the output of the content template:
|
|
812
812
|
|
813
813
|
This support requires {Erubi}[https://github.com/jeremyevans/erubi].
|
814
814
|
|
815
|
+
=== Unexpected Parameter Types
|
816
|
+
|
817
|
+
Rack converts submitted parameters into a hash of strings, arrays, and
|
818
|
+
nested hashes. Since the user controls the submission of parameters, you
|
819
|
+
should treat any submission of parameters with caution, and should be
|
820
|
+
explicitly checking and/or converting types before using any submitted
|
821
|
+
parameters. One way to do this is explicitly after accessing them:
|
822
|
+
|
823
|
+
# Convert foo_id parameter to an integer
|
824
|
+
request.params['foo_id'].to_i
|
825
|
+
|
826
|
+
However, it is easy to forget to convert the type, and if the user
|
827
|
+
submits +foo_id+ as a hash or array, a NoMethodError will be raised.
|
828
|
+
Worse is if you do:
|
829
|
+
|
830
|
+
some_method(request.params['bar'])
|
831
|
+
|
832
|
+
Where +some_method+ supports both a string argument and a hash
|
833
|
+
argument, and you expect the parameter will be submitted as a
|
834
|
+
string, and +some_method+'s handling of a hash argument performs
|
835
|
+
an unauthorized action.
|
836
|
+
|
837
|
+
Roda ships with a +typecast_params+ plugin that can easily handle
|
838
|
+
the typecasting of submitted parameters, and it is recommended
|
839
|
+
that all Roda applications that deal with parameters use it or
|
840
|
+
another tool to explicitly convert submitted parameters to the
|
841
|
+
expected types.
|
842
|
+
|
815
843
|
=== Security Related HTTP Headers
|
816
844
|
|
817
845
|
You may want to look into setting the following HTTP headers, which
|
@@ -0,0 +1,291 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* A typecast_params plugin has been added for handling the
|
4
|
+
conversion of params to the expected type. This plugin is
|
5
|
+
recommended for all applications that deal with submitted
|
6
|
+
parameters.
|
7
|
+
|
8
|
+
Submitted parameters should be considered untrusted input, and in
|
9
|
+
standard use with browsers, parameters are submitted as strings
|
10
|
+
(or a hash/array containing strings). In most cases it makes sense
|
11
|
+
to explicitly convert the parameter to the desired type. While this
|
12
|
+
can be done via manual conversion:
|
13
|
+
|
14
|
+
key = request.params['key'].to_i
|
15
|
+
key = nil unless key > 0
|
16
|
+
|
17
|
+
the typecast_params plugin adds a friendlier interface:
|
18
|
+
|
19
|
+
key = typecast_params.pos_int('key')
|
20
|
+
|
21
|
+
As typecast_params is a fairly long method name, you may want to
|
22
|
+
consider aliasing it to something more terse in your application,
|
23
|
+
such as tp.
|
24
|
+
|
25
|
+
One advantage of using typecast_params is that access or conversion
|
26
|
+
errors are raised as a specific exception class
|
27
|
+
(Roda::RodaPlugins::TypecastParams::Error). This allows you to
|
28
|
+
handle this specific exception class globally and return an
|
29
|
+
appropriate 4xx response to the client. You can use the
|
30
|
+
Error#param_name and Error#reason methods to get more information
|
31
|
+
about the error.
|
32
|
+
|
33
|
+
typecast_params offers support for default values:
|
34
|
+
|
35
|
+
key = typecast_params.pos_int('key', 1)
|
36
|
+
|
37
|
+
The default value is only used if no value has been submitted for
|
38
|
+
the parameter, or if the conversion of the value results in nil.
|
39
|
+
Handling defaults for parameter conversion manually is more
|
40
|
+
difficult, since the parameter may not be present at all, or it may
|
41
|
+
be present but an empty string because the user did not enter a
|
42
|
+
value on the related form. Use of typecast_params for the
|
43
|
+
conversion handles both cases.
|
44
|
+
|
45
|
+
In many cases, parameters should be required, and if they aren't
|
46
|
+
submitted, that should be considered an error. typecast_params
|
47
|
+
handles this with ! methods:
|
48
|
+
|
49
|
+
key = typecast_params.pos_int!('key')
|
50
|
+
|
51
|
+
These ! methods raise an error instead of returning nil, and do not
|
52
|
+
allow defaults.
|
53
|
+
|
54
|
+
To make it easy to handle cases where many parameters need the same
|
55
|
+
conversion done, you can pass an array of keys to a conversion
|
56
|
+
method, and it will return an array of converted values:
|
57
|
+
|
58
|
+
key1, key2 = typecast_params.pos_int(['key1', 'key2'])
|
59
|
+
|
60
|
+
This is equivalent to:
|
61
|
+
|
62
|
+
key1 = typecast_params.pos_int('key1')
|
63
|
+
key2 = typecast_params.pos_int('key2')
|
64
|
+
|
65
|
+
The ! methods also support arrays of keys, ensuring that all
|
66
|
+
parameters have a value:
|
67
|
+
|
68
|
+
key1, key2 = typecast_params.pos_int!(['key1', 'key2'])
|
69
|
+
|
70
|
+
For handling of array parameters, where all entries in the array
|
71
|
+
use the same conversion, there is an array method which takes the
|
72
|
+
type as the first argument and the keys to convert as the second
|
73
|
+
argument:
|
74
|
+
|
75
|
+
keys = typecast_params.array(:pos_int, 'keys')
|
76
|
+
|
77
|
+
If you want to ensure that all entries in the array are converted
|
78
|
+
successfully and that there is a value for the array itself, you
|
79
|
+
can use array!:
|
80
|
+
|
81
|
+
keys = typecast_params.array!(:pos_int, 'keys')
|
82
|
+
|
83
|
+
This will raise an exception if any of the values in the array for
|
84
|
+
parameter keys cannot be converted to a positive integer.
|
85
|
+
|
86
|
+
Both array and array! support default values which are used if no
|
87
|
+
value is present for the parameter:
|
88
|
+
|
89
|
+
keys = typecast_params.array(:pos_int, 'keys', [])
|
90
|
+
keys = typecast_params.array!(:pos_int, 'keys', [])
|
91
|
+
|
92
|
+
You can also pass an array of keys to array or array!, if you would
|
93
|
+
like to perform the same conversion on multiple arrays:
|
94
|
+
|
95
|
+
foo_ids, bar_ids = typecast_params.array!(:pos_int, ['foo_ids', 'bar_ids'])
|
96
|
+
|
97
|
+
The previous examples have shown use of the pos_int method, which
|
98
|
+
uses to_i to convert the value to an integer, but returns nil if the
|
99
|
+
resulting integer is not positive. Unless you need to handle
|
100
|
+
negative numbers, it is recommended to use pos_int instead of int as
|
101
|
+
int will convert invalid values to 0 (since that is how
|
102
|
+
<tt>String#to_i</tt> works).
|
103
|
+
|
104
|
+
There are many built in methods for type conversion:
|
105
|
+
|
106
|
+
any :: Returns the value as is without conversion
|
107
|
+
str :: Raises if value is not already a string
|
108
|
+
nonempty_str :: Raises if value is not already a string, and
|
109
|
+
converts the empty string or string containing only
|
110
|
+
whitespace to nil
|
111
|
+
bool :: Converts entry to boolean if in one of the recognized
|
112
|
+
formats (case insensitive for strings):
|
113
|
+
nil :: nil, ''
|
114
|
+
true :: true, 1, '1', 't', 'true', 'yes', 'y', 'on'
|
115
|
+
false :: false, 0, '0', 'f', 'false', 'no', 'n', 'off'
|
116
|
+
If not in one of those formats, raises an error.
|
117
|
+
int :: Converts value to integer using to_i (note that invalid
|
118
|
+
input strings will be converted to 0)
|
119
|
+
pos_int :: Converts value using to_i, but non-positive values
|
120
|
+
are converted to nil
|
121
|
+
Integer :: Converts value to integer using
|
122
|
+
Kernel::Integer(value, 10)
|
123
|
+
float :: Converts value to float using to_f (note that invalid
|
124
|
+
input strings will be converted to 0.0)
|
125
|
+
Float :: Converts value to float using Kernel::Float(value)
|
126
|
+
Hash :: Raises if value is not already a hash
|
127
|
+
date :: Converts value to Date using Date.parse(value)
|
128
|
+
time :: Converts value to Time using Time.parse(value)
|
129
|
+
datetime :: Converts value to DateTime using DateTime.parse(value)
|
130
|
+
file :: Raises if value is not already a hash with a :tempfile key
|
131
|
+
whose value responds to read (this is the format rack uses
|
132
|
+
for uploaded files).
|
133
|
+
|
134
|
+
All of these methods also support ! methods (e.g. pos_int!), and all
|
135
|
+
of them can be used in the array and array! methods to support
|
136
|
+
arrays of values.
|
137
|
+
|
138
|
+
Since parameter hashes can be nested, the [] method can be used to
|
139
|
+
access nested
|
140
|
+
hashes:
|
141
|
+
|
142
|
+
# params: {'key'=>{'sub_key'=>'1'}}
|
143
|
+
typecast_params['key'].pos_int!('sub_key') # => 1
|
144
|
+
|
145
|
+
This works to an arbitrary depth:
|
146
|
+
|
147
|
+
# params: {'key'=>{'sub_key'=>{'sub_sub_key'=>'1'}}}
|
148
|
+
typecast_params['key']['sub_key'].pos_int!('sub_sub_key') # => 1
|
149
|
+
|
150
|
+
And also works with arrays at any depth, if those arrays contain
|
151
|
+
hashes:
|
152
|
+
|
153
|
+
# params: {'key'=>[{'sub_key'=>{'sub_sub_key'=>'1'}}]}
|
154
|
+
typecast_params['key'][0]['sub_key'].pos_int!('sub_sub_key') # => 1
|
155
|
+
|
156
|
+
# params: {'key'=>[{'sub_key'=>['1']}]}
|
157
|
+
typecast_params['key'][0].array!(:pos_int, 'sub_key') # => [1]
|
158
|
+
|
159
|
+
To allow easier access to nested data, there is a dig method:
|
160
|
+
|
161
|
+
typecast_params.dig(:pos_int, 'key', 'sub_key')
|
162
|
+
typecast_params.dig(:pos_int, 'key', 0, 'sub_key', 'sub_sub_key')
|
163
|
+
|
164
|
+
dig will return nil if any access while looking up the nested value
|
165
|
+
returns nil. There is also a dig! method, which will raise an Error
|
166
|
+
if dig would return nil:
|
167
|
+
|
168
|
+
typecast_params.dig!(:pos_int, 'key', 'sub_key')
|
169
|
+
typecast_params.dig!(:pos_int, 'key', 0, 'sub_key', 'sub_sub_key')
|
170
|
+
|
171
|
+
Note that none of these conversion methods modify request.params.
|
172
|
+
They purely do the conversion and return the converted value.
|
173
|
+
However, in some cases it is useful to do all the conversion up
|
174
|
+
front, and then pass a hash of converted parameters to an internal
|
175
|
+
method that expects to receive values in specific types. The
|
176
|
+
convert! method does this, and there is also a convert_each! method
|
177
|
+
designed for converting multiple values using the same block:
|
178
|
+
|
179
|
+
converted_params = typecast_params.convert! do |tp|
|
180
|
+
tp.int('page')
|
181
|
+
tp.pos_int!('artist_id')
|
182
|
+
tp.array!(:pos_int, 'album_ids')
|
183
|
+
tp.convert!('sales') do |stp|
|
184
|
+
tp.pos_int!(['num_sold', 'num_shipped'])
|
185
|
+
end
|
186
|
+
tp.convert!('members') do |mtp|
|
187
|
+
mtp.convert_each! do |stp|
|
188
|
+
stp.str!(['first_name', 'last_name'])
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# converted_params:
|
194
|
+
# {
|
195
|
+
# 'page' => 1,
|
196
|
+
# 'artist_id' => 2,
|
197
|
+
# 'album_ids' => [3, 4],
|
198
|
+
# 'sales' => {
|
199
|
+
# 'num_sold' => 5,
|
200
|
+
# 'num_shipped' => 6
|
201
|
+
# },
|
202
|
+
# 'members' => [
|
203
|
+
# {'first_name' => 'Foo', 'last_name' => 'Bar'},
|
204
|
+
# {'first_name' => 'Baz', 'last_name' => 'Quux'}
|
205
|
+
# ]
|
206
|
+
# }
|
207
|
+
|
208
|
+
convert! and convert_each! only return values you explicitly specify
|
209
|
+
for conversion inside the passed block.
|
210
|
+
|
211
|
+
You can specify the :symbolize option to convert! or convert_each!,
|
212
|
+
which will symbolize the resulting hash keys:
|
213
|
+
|
214
|
+
converted_params = typecast_params.convert!(symbolize: true) do |tp|
|
215
|
+
tp.int('page')
|
216
|
+
tp.pos_int!('artist_id')
|
217
|
+
tp.array!(:pos_int, 'album_ids')
|
218
|
+
tp.convert!('sales') do |stp|
|
219
|
+
tp.pos_int!(['num_sold', 'num_shipped'])
|
220
|
+
end
|
221
|
+
tp.convert!('members') do |mtp|
|
222
|
+
mtp.convert_each! do |stp|
|
223
|
+
stp.str!(['first_name', 'last_name'])
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# converted_params:
|
229
|
+
# {
|
230
|
+
# :page => 1,
|
231
|
+
# :artist_id => 2,
|
232
|
+
# :album_ids => [3, 4],
|
233
|
+
# :sales => {
|
234
|
+
# :num_sold => 5,
|
235
|
+
# :num_shipped => 6
|
236
|
+
# },
|
237
|
+
# :members => [
|
238
|
+
# {:first_name => 'Foo', :last_name => 'Bar'},
|
239
|
+
# {:first_name => 'Baz', :last_name => 'Quux'}
|
240
|
+
# ]
|
241
|
+
# }
|
242
|
+
|
243
|
+
Using the :symbolize option makes it simpler to transition from
|
244
|
+
untrusted external data (string keys), to trusted data that can be
|
245
|
+
used internally (trusted in the sense that the expected types are
|
246
|
+
used).
|
247
|
+
|
248
|
+
Note that if there are multiple conversion errors raised inside a
|
249
|
+
convert! or convert_each! block, they are recorded and a single
|
250
|
+
Roda::RodaPlugins::TypecastParams::Error instance is raised after
|
251
|
+
processing the block. TypecastParams::Error#params_names can be
|
252
|
+
called on the exception to get an array of all parameter names
|
253
|
+
with conversion issues, and TypecastParams::Error#all_errors
|
254
|
+
can be used to get an array of all Error instances.
|
255
|
+
|
256
|
+
Because of how convert! and convert_each! work, you should avoid
|
257
|
+
calling TypecastParams::Params#[] inside the block you pass to
|
258
|
+
these methods, because if the #[] call fails, it will skip the
|
259
|
+
reminder of the block.
|
260
|
+
|
261
|
+
Be aware that when you use convert! and convert_each!, the
|
262
|
+
conversion methods called inside the block may return nil if there
|
263
|
+
is a error raised, and nested calls to convert! and convert_each!
|
264
|
+
may not return values.
|
265
|
+
|
266
|
+
When loading the typecast_params plugin, a subclass of
|
267
|
+
TypecastParams::Params is created specific to the Roda application.
|
268
|
+
You can add support for custom types by passing a block when loading
|
269
|
+
the typecast_params plugin. This block is executed in the context
|
270
|
+
of the subclass, and calling handle_type in the block can be used to
|
271
|
+
add conversion methods. handle_type accepts a type name and the
|
272
|
+
block used to convert the type:
|
273
|
+
|
274
|
+
plugin :typecast_params do
|
275
|
+
handle_type(:album) do |value|
|
276
|
+
if id = convert_pos_int(val)
|
277
|
+
Album[id]
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
By default, the typecast_params conversion procs are passed the
|
283
|
+
parameter value directly from request.params without modification.
|
284
|
+
In some cases, it may be beneficial to strip leading and trailing
|
285
|
+
whitespace from parameter string values before processing, which
|
286
|
+
you can do by passing the strip: :all> option when loading the
|
287
|
+
plugin.
|
288
|
+
|
289
|
+
By design, typecast_params only deals with string keys, it is not
|
290
|
+
possible to use symbol keys as arguments to the conversion methods
|
291
|
+
and have them converted.
|
@@ -28,7 +28,7 @@ class Roda
|
|
28
28
|
# This is useful to DRY up code if you are using the same type of pattern and
|
29
29
|
# type conversion in multiple places in your application.
|
30
30
|
#
|
31
|
-
# This plugin does not work with the
|
31
|
+
# This plugin does not work with the params_capturing plugin, as it does not
|
32
32
|
# offer the ability to associate block arguments with named keys.
|
33
33
|
module ClassMatchers
|
34
34
|
module ClassMethods
|
@@ -6,7 +6,14 @@ class Roda
|
|
6
6
|
# The indifferent_params plugin adds a +params+ instance
|
7
7
|
# method which offers indifferent access to the request
|
8
8
|
# params, allowing you to use symbols to lookup values in
|
9
|
-
# a hash where the keys are strings.
|
9
|
+
# a hash where the keys are strings. Note that while this
|
10
|
+
# allows for an easier transition from some other ruby frameworks,
|
11
|
+
# it is a bad idea in general as it makes it more difficult to
|
12
|
+
# separate external data from internal data, and doesn't handle
|
13
|
+
# any typecasting of the data. Consider using the typecast_params
|
14
|
+
# plugin instead of this plugin for accessing parameters.
|
15
|
+
#
|
16
|
+
# Example:
|
10
17
|
#
|
11
18
|
# plugin :indifferent_params
|
12
19
|
#
|
@@ -9,7 +9,7 @@ class Roda
|
|
9
9
|
# It adds a :param matcher for matching on any param with the
|
10
10
|
# same name, yielding the value of the param:
|
11
11
|
#
|
12
|
-
# r.on param: 'foo' do |
|
12
|
+
# r.on param: 'foo' do |foo|
|
13
13
|
# # Matches '?foo=bar', '?foo='
|
14
14
|
# # Doesn't match '?bar=foo'
|
15
15
|
# end
|
@@ -17,7 +17,7 @@ class Roda
|
|
17
17
|
# It adds a :param! matcher for matching on any non-empty param
|
18
18
|
# with the same name, yielding the value of the param:
|
19
19
|
#
|
20
|
-
# r.on(param!: 'foo') do |
|
20
|
+
# r.on(param!: 'foo') do |foo|
|
21
21
|
# # Matches '?foo=bar'
|
22
22
|
# # Doesn't match '?foo=', '?bar=foo'
|
23
23
|
# end
|
@@ -25,16 +25,23 @@ class Roda
|
|
25
25
|
# It also adds :params and :params! matchers, for matching multiple
|
26
26
|
# params at the same time:
|
27
27
|
#
|
28
|
-
# r.on params: ['foo', 'baz'] do |
|
28
|
+
# r.on params: ['foo', 'baz'] do |foo, baz|
|
29
29
|
# # Matches '?foo=bar&baz=quuz', '?foo=&baz='
|
30
30
|
# # Doesn't match '?foo=bar', '?baz='
|
31
31
|
# end
|
32
32
|
#
|
33
|
-
# r.on params!: ['foo', 'baz'] do |
|
33
|
+
# r.on params!: ['foo', 'baz'] do |foo, baz|
|
34
34
|
# # Matches '?foo=bar&baz=quuz'
|
35
35
|
# # Doesn't match '?foo=bar', '?baz=', '?foo=&baz=', '?foo=bar&baz='
|
36
36
|
# end
|
37
37
|
#
|
38
|
+
# Because users have some control over the types of submitted parameters,
|
39
|
+
# it is recommended that you explicitly force the correct type for values
|
40
|
+
# yielded by the block:
|
41
|
+
#
|
42
|
+
# r.get(:param=>'foo') do |foo|
|
43
|
+
# foo = foo.to_s
|
44
|
+
# end
|
38
45
|
module ParamMatchers
|
39
46
|
module RequestMethods
|
40
47
|
# Match the given parameter if present, even if the parameter is empty.
|
data/lib/roda/plugins/path.rb
CHANGED
@@ -32,8 +32,11 @@ class Roda
|
|
32
32
|
# end
|
33
33
|
#
|
34
34
|
# r.post 'bar' do
|
35
|
-
#
|
36
|
-
#
|
35
|
+
# bar_params = r.params['bar']
|
36
|
+
# if bar_params.is_a?(Hash)
|
37
|
+
# bar = Bar.create(bar_params)
|
38
|
+
# r.redirect bar_path(bar) # /bar/1
|
39
|
+
# end
|
37
40
|
# end
|
38
41
|
#
|
39
42
|
# r.post 'baz' do
|
@@ -7,10 +7,10 @@ class Roda
|
|
7
7
|
# the default behaviour is used.
|
8
8
|
#
|
9
9
|
# Examples:
|
10
|
-
# r.is "needs_authorization"
|
10
|
+
# r.is "needs_authorization" do
|
11
11
|
# response.status = :unauthorized
|
12
12
|
# end
|
13
|
-
# r.is "nothing"
|
13
|
+
# r.is "nothing" do
|
14
14
|
# response.status = :no_content
|
15
15
|
# end
|
16
16
|
#
|