rack-reducer 0.1.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 +7 -0
- data/README.md +382 -0
- data/lib/rack/reducer.rb +46 -0
- data/lib/rack/reducer/parser.rb +20 -0
- data/lib/rack/reducer/reduction.rb +49 -0
- data/lib/rack/reducer/refinements.rb +30 -0
- data/spec/behavior.rb +44 -0
- data/spec/benchmarks.rb +56 -0
- data/spec/fixtures.rb +19 -0
- data/spec/middleware_spec.rb +22 -0
- data/spec/rails_example/app/channels/application_cable/channel.rb +4 -0
- data/spec/rails_example/app/channels/application_cable/connection.rb +4 -0
- data/spec/rails_example/app/controllers/application_controller.rb +2 -0
- data/spec/rails_example/app/controllers/artists_controller.rb +53 -0
- data/spec/rails_example/app/jobs/application_job.rb +2 -0
- data/spec/rails_example/app/mailers/application_mailer.rb +4 -0
- data/spec/rails_example/app/models/application_record.rb +3 -0
- data/spec/rails_example/app/models/artist.rb +18 -0
- data/spec/rails_example/config/application.rb +35 -0
- data/spec/rails_example/config/boot.rb +3 -0
- data/spec/rails_example/config/environment.rb +5 -0
- data/spec/rails_example/config/environments/development.rb +47 -0
- data/spec/rails_example/config/environments/production.rb +83 -0
- data/spec/rails_example/config/environments/test.rb +42 -0
- data/spec/rails_example/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/rails_example/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails_example/config/initializers/cors.rb +16 -0
- data/spec/rails_example/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/rails_example/config/initializers/inflections.rb +16 -0
- data/spec/rails_example/config/initializers/mime_types.rb +4 -0
- data/spec/rails_example/config/initializers/schema.rb +13 -0
- data/spec/rails_example/config/initializers/wrap_parameters.rb +14 -0
- data/spec/rails_example/config/puma.rb +56 -0
- data/spec/rails_example/config/routes.rb +4 -0
- data/spec/rails_example/db/seeds.rb +7 -0
- data/spec/rails_example/test/test_helper.rb +10 -0
- data/spec/rails_spec.rb +7 -0
- data/spec/sinatra_functional_spec.rb +32 -0
- data/spec/sinatra_mixin_spec.rb +26 -0
- data/spec/spec_helper.rb +13 -0
- metadata +278 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1fd376a5091b8dac2c04e97b36e3e8880e7cfaecf01e1ef25d69580b30ba0bc4
|
4
|
+
data.tar.gz: 7511b21435b309a104381dc2a5a3ccaad4b3b916d937de986c7087b7632ebe95
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 482102225a82151f8bbb0036ad4234f9caa741d112477f367e36c63e5c86e688defff43c130f70a0e88b1d006673e42d095228eeb376a86acd0d327c441a14e1
|
7
|
+
data.tar.gz: af0473c9840adfea73479dffe3def6c96ca46fa9b1452bbefd9c680b1231f767ff9179dcedfa30a501b10b5726f77e677e500ff666db4ce73b70b128049e788f
|
data/README.md
ADDED
@@ -0,0 +1,382 @@
|
|
1
|
+
Rack::Reducer
|
2
|
+
==========================================
|
3
|
+
Safely map URL params to database filters, in any Rack app. If your users need
|
4
|
+
to filter or sort data by making HTTP requests, this gem can help.
|
5
|
+
|
6
|
+
If you're working in Rails, note that Rack::Reducer solves the same problem
|
7
|
+
as Platformatec's excellent [HasScope][has_scope]. But Rack::Reducer works in
|
8
|
+
any Rack app, with any ORM, or without an ORM at all. Even in Rails, Reducer's
|
9
|
+
simpler, more functional API may be a better fit for your needs.
|
10
|
+
|
11
|
+
Install
|
12
|
+
------------------------------------------
|
13
|
+
Add `rack-reducer` to your Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'rack-reducer', require: 'rack/reducer'
|
17
|
+
```
|
18
|
+
|
19
|
+
Use
|
20
|
+
------------------------------------------
|
21
|
+
Rack::Reducer maps incoming URL params to an array of filter functions you
|
22
|
+
define, chains the applicable filters, and returns filtered data.
|
23
|
+
|
24
|
+
Suppose you have some incoming requests like these...
|
25
|
+
|
26
|
+
`GET /artists`
|
27
|
+
`GET /artists?name=janelle+monae`
|
28
|
+
`GET /artists?name=blake&genre=electronic`
|
29
|
+
|
30
|
+
You want to filter your `artists` table by name and/or genre when those
|
31
|
+
params are present, or return all artists otherwise.
|
32
|
+
|
33
|
+
Even with just a few optional filters, running them conditonally via `if`
|
34
|
+
statements gets messy.
|
35
|
+
|
36
|
+
### A Mess
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
# app/controllers/artists_controller.rb
|
40
|
+
class ArtistsController < ApplicationController
|
41
|
+
def index
|
42
|
+
@artists = Artist.all
|
43
|
+
@artists = @artists.where('lower(name) like ?', "%#{name.downcase}%") if params[:name]
|
44
|
+
@artists = @artists.where(genre: params[:genre]) if params[:genre]
|
45
|
+
@artists = @artists.order(params[:order].to_sym) if params[:order]
|
46
|
+
# ...
|
47
|
+
# ...
|
48
|
+
# pages later...
|
49
|
+
@artists.all.to_json
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
Rack::Reducer lets you chain filters elegantly, whether you're chaining two
|
54
|
+
filters or twenty. You can use it by extending it in your models, or by
|
55
|
+
calling it as a function.
|
56
|
+
|
57
|
+
### Cleaned up by extending Rack::Reducer
|
58
|
+
Call `Model.reduce(params)` in your controllers...
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
# app/controllers/artists_controller.rb
|
62
|
+
class ArtistsController < ApplicatonController
|
63
|
+
def index
|
64
|
+
@artists = Artist.reduce(params)
|
65
|
+
@artists.all.to_json
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
... and `extend Rack::Reducer` in your models:
|
71
|
+
```ruby
|
72
|
+
# app/models/artist.rb
|
73
|
+
class Artist < ActiveRecord::Base
|
74
|
+
extend Rack::Reducer # makes `self.reduce` available at class level
|
75
|
+
|
76
|
+
# configure by calling
|
77
|
+
# `reduces(some_initial_scope, filters: [an, array, of, lambdas])`
|
78
|
+
#
|
79
|
+
# filters can use any methods your initial dataset understands.
|
80
|
+
# here it's an ActiveRecord query, so filters use AR query methods
|
81
|
+
reduces self.all, filters: [
|
82
|
+
->(name:) { where('lower(name) like ?', "%#{name.downcase}%") },
|
83
|
+
->(genre:) { where(genre: genre) },
|
84
|
+
->(order:) { order(order.to_sym) },
|
85
|
+
]
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
### Cleaned up by calling Rack::Reducer as a function
|
90
|
+
If you prefer composition to inheritance, you can call Rack::Reducer as a
|
91
|
+
function instead of extending it. The functional style can (a) help keep
|
92
|
+
your filtering logic in one file, and (b) let you use Rack::Reducer without
|
93
|
+
polluting your model's methods.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
# app/controllers/artists_controller.rb
|
97
|
+
class ArtistsController < ApplicationController
|
98
|
+
def index
|
99
|
+
@artists = Rack::Reducer.call(params, dataset: Artist.all, filters: [
|
100
|
+
->(name:) { where('lower(name) like ?', "%#{name.downcase}%") },
|
101
|
+
->(genre:) { where(genre: genre) },
|
102
|
+
->(order:) { order(order.to_sym) },
|
103
|
+
])
|
104
|
+
@artists.all.to_json
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
The mixin style requires less boilerplate, and is stylistically Railsier.
|
110
|
+
The functional style is more flexible. Both styles are supported, tested, and
|
111
|
+
handle requests identically. In the examples above:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# GET /artists returns all artists, e.g.
|
115
|
+
[
|
116
|
+
{ "name": "Blake Mills", "genre": "alternative" },
|
117
|
+
{ "name": "Björk", "genre": "electronic" },
|
118
|
+
{ "name": "James Blake", "genre": "electronic" },
|
119
|
+
{ "name": "Janelle Monae", "genre": "alt-soul" },
|
120
|
+
{ "name": "SZA", "genre": "alt-soul" }
|
121
|
+
]
|
122
|
+
|
123
|
+
# GET /artists?name=blake returns e.g.
|
124
|
+
[
|
125
|
+
{ "name": "Blake Mills", "genre": "alternative" },
|
126
|
+
{ "name": "James Blake", "genre": "electronic" }
|
127
|
+
]
|
128
|
+
|
129
|
+
# GET /artists?name=blake&genre=electronic returns e.g.
|
130
|
+
[{ "name": "James Blake", "genre": "electronic" }]
|
131
|
+
```
|
132
|
+
|
133
|
+
|
134
|
+
Framework-specific Examples
|
135
|
+
---------------------------
|
136
|
+
These examples apply Rack::Reducer in different frameworks, with a different
|
137
|
+
ORM each time. The pairings of ORMs and frameworks are abitrary, just to
|
138
|
+
demonstrate a few possible stacks.
|
139
|
+
|
140
|
+
- [Sinatra](#sinatrasequel)
|
141
|
+
- [Rack Middleware](#rack-middlewarehash)
|
142
|
+
- [Rails](#railsadvanced)
|
143
|
+
|
144
|
+
### Sinatra/Sequel
|
145
|
+
This example uses [Sinatra][sinatra] to handle requests, and [Sequel][sequel]
|
146
|
+
as an ORM.
|
147
|
+
|
148
|
+
#### Mixin-style
|
149
|
+
```ruby
|
150
|
+
# sintra_mixin_style.rb
|
151
|
+
class SinatraMixinApp < Sinatra::Base
|
152
|
+
class Artist < Sequel::Model
|
153
|
+
extend Rack::Reducer
|
154
|
+
reduces self.dataset, filters: [
|
155
|
+
->(genre:) { where(genre: genre) },
|
156
|
+
->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
|
157
|
+
->(order:) { order(order.to_sym) },
|
158
|
+
]
|
159
|
+
end
|
160
|
+
|
161
|
+
get '/artists' do
|
162
|
+
@artists = Artist.reduce(params)
|
163
|
+
@artists.all.to_json
|
164
|
+
end
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
#### Functional style
|
169
|
+
```ruby
|
170
|
+
# sinatra_functional_style.rb
|
171
|
+
class SinatraFunctionalApp < Sinatra::Base
|
172
|
+
DB = Sequel.connect ENV['DATABASE_URL']
|
173
|
+
|
174
|
+
get '/artists' do
|
175
|
+
# dataset is a Sequel::Dataset, so filters use Sequel query methods
|
176
|
+
@artists = Rack::Reducer.call(params, dataset: DB[:artists], filters: [
|
177
|
+
->(genre:) { where(genre: genre) },
|
178
|
+
->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
|
179
|
+
->(order:) { order(order.to_sym) },
|
180
|
+
])
|
181
|
+
@artists.all.to_json
|
182
|
+
end
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
### Rack Middleware/Hash
|
187
|
+
This example runs a raw Rack app with Rack::Reducer mounted as middleware.
|
188
|
+
It doesn't use an ORM at all -- it just stores data in a hash.
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
# config.ru
|
192
|
+
require 'rack'
|
193
|
+
require 'rack/reducer'
|
194
|
+
require 'json'
|
195
|
+
|
196
|
+
ARTISTS = [
|
197
|
+
{ name: 'Blake Mills', genre: 'alternative' },
|
198
|
+
{ name: 'Björk', genre: 'electronic' },
|
199
|
+
{ name: 'James Blake', genre: 'electronic' },
|
200
|
+
{ name: 'Janelle Monae', genre: 'alt-soul' },
|
201
|
+
{ name: 'SZA', genre: 'alt-soul' },
|
202
|
+
]
|
203
|
+
|
204
|
+
app = Rack::Builder.new do
|
205
|
+
# dataset is a hash, so filter functions use ruby hash methods
|
206
|
+
use Rack::Reducer, dataset: ARTISTS, filters: [
|
207
|
+
->(genre:) { select { |item| item[:genre].match(/#{genre}/i) } },
|
208
|
+
->(name:) { select { |item| item[:name].match(/#{name}/i) } },
|
209
|
+
->(order:) { sort_by { |item| item[order.to_sym] } },
|
210
|
+
]
|
211
|
+
run ->(env) { [200, {}, [env['rack.reduction'].to_json]] }
|
212
|
+
end
|
213
|
+
|
214
|
+
run app
|
215
|
+
```
|
216
|
+
|
217
|
+
When Rack::Reducer is mounted as middleware, it stores its filtered data in
|
218
|
+
env['rack.reduction'], then calls the next app in the middleware stack. You can
|
219
|
+
change the `env` key by passing a new name as option to `use`:
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
# config.ru
|
223
|
+
use Rack::Reducer, key: 'myapp.custom_key', dataset: ARTISTS, filters: [
|
224
|
+
#an array of lambdas
|
225
|
+
]
|
226
|
+
```
|
227
|
+
|
228
|
+
### Rails/Advanced
|
229
|
+
The examples in the [introduction](#use) cover basic Rails use. The examples
|
230
|
+
below cover more advanced use.
|
231
|
+
|
232
|
+
If you're comfortable in a non-Rails stack, you can apply these advanced
|
233
|
+
techniques there too. I wholeheartedly endorse [Roda][roda], and use
|
234
|
+
Rack::Reducer with Roda/Sequel in production.
|
235
|
+
|
236
|
+
#### Chaining reduce with other ActiveRecord query methods
|
237
|
+
In the mixin-style, you can chain `Model.reduce` with other ActiveRecord
|
238
|
+
queries, as long as `reduce` is the first call in the chain:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
# app/models/artist.rb
|
242
|
+
class Artist < ApplicationRecord
|
243
|
+
extend Rack::Reducer
|
244
|
+
reduces self.all, filters: [
|
245
|
+
# filters get instance_exec'd against the initial dataset,
|
246
|
+
# in this case `self.all`, so filters can use query methods, scopes, etc
|
247
|
+
->(name:) { by_name(name) },
|
248
|
+
->(genre:) { where(genre: genre) },
|
249
|
+
->(order:) { order(order.to_sym) }
|
250
|
+
]
|
251
|
+
|
252
|
+
scope :by_name, lambda { |name|
|
253
|
+
where('lower(name) like ?', "%#{name.downcase}%")
|
254
|
+
}
|
255
|
+
|
256
|
+
# here's a scope we're not using in our Reducer filters,
|
257
|
+
# but will use in our controller
|
258
|
+
scope :signed, lambda { where(signed: true) }
|
259
|
+
end
|
260
|
+
|
261
|
+
# app/controllers/artists_controller.rb
|
262
|
+
class ArtistsController < ApplicationController
|
263
|
+
def index
|
264
|
+
# you can chain reduce with other ActiveRecord queries,
|
265
|
+
# as long as reduce is first in the chain
|
266
|
+
@artists = Artist.reduce(params).signed
|
267
|
+
@artists.to_json
|
268
|
+
end
|
269
|
+
end
|
270
|
+
```
|
271
|
+
|
272
|
+
|
273
|
+
#### Dynamically setting Reducer's initial dataset
|
274
|
+
Rack::Reducer's mixin style only lets you target one dataset for reduction.
|
275
|
+
If you need different initial data in different contexts, and don't want to
|
276
|
+
determine that data via filters, you can use the functional style:
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
# app/controllers/artists_controller.rb
|
280
|
+
class ArtistsController < ApplicationController
|
281
|
+
def index
|
282
|
+
@scope = current_user.admin? ? Artist.all : Artist.signed
|
283
|
+
@artists = Rack::Reducer.call(params, dataset: @scope, filters: [
|
284
|
+
->(name:) { by_name(name) },
|
285
|
+
->(genre:) { where(genre: genre) },
|
286
|
+
->(order:) { order(order.to_sym) }
|
287
|
+
])
|
288
|
+
@artists.to_json
|
289
|
+
end
|
290
|
+
end
|
291
|
+
```
|
292
|
+
|
293
|
+
#### Default filters
|
294
|
+
Most of the time it makes sense to use *required* keyword arguments for each
|
295
|
+
filter, and skip running the filter altogether when the keyword argments aren't
|
296
|
+
present.
|
297
|
+
|
298
|
+
But you may want to run a filter always, with a sensible default when the params
|
299
|
+
don't specify a value. Ordering results is a common case.
|
300
|
+
|
301
|
+
The code below will order by `params[:order]` when it exists, and by name
|
302
|
+
otherwise.
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
# app/controllers/artists_controller.rb
|
306
|
+
class ArtistsController < ApplicationController
|
307
|
+
def index
|
308
|
+
@artists = Rack::Reducer.call(params, dataset: Artist.all, filters: [
|
309
|
+
->(genre:) { where(genre: genre) },
|
310
|
+
->(order: 'name') { order(order.to_sym) }
|
311
|
+
])
|
312
|
+
@artists.to_json
|
313
|
+
end
|
314
|
+
end
|
315
|
+
```
|
316
|
+
|
317
|
+
|
318
|
+
How Rack::Reducer Works
|
319
|
+
--------------------------------------
|
320
|
+
Rack::Reducer takes a dataset, a params hash, and an array of lambda functions.
|
321
|
+
|
322
|
+
To return filtered data, it calls [reduce][reduce] on your array of lambdas,
|
323
|
+
with the reduction's initial value set to `dataset`.
|
324
|
+
|
325
|
+
Each reduction looks for keys in the `params` hash that match the
|
326
|
+
current lambda's [keyword arguments][keywords]. If the keys exist, it
|
327
|
+
`instance_exec`s the lambda against the dataset, passing just those keys as
|
328
|
+
arguments, and finally passes the filtered dataset on to the next lambda.
|
329
|
+
|
330
|
+
Lambdas that don't find all their required keyword arguments in `params` don't
|
331
|
+
execute at all, and just pass the unaltered dataset down the chain.
|
332
|
+
|
333
|
+
The reason Reducer works with any ORM is that *you* supply the dataset and
|
334
|
+
filter functions. Reducer doesn't need to know anything about ActiveRecord,
|
335
|
+
Mongoid, etc -- it just `instance_exec`s your own code against your own dataset.
|
336
|
+
|
337
|
+
### Security
|
338
|
+
Rack::Reducer claims to "safely" map URL params to filters, but it accepts an
|
339
|
+
unfiltered params hash. What gives?
|
340
|
+
|
341
|
+
By using keyword arguments in your filter lambdas, you are *explicitly* naming
|
342
|
+
the params you'll accept into your filters. Params that aren't keywords never
|
343
|
+
get evaluated.
|
344
|
+
|
345
|
+
For even more security, you can typecast the params in your filters. Most ORMs
|
346
|
+
handle this for you, but as an example:
|
347
|
+
|
348
|
+
```ruby
|
349
|
+
FILTERS = [
|
350
|
+
# typecast params[:name] to a string
|
351
|
+
->(name:) { where(name: name.to_s) },
|
352
|
+
# typecast params[:updated_before] and params[:updated_after]
|
353
|
+
# to times, and set a default for updated_after if it's missing
|
354
|
+
lambda |updated_before:, updated_after: 1.month.ago| {
|
355
|
+
where(updated_at: updated_after.to_time..updated_before.to_time)
|
356
|
+
}
|
357
|
+
]
|
358
|
+
```
|
359
|
+
|
360
|
+
### Performance
|
361
|
+
According to `spec/benchmarks.rb`, Rack::Reducer executes about 90% as quickly
|
362
|
+
as a set of hard-coded conditional filters. It is extremly unlikely to be a
|
363
|
+
bottleneck in your application.
|
364
|
+
|
365
|
+
|
366
|
+
|
367
|
+
Contributing
|
368
|
+
-------------------------------
|
369
|
+
### Bugs
|
370
|
+
Open [an issue](https://github.com/chrisfrank/rack-reducer/issues) on Github.
|
371
|
+
|
372
|
+
### Pull Requests
|
373
|
+
Please include tests, following the style of the specs in `spec/*_spec.rb`.
|
374
|
+
|
375
|
+
|
376
|
+
|
377
|
+
[has_scope]: https://github.com/plataformatec/has_scope
|
378
|
+
[sinatra]: https://github.com/sinatra/sinatra
|
379
|
+
[sequel]: https://github.com/jeremyevans/sequel
|
380
|
+
[roda]: https://github.com/jeremyevans/roda
|
381
|
+
[reduce]: http://ruby-doc.org/core-2.5.0/Enumerable.html#method-i-reduce
|
382
|
+
[keywords]: https://robots.thoughtbot.com/ruby-2-keyword-arguments
|
data/lib/rack/reducer.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack/request'
|
4
|
+
require_relative 'reducer/reduction'
|
5
|
+
|
6
|
+
module Rack
|
7
|
+
# use request params to apply filters to a dataset
|
8
|
+
module Reducer
|
9
|
+
# call Rack::Reducer as a function, instead of mounting it as middleware
|
10
|
+
def self.call(params, dataset:, filters:)
|
11
|
+
Reduction.new(
|
12
|
+
nil, # first arg to Reduction is `app`, which is for middleware only
|
13
|
+
params: params,
|
14
|
+
filters: filters,
|
15
|
+
dataset: dataset,
|
16
|
+
).reduce
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.new(app, options = {})
|
20
|
+
Reduction.new(app, options)
|
21
|
+
end
|
22
|
+
|
23
|
+
# extend Rack::Reducer to get `reduce` and `reduces` as class-methods
|
24
|
+
#
|
25
|
+
# class Artist < SomeORM::Model
|
26
|
+
# extend Rack::Reducer
|
27
|
+
# reduces self.all, filters: [
|
28
|
+
# lambda { |name:| where(name: name) },
|
29
|
+
# lambda { |genre:| where(genre: genre) },
|
30
|
+
# ]
|
31
|
+
# end
|
32
|
+
def reduce(params)
|
33
|
+
Reduction.new(
|
34
|
+
nil,
|
35
|
+
params: params,
|
36
|
+
filters: @rack_reducer_filters,
|
37
|
+
dataset: @rack_reducer_dataset
|
38
|
+
).reduce
|
39
|
+
end
|
40
|
+
|
41
|
+
def reduces(dataset, filters:)
|
42
|
+
@rack_reducer_dataset = dataset
|
43
|
+
@rack_reducer_filters = filters
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Reducer
|
5
|
+
# convert params from Sinatra, Rails, Roda, etc into a symbol hash
|
6
|
+
module Parser
|
7
|
+
def self.call(data)
|
8
|
+
data.is_a?(Hash) ? data : hashify(data)
|
9
|
+
end
|
10
|
+
|
11
|
+
# turns out a Rails params hash is not really a hash
|
12
|
+
# it's safe to call .to_unsafe_hash here, because params
|
13
|
+
# are automatically sanitized by the lambda keywords
|
14
|
+
def self.hashify(data)
|
15
|
+
fn = %i[to_unsafe_h to_h].find { |name| data.respond_to?(name) }
|
16
|
+
data.send(fn)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|