rack-reducer 1.0.1 → 1.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 +4 -4
- data/README.md +112 -252
- data/lib/rack/reducer/middleware.rb +17 -2
- data/lib/rack/reducer/reduction.rb +19 -16
- data/lib/rack/reducer/refinements.rb +13 -1
- data/lib/rack/reducer/version.rb +3 -1
- data/lib/rack/reducer/warnings.rb +27 -0
- data/lib/rack/reducer.rb +51 -20
- data/spec/benchmarks.rb +51 -21
- data/spec/fixtures.rb +30 -0
- data/spec/middleware_spec.rb +55 -23
- data/spec/rails_spec.rb +33 -3
- data/spec/reducer_spec.rb +104 -0
- data/spec/spec_helper.rb +6 -15
- metadata +34 -136
- data/lib/rack/reducer/parser.rb +0 -26
- data/spec/_hanami_example/apps/web/application.rb +0 -326
- data/spec/_hanami_example/apps/web/config/routes.rb +0 -4
- data/spec/_hanami_example/apps/web/controllers/artists/index.rb +0 -12
- data/spec/_hanami_example/apps/web/views/application_layout.rb +0 -7
- data/spec/_hanami_example/config/boot.rb +0 -2
- data/spec/_hanami_example/config/environment.rb +0 -29
- data/spec/_hanami_example/lib/hanami_example/entities/artist.rb +0 -2
- data/spec/_hanami_example/lib/hanami_example/repositories/artist_repository.rb +0 -9
- data/spec/_hanami_example/lib/hanami_example.rb +0 -5
- data/spec/_rails_example/app/channels/application_cable/channel.rb +0 -4
- data/spec/_rails_example/app/channels/application_cable/connection.rb +0 -4
- data/spec/_rails_example/app/controllers/application_controller.rb +0 -2
- data/spec/_rails_example/app/controllers/artists_controller.rb +0 -8
- data/spec/_rails_example/app/jobs/application_job.rb +0 -2
- data/spec/_rails_example/app/mailers/application_mailer.rb +0 -4
- data/spec/_rails_example/app/models/application_record.rb +0 -3
- data/spec/_rails_example/app/models/rails_example/artist.rb +0 -21
- data/spec/_rails_example/config/application.rb +0 -35
- data/spec/_rails_example/config/boot.rb +0 -3
- data/spec/_rails_example/config/environment.rb +0 -5
- data/spec/_rails_example/config/environments/development.rb +0 -47
- data/spec/_rails_example/config/environments/production.rb +0 -83
- data/spec/_rails_example/config/environments/test.rb +0 -42
- data/spec/_rails_example/config/initializers/application_controller_renderer.rb +0 -8
- data/spec/_rails_example/config/initializers/backtrace_silencers.rb +0 -7
- data/spec/_rails_example/config/initializers/cors.rb +0 -16
- data/spec/_rails_example/config/initializers/filter_parameter_logging.rb +0 -4
- data/spec/_rails_example/config/initializers/inflections.rb +0 -16
- data/spec/_rails_example/config/initializers/mime_types.rb +0 -4
- data/spec/_rails_example/config/initializers/wrap_parameters.rb +0 -14
- data/spec/_rails_example/config/puma.rb +0 -56
- data/spec/_rails_example/config/routes.rb +0 -4
- data/spec/_rails_example/db/seeds.rb +0 -7
- data/spec/behavior.rb +0 -51
- data/spec/hanami_spec.rb +0 -6
- data/spec/roda_spec.rb +0 -13
- data/spec/sinatra_functional_spec.rb +0 -26
- data/spec/sinatra_mixin_spec.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96e9d87cd9c12373fb42f37eedb4246d6296009faab5bb5f1831236a4c200b0f
|
4
|
+
data.tar.gz: cb1e793d45ba3e71f588b22df0469358a40f56ab3167d8dc734b105306a841f0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ebb52de791e396e475cbfb76040645120e4892f12d9fc9f3b7901d86a9b52adf2d7314b6795472640fb5a3eb2c1f44b49191a823457a9b94588fea129a530870
|
7
|
+
data.tar.gz: c373da2d40a7e3da1b281408636c38107430a7583155f3923539e8db7ce85c09cffd2b1233fc867dc1f56dd94e86281dd7820a9131d5ed544f1dd3439def249b
|
data/README.md
CHANGED
@@ -3,15 +3,7 @@ Rack::Reducer
|
|
3
3
|
[](https://travis-ci.org/chrisfrank/rack-reducer)
|
4
4
|
[](https://codeclimate.com/github/chrisfrank/rack-reducer/maintainability)
|
5
5
|
|
6
|
-
|
7
|
-
succint as
|
8
|
-
|
9
|
-
```ruby
|
10
|
-
@artists = Artist.reduce(params)
|
11
|
-
```
|
12
|
-
|
13
|
-
Rack::Reducer works in any Rack-compatible app, with any ORM, and has no
|
14
|
-
dependencies beyond Rack itself.
|
6
|
+
Declaratively filter data via URL params, in any Rack app, with any ORM.
|
15
7
|
|
16
8
|
Install
|
17
9
|
------------------------------------------
|
@@ -21,85 +13,45 @@ Add `rack-reducer` to your Gemfile:
|
|
21
13
|
gem 'rack-reducer', require: 'rack/reducer'
|
22
14
|
```
|
23
15
|
|
16
|
+
Rack::Reducer has zero dependencies beyond Rack itself.
|
17
|
+
|
24
18
|
Use
|
25
19
|
------------------------------------------
|
26
20
|
If your app needs to render a list of database records, you probably want those
|
27
21
|
records to be filterable via URL params, like so:
|
28
22
|
|
29
23
|
```
|
30
|
-
GET /artists?name=blake` => artists named 'blake'
|
31
|
-
GET /artists?genre=electronic&sort=name => electronic artists, sorted by name
|
32
24
|
GET /artists => all artists
|
25
|
+
GET /artists?name=blake` => artists named 'blake'
|
26
|
+
GET /artists?genre=electronic&name=blake => electronic artists named 'blake'
|
33
27
|
```
|
34
28
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
Rack::Reducer can help. It maps incoming URL params to an array of filter
|
39
|
-
functions you define, applies only the applicable filters, and returns your
|
40
|
-
filtered data.
|
41
|
-
|
42
|
-
You can use Rack::Reducer in your choice of two styles: **mixin** or
|
43
|
-
**functional**.
|
44
|
-
|
45
|
-
### Mixin style
|
46
|
-
Call `Model.reduce(params)` in your controllers...
|
29
|
+
Rack::Reducer can help. It applies incoming URL params to an array of filter
|
30
|
+
functions you define, runs only the relevant filters, and returns your filtered
|
31
|
+
data. Here’s how you might use it in a Rails controller:
|
47
32
|
|
48
33
|
```ruby
|
49
34
|
# app/controllers/artists_controller.rb
|
50
35
|
class ArtistsController < ApplicationController
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
55
|
-
end
|
56
|
-
```
|
57
|
-
|
58
|
-
...and `extend Rack::Reducer` in your models:
|
59
|
-
|
60
|
-
```ruby
|
61
|
-
# app/models/artist.rb
|
62
|
-
class Artist < ActiveRecord::Base
|
63
|
-
extend Rack::Reducer
|
64
|
-
|
65
|
-
# Configure by calling
|
66
|
-
# `reduces(some_initial_scope, filters: [an, array, of, lambdas])`
|
67
|
-
#
|
68
|
-
# Filters can use any methods your initial dataset understands,
|
69
|
-
# in this case Artist class methods and scopes
|
70
|
-
reduces self.all, filters: [
|
36
|
+
# Step 1: Create a reducer
|
37
|
+
ArtistReducer = Rack::Reducer.create(
|
38
|
+
Artist.all,
|
71
39
|
->(name:) { where('lower(name) like ?', "%#{name.downcase}%") },
|
72
40
|
->(genre:) { where(genre: genre) },
|
73
|
-
|
74
|
-
]
|
75
|
-
end
|
76
|
-
```
|
41
|
+
)
|
77
42
|
|
78
|
-
|
79
|
-
Call Rack::Reducer as a function, maybe right in your controllers, maybe in
|
80
|
-
a dedicated [query object][query_obj], or really anywhere you like:
|
81
|
-
|
82
|
-
```ruby
|
83
|
-
# app/controllers/artists_controller.rb
|
84
|
-
class ArtistsController < ApplicationController
|
43
|
+
# Step 2: Apply the reducer to incoming requests
|
85
44
|
def index
|
86
|
-
@artists =
|
87
|
-
->(name:) { where('lower(name) like ?', "%#{name.downcase}%") },
|
88
|
-
->(genre:) { where(genre: genre) },
|
89
|
-
->(sort:) { order(sort.to_sym) },
|
90
|
-
])
|
45
|
+
@artists = ArtistReducer.apply(params)
|
91
46
|
render json: @artists
|
92
47
|
end
|
93
48
|
end
|
94
49
|
```
|
95
50
|
|
96
|
-
|
97
|
-
flexible. Both styles are supported, tested, and handle requests identically.
|
98
|
-
|
99
|
-
In the examples above:
|
51
|
+
This example app would handle requests as follows:
|
100
52
|
|
101
53
|
```ruby
|
102
|
-
# GET /artists
|
54
|
+
# GET /artists => All artists:
|
103
55
|
[
|
104
56
|
{ "name": "Blake Mills", "genre": "alternative" },
|
105
57
|
{ "name": "Björk", "genre": "electronic" },
|
@@ -108,79 +60,48 @@ In the examples above:
|
|
108
60
|
{ "name": "SZA", "genre": "alt-soul" }
|
109
61
|
]
|
110
62
|
|
111
|
-
# GET /artists?name=blake
|
63
|
+
# GET /artists?name=blake => Artists named "blake":
|
112
64
|
[
|
113
65
|
{ "name": "Blake Mills", "genre": "alternative" },
|
114
66
|
{ "name": "James Blake", "genre": "electronic" }
|
115
67
|
]
|
116
68
|
|
117
|
-
# GET /artists?name=blake&genre=electronic
|
69
|
+
# GET /artists?name=blake&genre=electronic => Electronic artists named "blake"
|
118
70
|
[{ "name": "James Blake", "genre": "electronic" }]
|
119
71
|
```
|
120
72
|
|
121
|
-
|
122
73
|
Framework-specific Examples
|
123
74
|
---------------------------
|
124
75
|
These examples apply Rack::Reducer in different frameworks and ORMs. The
|
125
76
|
pairings of ORMs and frameworks are arbitrary, just to demonstrate a few
|
126
77
|
possible stacks.
|
127
78
|
|
128
|
-
- [Sinatra/Sequel](#sinatrasequel)
|
129
|
-
- [Rack Middleware/Ruby Hash](#rack-middlewarehash)
|
130
|
-
- [Roda](#roda)
|
131
|
-
- [Hanami](#hanami)
|
132
|
-
- [Advanced use in Rails and other frameworks](#advanced-use-in-rails-and-other-frameworks)
|
133
|
-
|
134
79
|
### Sinatra/Sequel
|
135
80
|
This example uses [Sinatra][sinatra] to handle requests, and [Sequel][sequel]
|
136
81
|
as an ORM.
|
137
82
|
|
138
|
-
#### Functional-style
|
139
83
|
```ruby
|
140
84
|
# sinatra_functional_style.rb
|
141
|
-
class
|
85
|
+
class SinatraExample < Sinatra::Base
|
142
86
|
DB = Sequel.connect ENV['DATABASE_URL']
|
143
87
|
|
144
88
|
# dataset is a Sequel::Dataset, so filters use Sequel query methods
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
->(sort:) { order(sort.to_sym) },
|
151
|
-
]
|
152
|
-
}
|
153
|
-
|
154
|
-
get '/artists' do
|
155
|
-
@artists = Rack::Reducer.call(params, QUERY)
|
156
|
-
@artists.to_a.to_json
|
157
|
-
end
|
158
|
-
end
|
159
|
-
```
|
160
|
-
|
161
|
-
#### Mixin-style
|
162
|
-
```ruby
|
163
|
-
# sintra_mixin_style.rb
|
164
|
-
class SinatraMixinApp < Sinatra::Base
|
165
|
-
class Artist < Sequel::Model
|
166
|
-
extend Rack::Reducer
|
167
|
-
reduces self.dataset, filters: [
|
168
|
-
->(genre:) { where(genre: genre) },
|
169
|
-
->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
|
170
|
-
->(sort:) { order(sort.to_sym) },
|
171
|
-
]
|
172
|
-
end
|
89
|
+
ArtistReducer = Rack::Reducer.create(
|
90
|
+
DB[:artists],
|
91
|
+
->(genre:) { where(genre: genre) },
|
92
|
+
->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
|
93
|
+
)
|
173
94
|
|
174
95
|
get '/artists' do
|
175
|
-
@artists =
|
176
|
-
@artists.
|
96
|
+
@artists = ArtistReducer.apply(params).all
|
97
|
+
@artists.to_json
|
177
98
|
end
|
178
99
|
end
|
179
100
|
```
|
180
101
|
|
181
|
-
### Rack Middleware/
|
102
|
+
### Rack Middleware/Ruby Array
|
182
103
|
This example runs a raw Rack app with Rack::Reducer mounted as middleware.
|
183
|
-
It doesn't use an ORM at all -- it just stores data in a ruby
|
104
|
+
It doesn't use an ORM at all -- it just stores data in a ruby array.
|
184
105
|
|
185
106
|
```ruby
|
186
107
|
# config.ru
|
@@ -197,8 +118,8 @@ ARTISTS = [
|
|
197
118
|
]
|
198
119
|
|
199
120
|
app = Rack::Builder.new do
|
200
|
-
# dataset
|
201
|
-
use Rack::Reducer, dataset: ARTISTS, filters: [
|
121
|
+
# dataset is an Array, so filter functions use Array methods
|
122
|
+
use Rack::Reducer::Middleware, dataset: ARTISTS, filters: [
|
202
123
|
->(genre:) { select { |item| item[:genre].match(/#{genre}/i) } },
|
203
124
|
->(name:) { select { |item| item[:name].match(/#{name}/i) } },
|
204
125
|
->(sort:) { sort_by { |item| item[sort.to_sym] } },
|
@@ -215,161 +136,92 @@ change the `env` key by passing a new name as option to `use`:
|
|
215
136
|
|
216
137
|
```ruby
|
217
138
|
# config.ru
|
218
|
-
use Rack::Reducer, key: '
|
219
|
-
#an array of lambdas
|
139
|
+
use Rack::Reducer::Midleware, key: 'custom.key', dataset: ARTISTS, filters: [
|
140
|
+
# an array of lambdas
|
220
141
|
]
|
221
142
|
```
|
222
143
|
|
223
|
-
###
|
224
|
-
|
225
|
-
|
144
|
+
### With Rails scopes
|
145
|
+
The Rails [quickstart example](#use) created a reducer inside a
|
146
|
+
controller, but if your filters use lots of ActiveRecord scopes, it might make
|
147
|
+
more sense to keep your reducers in your models instead.
|
226
148
|
|
227
149
|
```ruby
|
228
|
-
# app.rb
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
150
|
+
# app/models/artist.rb
|
151
|
+
class Artist < ApplicationRecord
|
152
|
+
# filters get instance_exec'd against the dataset you provide -- in this case
|
153
|
+
# it's `self.all` -- so filters can use query methods, scopes, etc
|
154
|
+
Reducer = Rack::Reducer.create(
|
155
|
+
self.all,
|
156
|
+
->(name:) { by_name(name) },
|
157
|
+
->(genre:) { where(genre: genre) },
|
158
|
+
->(sort:) { order(sort.to_sym) }
|
159
|
+
]
|
236
160
|
|
237
|
-
|
238
|
-
|
239
|
-
filters: [
|
240
|
-
->(genre:) { where(genre: genre) },
|
241
|
-
->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
|
242
|
-
->(sort:) { order(sort.to_sym) },
|
243
|
-
]
|
161
|
+
scope :by_name, lambda { |name|
|
162
|
+
where('lower(name) like ?', "%#{name.downcase}%")
|
244
163
|
}
|
245
|
-
# Note that QUERY[:dataset] is a Sequel::Dataset, so the functions
|
246
|
-
# in QUERY[:filters] use Sequel methods
|
247
|
-
|
248
|
-
route do |r|
|
249
|
-
r.get('artists') { Rack::Reducer.call(r.params, QUERY).to_a }
|
250
|
-
end
|
251
|
-
end
|
252
|
-
```
|
253
|
-
|
254
|
-
### Hanami
|
255
|
-
This example uses [Hanami][hanami] to handle requests, and hanami-model as an
|
256
|
-
ORM.
|
257
|
-
|
258
|
-
```ruby
|
259
|
-
# apps/web/controllers/artists/index.rb
|
260
|
-
module Web::Controllers::Artists
|
261
|
-
class Index
|
262
|
-
include Web::Action
|
263
|
-
|
264
|
-
def call(params)
|
265
|
-
@artists = ArtistRepository.new.reduce(params)
|
266
|
-
self.body = @artists.to_a.to_json
|
267
|
-
end
|
268
|
-
end
|
269
164
|
end
|
270
165
|
|
271
|
-
#
|
272
|
-
class
|
273
|
-
def
|
274
|
-
|
275
|
-
|
276
|
-
->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
|
277
|
-
->(sort:) { order(sort.to_sym) },
|
278
|
-
])
|
166
|
+
# app/controllers/artists_controller.rb
|
167
|
+
class ArtistsController < ApplicationController
|
168
|
+
def index
|
169
|
+
@artists = Artist::Reducer.apply(params)
|
170
|
+
render json: @artists
|
279
171
|
end
|
280
172
|
end
|
281
173
|
```
|
282
174
|
|
283
|
-
|
284
|
-
|
285
|
-
below cover more advanced use.
|
286
|
-
|
287
|
-
If you're comfortable in a non-Rails stack, you can apply these advanced
|
288
|
-
techniques there too.
|
289
|
-
|
290
|
-
#### Default filters
|
175
|
+
Default filters
|
176
|
+
------------------------------------------
|
291
177
|
Most of the time it makes sense to use *required* keyword arguments for each
|
292
178
|
filter, and skip running the filter altogether when the keyword argments aren't
|
293
179
|
present.
|
294
180
|
|
295
|
-
But you
|
296
|
-
|
297
|
-
|
298
|
-
The code below will order by `params[:sort]` when it exists, and by name
|
299
|
-
otherwise.
|
181
|
+
But sometimes you'll want to run a filter with a default value, even when the
|
182
|
+
required params are missing. The code below will order by `params[:sort]` when
|
183
|
+
it exists, and by name otherwise.
|
300
184
|
|
301
185
|
```ruby
|
302
186
|
# app/controllers/artists_controller.rb
|
303
187
|
class ArtistsController < ApplicationController
|
188
|
+
ArtistReducer = Rack::Reducer.create(
|
189
|
+
Artist.all,
|
190
|
+
->(genre:) { where(genre: genre) },
|
191
|
+
->(sort: 'name') { order(sort.to_sym) }
|
192
|
+
)
|
193
|
+
|
304
194
|
def index
|
305
|
-
@artists =
|
306
|
-
->(genre:) { where(genre: genre) },
|
307
|
-
->(sort: 'name') { order(sort.to_sym) }
|
308
|
-
])
|
195
|
+
@artists = ArtistReducer.apply(params)
|
309
196
|
render json: @artists
|
310
197
|
end
|
311
198
|
end
|
312
199
|
```
|
313
200
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
201
|
+
Calling Rack::Reducer as a function
|
202
|
+
-------------------------------------------
|
203
|
+
For a slight performance penalty (~5%), you can skip creating a reducer via
|
204
|
+
`::create` and just call Rack::Reducer as a function. This can be useful when
|
205
|
+
prototyping, mostly because you don't need to think about naming anything.
|
318
206
|
|
319
207
|
```ruby
|
320
208
|
# app/controllers/artists_controller.rb
|
321
209
|
class ArtistsController < ApplicationController
|
210
|
+
# Step 1: there is no step 2
|
322
211
|
def index
|
323
|
-
@
|
324
|
-
|
325
|
-
->(name:) { by_name(name) },
|
212
|
+
@artists = Rack::Reducer.call(params, dataset: Artist.all, filters: [
|
213
|
+
->(name:) { where('lower(name) like ?', "%#{name.downcase}%") },
|
326
214
|
->(genre:) { where(genre: genre) },
|
327
|
-
->(sort:) { order(sort.to_sym) }
|
328
215
|
])
|
329
216
|
render json: @artists
|
330
217
|
end
|
331
218
|
end
|
332
219
|
```
|
333
220
|
|
334
|
-
#### Chaining reduce with other ActiveRecord query methods
|
335
|
-
In the mixin-style, you can chain `Model.reduce` with other ActiveRecord
|
336
|
-
queries, as long as `reduce` is the first call in the chain:
|
337
|
-
|
338
|
-
```ruby
|
339
|
-
# app/models/artist.rb
|
340
|
-
class Artist < ApplicationRecord
|
341
|
-
extend Rack::Reducer
|
342
|
-
reduces self.all, filters: [
|
343
|
-
# filters get instance_exec'd against the initial dataset,
|
344
|
-
# in this case `self.all`, so filters can use query methods, scopes, etc
|
345
|
-
->(name:) { by_name(name) },
|
346
|
-
->(genre:) { where(genre: genre) },
|
347
|
-
->(sort:) { order(sort.to_sym) }
|
348
|
-
]
|
349
|
-
|
350
|
-
scope :by_name, lambda { |name|
|
351
|
-
where('lower(name) like ?', "%#{name.downcase}%")
|
352
|
-
}
|
353
|
-
|
354
|
-
# here's a scope we're not using in our Reducer filters,
|
355
|
-
# but will use in our controller
|
356
|
-
scope :signed, lambda { where(signed: true) }
|
357
|
-
end
|
358
|
-
|
359
|
-
# app/controllers/artists_controller.rb
|
360
|
-
class ArtistsController < ApplicationController
|
361
|
-
def index
|
362
|
-
# you can chain reduce with other ActiveRecord queries,
|
363
|
-
# as long as reduce is first in the chain
|
364
|
-
@artists = Artist.reduce(params).signed
|
365
|
-
render json: @artists
|
366
|
-
end
|
367
|
-
end
|
368
|
-
```
|
369
221
|
|
370
222
|
How Rack::Reducer Works
|
371
223
|
--------------------------------------
|
372
|
-
Rack::Reducer takes a dataset,
|
224
|
+
Rack::Reducer takes a dataset, an array of lambdas, and a params hash.
|
373
225
|
|
374
226
|
To return filtered data, it calls Enumerable#[reduce][reduce] on your array of
|
375
227
|
lambdas, with the reduction's initial value set to `dataset`.
|
@@ -387,42 +239,53 @@ filter functions. Reducer doesn't need to know anything about ActiveRecord,
|
|
387
239
|
Sequel, Mongoid, etc -- it just `instance_exec`s your own code against your
|
388
240
|
own dataset.
|
389
241
|
|
390
|
-
|
391
|
-
|
392
|
-
|
242
|
+
Performance
|
243
|
+
---------------------
|
244
|
+
For requests with empty params, Rack::Reducer has no measurable performance
|
245
|
+
impact. For requests with populated params, Rack::Reducer is about 10% slower
|
246
|
+
than a set of hand-coded conditionals, according to `spec/benchmarks.rb`.
|
393
247
|
|
394
|
-
|
395
|
-
|
396
|
-
|
248
|
+
```
|
249
|
+
Conditionals (full) 530.000 i/100ms
|
250
|
+
Reducer (full) 432.000 i/100ms
|
251
|
+
Conditionals (empty) 780.000 i/100ms
|
252
|
+
Reducer (empty) 808.000 i/100ms
|
253
|
+
Calculating -------------------------------------
|
254
|
+
Conditionals (full) 4.864k (± 2.3%) i/s - 24.380k in 5.015551s
|
255
|
+
Reducer (full) 4.384k (± 1.3%) i/s - 22.032k in 5.026651s
|
256
|
+
Conditionals (empty) 7.889k (± 1.7%) i/s - 39.780k in 5.043797s
|
257
|
+
Reducer (empty) 8.129k (± 1.7%) i/s - 41.208k in 5.070453s
|
258
|
+
|
259
|
+
Comparison:
|
260
|
+
Reducer (empty): 8129.5 i/s
|
261
|
+
Conditionals (empty): 7889.3 i/s - same-ish: difference falls within error
|
262
|
+
Conditionals (full): 4863.7 i/s - 1.67x slower
|
263
|
+
Reducer (full): 4383.8 i/s - 1.85x slower
|
264
|
+
```
|
397
265
|
|
398
|
-
|
399
|
-
|
266
|
+
In Rails, note that `params` is never empty, so use `request.query_parameters`
|
267
|
+
instead if you want to handle parameterless requests at top speed.
|
400
268
|
|
401
269
|
```ruby
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
# typecast params[:updated_before] and params[:updated_after]
|
406
|
-
# to times, and set a default for updated_after if it's missing
|
407
|
-
lambda |updated_before:, updated_after: 1.month.ago| {
|
408
|
-
where(updated_at: updated_after.to_time..updated_before.to_time)
|
409
|
-
}
|
410
|
-
]
|
411
|
-
```
|
270
|
+
# app/controllers/artists_controller.rb
|
271
|
+
class ArtistController < ApplicationController
|
272
|
+
# ArtistReducer = Rack::Reducer.create(...etc etc)
|
412
273
|
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
274
|
+
def index
|
275
|
+
@artists = ArtistReducer.apply(request.query_parameters)
|
276
|
+
render json: @artists
|
277
|
+
end
|
278
|
+
end
|
279
|
+
```
|
417
280
|
|
418
281
|
Alternatives
|
419
282
|
-------------------
|
420
283
|
If you're working in Rails, Plataformatec's excellent [HasScope][has_scope] has
|
421
|
-
been solving this problem since 2009. I prefer keeping my
|
422
|
-
place, though, instead of spreading it across my controllers and models.
|
284
|
+
been solving this problem since 2009. I prefer keeping my request logic all in
|
285
|
+
one place, though, instead of spreading it across my controllers and models.
|
423
286
|
|
424
|
-
[Periscope][periscope], by
|
425
|
-
|
287
|
+
[Periscope][periscope], by Steve Richert, seems like another solid Rails option.
|
288
|
+
It is Rails-only, but it supports more than just ActiveRecord.
|
426
289
|
|
427
290
|
For Sinatra, Simon Courtois has a [Sinatra port of has_scope][sin_has_scope].
|
428
291
|
It depends on ActiveRecord.
|
@@ -434,7 +297,7 @@ Please open [an issue](https://github.com/chrisfrank/rack-reducer/issues) on
|
|
434
297
|
Github.
|
435
298
|
|
436
299
|
### Pull Requests
|
437
|
-
|
300
|
+
PRs are welcome, and I'll do my best to review them promptly.
|
438
301
|
|
439
302
|
License
|
440
303
|
----------
|
@@ -454,9 +317,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
|
454
317
|
[sin_has_scope]: https://github.com/simonc/sinatra-has_scope
|
455
318
|
[sinatra]: https://github.com/sinatra/sinatra
|
456
319
|
[sequel]: https://github.com/jeremyevans/sequel
|
457
|
-
[roda]: https://github.com/jeremyevans/roda
|
458
320
|
[reduce]: http://ruby-doc.org/core-2.5.0/Enumerable.html#method-i-reduce
|
459
321
|
[keywords]: https://robots.thoughtbot.com/ruby-2-keyword-arguments
|
460
|
-
[query_obj]: https://robots.thoughtbot.com/using-yieldself-for-composable-activerecord-relations
|
461
322
|
[periscope]: https://github.com/laserlemon/periscope
|
462
|
-
[hanami]: http://hanamirb.org
|
@@ -1,21 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/request'
|
2
4
|
require_relative 'reduction'
|
3
5
|
|
4
6
|
module Rack
|
5
7
|
module Reducer
|
6
8
|
# Mount Rack::Reducer as middleware
|
9
|
+
# @example A microservice that filters artists
|
10
|
+
# ArtistService = Rack::Builder.new do
|
11
|
+
# use(
|
12
|
+
# Rack::Reducer::Middleware,
|
13
|
+
# dataset: Artist.all,
|
14
|
+
# filters: [
|
15
|
+
# lambda { |name:| where(name: name) },
|
16
|
+
# lambda { |genre:| where(genre: genre) },
|
17
|
+
# ]
|
18
|
+
# )
|
19
|
+
#
|
20
|
+
# run ->(env) { [200, {}, [env['rack.reduction'].to_json]] }
|
21
|
+
# end
|
7
22
|
class Middleware
|
8
23
|
def initialize(app, options = {})
|
9
24
|
@app = app
|
10
25
|
@key = options[:key] || 'rack.reduction'
|
11
|
-
@
|
26
|
+
@reducer = Rack::Reducer.create(options[:dataset], *options[:filters])
|
12
27
|
end
|
13
28
|
|
14
29
|
# Call the next app in the middleware stack, with env[key] set
|
15
30
|
# to the ouput of a reduction
|
16
31
|
def call(env)
|
17
32
|
params = Rack::Request.new(env).params
|
18
|
-
reduction =
|
33
|
+
reduction = @reducer.apply(params)
|
19
34
|
@app.call env.merge(@key => reduction)
|
20
35
|
end
|
21
36
|
end
|
@@ -1,5 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'refinements'
|
2
|
-
require_relative 'parser'
|
3
4
|
|
4
5
|
module Rack
|
5
6
|
module Reducer
|
@@ -8,26 +9,28 @@ module Rack
|
|
8
9
|
class Reduction
|
9
10
|
using Refinements # define Proc#required_argument_names, #satisfies?, etc
|
10
11
|
|
11
|
-
|
12
|
-
dataset
|
13
|
-
filters
|
14
|
-
params: nil
|
15
|
-
}.freeze
|
16
|
-
|
17
|
-
def initialize(options)
|
18
|
-
@props = DEFAULTS.merge(options)
|
19
|
-
@params = Parser.call(@props[:params])
|
12
|
+
def initialize(dataset, *filters)
|
13
|
+
@dataset = dataset
|
14
|
+
@filters = filters
|
20
15
|
end
|
21
16
|
|
22
|
-
|
23
|
-
|
24
|
-
|
17
|
+
# Run +@filters+ against the params argument
|
18
|
+
# @param [Hash, ActionController::Parameters, nil] params
|
19
|
+
# a Rack-compatible params hash
|
20
|
+
# @return +@dataset+ with the matching filters applied
|
21
|
+
def apply(params)
|
22
|
+
return @dataset if !params || params.empty?
|
25
23
|
|
26
|
-
|
24
|
+
symbolized_params = params.to_unsafe_h.symbolize_keys
|
25
|
+
@filters.reduce(@dataset) do |data, filter|
|
26
|
+
next data unless filter.satisfies?(symbolized_params)
|
27
|
+
|
28
|
+
data.instance_exec(
|
29
|
+
**symbolized_params.slice(*filter.all_argument_names),
|
30
|
+
&filter
|
31
|
+
)
|
27
32
|
end
|
28
33
|
end
|
29
34
|
end
|
30
|
-
|
31
|
-
private_constant :Reduction
|
32
35
|
end
|
33
36
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
module Reducer
|
3
5
|
# refine Proc and hash in this scope only
|
@@ -17,7 +19,7 @@ module Rack
|
|
17
19
|
end
|
18
20
|
end
|
19
21
|
|
20
|
-
# backport Hash#slice for
|
22
|
+
# backport Hash#slice for Ruby < 2.4
|
21
23
|
unless {}.respond_to?(:slice)
|
22
24
|
refine Hash do
|
23
25
|
def slice(*keys)
|
@@ -25,6 +27,16 @@ module Rack
|
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|
30
|
+
|
31
|
+
refine Hash do
|
32
|
+
def symbolize_keys
|
33
|
+
each_with_object({}) do |(key, val), hash|
|
34
|
+
hash[key.to_sym] = val.is_a?(Hash) ? symbolize(val) : val
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
alias_method :to_unsafe_h, :to_h
|
39
|
+
end
|
28
40
|
end
|
29
41
|
|
30
42
|
private_constant :Refinements
|