rack-reducer 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +170 -104
- data/lib/rack/reducer/middleware.rb +24 -0
- data/lib/rack/reducer/reduction.rb +5 -17
- data/lib/rack/reducer.rb +7 -7
- data/spec/_hanami_example/apps/web/application.rb +326 -0
- data/spec/_hanami_example/apps/web/config/routes.rb +4 -0
- data/spec/_hanami_example/apps/web/controllers/artists/index.rb +12 -0
- data/spec/_hanami_example/apps/web/views/application_layout.rb +7 -0
- data/spec/_hanami_example/config/boot.rb +2 -0
- data/spec/_hanami_example/config/environment.rb +29 -0
- data/spec/_hanami_example/lib/hanami_example/entities/artist.rb +2 -0
- data/spec/_hanami_example/lib/hanami_example/repositories/artist_repository.rb +9 -0
- data/spec/_hanami_example/lib/hanami_example.rb +5 -0
- data/spec/{rails_example → _rails_example}/app/channels/application_cable/channel.rb +0 -0
- data/spec/{rails_example → _rails_example}/app/channels/application_cable/connection.rb +0 -0
- data/spec/{rails_example → _rails_example}/app/controllers/application_controller.rb +0 -0
- data/spec/_rails_example/app/controllers/artists_controller.rb +8 -0
- data/spec/{rails_example → _rails_example}/app/jobs/application_job.rb +0 -0
- data/spec/{rails_example → _rails_example}/app/mailers/application_mailer.rb +0 -0
- data/spec/{rails_example → _rails_example}/app/models/application_record.rb +0 -0
- data/spec/_rails_example/app/models/rails_example/artist.rb +20 -0
- data/spec/{rails_example → _rails_example}/config/application.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/boot.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/environment.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/environments/development.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/environments/production.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/environments/test.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/initializers/application_controller_renderer.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/initializers/backtrace_silencers.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/initializers/cors.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/initializers/filter_parameter_logging.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/initializers/inflections.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/initializers/mime_types.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/initializers/wrap_parameters.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/puma.rb +0 -0
- data/spec/{rails_example → _rails_example}/config/routes.rb +0 -0
- data/spec/{rails_example → _rails_example}/db/seeds.rb +0 -0
- data/spec/behavior.rb +2 -2
- data/spec/hanami_spec.rb +6 -0
- data/spec/middleware_spec.rb +16 -8
- data/spec/rails_spec.rb +1 -2
- data/spec/roda_spec.rb +15 -0
- data/spec/sinatra_functional_spec.rb +3 -9
- data/spec/sinatra_mixin_spec.rb +0 -2
- data/spec/spec_helper.rb +12 -1
- metadata +121 -62
- data/spec/fixtures.rb +0 -19
- data/spec/rails_example/app/controllers/artists_controller.rb +0 -53
- data/spec/rails_example/app/models/artist.rb +0 -18
- data/spec/rails_example/config/initializers/schema.rb +0 -13
- data/spec/rails_example/test/test_helper.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1877d2e46030a53f725c98db2652b4523d292c8f2d069633e7ea41a03effc211
|
4
|
+
data.tar.gz: 4681a5249f40b6669c4750ea8cfd7788c4f3cc01440d334aebe48b3b2cdf05c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b02f1193154c6ae870fb0de14d0876e50adae875189e698b10532569601495465afdfb3a218ab6b04ac4831fdab1ff5d334e17a3bdf806c87f517962885ef963
|
7
|
+
data.tar.gz: ca39507186ea87517fa1148f29b30338b8326a79dc37f945823caecfbd06bf27e7bdd1a306fe05ce2c1e22dbbbe597c6dfa5928bc96ae2f68d50dce13f4076dd
|
data/README.md
CHANGED
@@ -1,12 +1,17 @@
|
|
1
1
|
Rack::Reducer
|
2
2
|
==========================================
|
3
|
-
|
4
|
-
|
3
|
+
[![Build Status](https://travis-ci.org/chrisfrank/rack-reducer.svg?branch=master)](https://travis-ci.org/chrisfrank/rack-reducer)
|
4
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/675e7a654c7e11c24b9f/maintainability)](https://codeclimate.com/github/chrisfrank/rack-reducer/maintainability)
|
5
5
|
|
6
|
-
|
7
|
-
as
|
8
|
-
|
9
|
-
|
6
|
+
Dynamically filter, sort, and paginate data via URL params, with controller
|
7
|
+
logic as simple 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.
|
10
15
|
|
11
16
|
Install
|
12
17
|
------------------------------------------
|
@@ -18,8 +23,8 @@ gem 'rack-reducer', require: 'rack/reducer'
|
|
18
23
|
|
19
24
|
Use
|
20
25
|
------------------------------------------
|
21
|
-
Rack::Reducer maps incoming URL params to an array of filter functions
|
22
|
-
define, chains the applicable filters, and returns filtered data.
|
26
|
+
Rack::Reducer safely maps incoming URL params to an array of filter functions
|
27
|
+
you define, chains the applicable filters, and returns filtered data.
|
23
28
|
|
24
29
|
Suppose you have some incoming requests like these...
|
25
30
|
|
@@ -30,7 +35,7 @@ Suppose you have some incoming requests like these...
|
|
30
35
|
You want to filter your `artists` table by name and/or genre when those
|
31
36
|
params are present, or return all artists otherwise.
|
32
37
|
|
33
|
-
Even with just a few optional filters, running them conditonally via `if`
|
38
|
+
Even with just a few optional filters, running them conditonally via `if`
|
34
39
|
statements gets messy.
|
35
40
|
|
36
41
|
### A Mess
|
@@ -44,40 +49,39 @@ class ArtistsController < ApplicationController
|
|
44
49
|
@artists = @artists.where(genre: params[:genre]) if params[:genre]
|
45
50
|
@artists = @artists.order(params[:order].to_sym) if params[:order]
|
46
51
|
# ...
|
47
|
-
# ...
|
48
52
|
# pages later...
|
49
53
|
@artists.all.to_json
|
50
54
|
end
|
51
55
|
```
|
52
56
|
|
53
|
-
Rack::Reducer
|
54
|
-
|
55
|
-
calling it as a function.
|
57
|
+
Rack::Reducer helps you clean this mess up, in your choice of two styles: mixin
|
58
|
+
or functional.
|
56
59
|
|
57
|
-
### Cleaned up
|
60
|
+
### Cleaned up, mixin-style
|
58
61
|
Call `Model.reduce(params)` in your controllers...
|
59
62
|
|
60
63
|
```ruby
|
61
64
|
# app/controllers/artists_controller.rb
|
62
|
-
class ArtistsController <
|
65
|
+
class ArtistsController < ApplicationController
|
63
66
|
def index
|
64
67
|
@artists = Artist.reduce(params)
|
65
|
-
@artists
|
68
|
+
render json: @artists
|
66
69
|
end
|
67
70
|
end
|
68
71
|
```
|
69
72
|
|
70
73
|
... and `extend Rack::Reducer` in your models:
|
74
|
+
|
71
75
|
```ruby
|
72
76
|
# app/models/artist.rb
|
73
77
|
class Artist < ActiveRecord::Base
|
74
78
|
extend Rack::Reducer # makes `self.reduce` available at class level
|
75
79
|
|
76
|
-
#
|
80
|
+
# Configure by calling
|
77
81
|
# `reduces(some_initial_scope, filters: [an, array, of, lambdas])`
|
78
82
|
#
|
79
|
-
#
|
80
|
-
#
|
83
|
+
# Filters can use any methods your initial dataset understands.
|
84
|
+
# Here it's an ActiveRecord query, so filters use AR query methods.
|
81
85
|
reduces self.all, filters: [
|
82
86
|
->(name:) { where('lower(name) like ?', "%#{name.downcase}%") },
|
83
87
|
->(genre:) { where(genre: genre) },
|
@@ -86,29 +90,34 @@ class Artist < ActiveRecord::Base
|
|
86
90
|
end
|
87
91
|
```
|
88
92
|
|
89
|
-
### Cleaned up
|
90
|
-
|
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.
|
93
|
+
### Cleaned up, functional-style
|
94
|
+
Call Rack::Reducer as a function:
|
94
95
|
|
95
96
|
```ruby
|
96
97
|
# app/controllers/artists_controller.rb
|
97
98
|
class ArtistsController < ApplicationController
|
98
|
-
|
99
|
-
|
99
|
+
# this is an options hash that we'll pass to Rack::Reducer
|
100
|
+
QUERY = {
|
101
|
+
dataset: Artist.all,
|
102
|
+
filters: [
|
100
103
|
->(name:) { where('lower(name) like ?', "%#{name.downcase}%") },
|
101
104
|
->(genre:) { where(genre: genre) },
|
102
105
|
->(order:) { order(order.to_sym) },
|
103
|
-
]
|
104
|
-
|
106
|
+
]
|
107
|
+
}
|
108
|
+
|
109
|
+
def index
|
110
|
+
@artists = Rack::Reducer.call(params, QUERY)
|
111
|
+
render json: @artists
|
105
112
|
end
|
106
113
|
end
|
107
114
|
```
|
108
115
|
|
109
116
|
The mixin style requires less boilerplate, and is stylistically Railsier.
|
110
|
-
The functional style is more flexible
|
111
|
-
handle requests identically.
|
117
|
+
The functional style is more flexible, and keeps your filtering logic all in
|
118
|
+
one place. Both styles are supported, tested, and handle requests identically.
|
119
|
+
|
120
|
+
In the examples above:
|
112
121
|
|
113
122
|
```ruby
|
114
123
|
# GET /artists returns all artists, e.g.
|
@@ -120,13 +129,13 @@ handle requests identically. In the examples above:
|
|
120
129
|
{ "name": "SZA", "genre": "alt-soul" }
|
121
130
|
]
|
122
131
|
|
123
|
-
# GET /artists?name=blake returns e.g.
|
132
|
+
# GET /artists?name=blake returns artists named 'blake', e.g.
|
124
133
|
[
|
125
134
|
{ "name": "Blake Mills", "genre": "alternative" },
|
126
135
|
{ "name": "James Blake", "genre": "electronic" }
|
127
136
|
]
|
128
137
|
|
129
|
-
# GET /artists?name=blake&genre=electronic returns e.g.
|
138
|
+
# GET /artists?name=blake&genre=electronic returns e.g.
|
130
139
|
[{ "name": "James Blake", "genre": "electronic" }]
|
131
140
|
```
|
132
141
|
|
@@ -134,50 +143,56 @@ handle requests identically. In the examples above:
|
|
134
143
|
Framework-specific Examples
|
135
144
|
---------------------------
|
136
145
|
These examples apply Rack::Reducer in different frameworks, with a different
|
137
|
-
ORM each time. The pairings of ORMs and frameworks are
|
146
|
+
ORM each time. The pairings of ORMs and frameworks are arbitrary, just to
|
138
147
|
demonstrate a few possible stacks.
|
139
148
|
|
140
|
-
- [Sinatra](#sinatrasequel)
|
141
|
-
- [Rack Middleware](#rack-middlewarehash)
|
142
|
-
- [
|
149
|
+
- [Sinatra/Sequel](#sinatrasequel)
|
150
|
+
- [Rack Middleware/Ruby Hash](#rack-middlewarehash)
|
151
|
+
- [Hanami](#hanami)
|
152
|
+
- [Advanced use in Rails and other frameworks](#advanced-use-in-rails-and-other-frameworks)
|
143
153
|
|
144
154
|
### Sinatra/Sequel
|
145
155
|
This example uses [Sinatra][sinatra] to handle requests, and [Sequel][sequel]
|
146
156
|
as an ORM.
|
147
157
|
|
148
|
-
####
|
158
|
+
#### Functional-style
|
149
159
|
```ruby
|
150
|
-
#
|
151
|
-
class
|
152
|
-
|
153
|
-
|
154
|
-
|
160
|
+
# sinatra_functional_style.rb
|
161
|
+
class SinatraFunctionalApp < Sinatra::Base
|
162
|
+
DB = Sequel.connect ENV['DATABASE_URL']
|
163
|
+
|
164
|
+
# dataset is a Sequel::Dataset, so filters use Sequel query methods
|
165
|
+
QUERY = {
|
166
|
+
dataset: DB[:artists],
|
167
|
+
filters: [
|
155
168
|
->(genre:) { where(genre: genre) },
|
156
169
|
->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
|
157
170
|
->(order:) { order(order.to_sym) },
|
158
171
|
]
|
159
|
-
|
160
|
-
|
172
|
+
}
|
173
|
+
|
161
174
|
get '/artists' do
|
162
|
-
@artists =
|
175
|
+
@artists = Rack::Reducer.call(params, QUERY).to_a
|
163
176
|
@artists.all.to_json
|
164
177
|
end
|
165
178
|
end
|
166
179
|
```
|
167
180
|
|
168
|
-
####
|
181
|
+
#### Mixin-style
|
169
182
|
```ruby
|
170
|
-
#
|
171
|
-
class
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
# dataset is a Sequel::Dataset, so filters use Sequel query methods
|
176
|
-
@artists = Rack::Reducer.call(params, dataset: DB[:artists], filters: [
|
183
|
+
# sintra_mixin_style.rb
|
184
|
+
class SinatraMixinApp < Sinatra::Base
|
185
|
+
class Artist < Sequel::Model
|
186
|
+
extend Rack::Reducer
|
187
|
+
reduces self.dataset, filters: [
|
177
188
|
->(genre:) { where(genre: genre) },
|
178
189
|
->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
|
179
190
|
->(order:) { order(order.to_sym) },
|
180
|
-
]
|
191
|
+
]
|
192
|
+
end
|
193
|
+
|
194
|
+
get '/artists' do
|
195
|
+
@artists = Artist.reduce(params)
|
181
196
|
@artists.all.to_json
|
182
197
|
end
|
183
198
|
end
|
@@ -185,7 +200,7 @@ end
|
|
185
200
|
|
186
201
|
### Rack Middleware/Hash
|
187
202
|
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.
|
203
|
+
It doesn't use an ORM at all -- it just stores data in a ruby hash.
|
189
204
|
|
190
205
|
```ruby
|
191
206
|
# config.ru
|
@@ -225,7 +240,34 @@ use Rack::Reducer, key: 'myapp.custom_key', dataset: ARTISTS, filters: [
|
|
225
240
|
]
|
226
241
|
```
|
227
242
|
|
228
|
-
###
|
243
|
+
### Hanami
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
# apps/web/controllers/artists/index.rb
|
247
|
+
module Web::Controllers::Artists
|
248
|
+
class Index
|
249
|
+
include Web::Action
|
250
|
+
|
251
|
+
def call(params)
|
252
|
+
@artists = ArtistRepository.new.reduce(params)
|
253
|
+
self.body = @artists.all.to_json
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# lib/app_name/repositories/artist_repository.rb
|
259
|
+
class ArtistRepository < Hanami::Repository
|
260
|
+
def reduce(params)
|
261
|
+
Rack::Reducer.call(params, dataset: artists.dataset, filters: [
|
262
|
+
->(genre:) { where(genre: genre) },
|
263
|
+
->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
|
264
|
+
->(order:) { order(order.to_sym) },
|
265
|
+
])
|
266
|
+
end
|
267
|
+
end
|
268
|
+
```
|
269
|
+
|
270
|
+
### Advanced use in Rails
|
229
271
|
The examples in the [introduction](#use) cover basic Rails use. The examples
|
230
272
|
below cover more advanced use.
|
231
273
|
|
@@ -233,47 +275,34 @@ If you're comfortable in a non-Rails stack, you can apply these advanced
|
|
233
275
|
techniques there too. I wholeheartedly endorse [Roda][roda], and use
|
234
276
|
Rack::Reducer with Roda/Sequel in production.
|
235
277
|
|
236
|
-
####
|
237
|
-
|
238
|
-
|
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
|
-
]
|
278
|
+
#### Default filters
|
279
|
+
Most of the time it makes sense to use *required* keyword arguments for each
|
280
|
+
filter, and skip running the filter altogether when the keyword argments aren't
|
281
|
+
present.
|
251
282
|
|
252
|
-
|
253
|
-
|
254
|
-
}
|
283
|
+
But you may want to run a filter always, with a sensible default when the params
|
284
|
+
don't specify a value. Ordering results is a common case.
|
255
285
|
|
256
|
-
|
257
|
-
|
258
|
-
scope :signed, lambda { where(signed: true) }
|
259
|
-
end
|
286
|
+
The code below will order by `params[:order]` when it exists, and by name
|
287
|
+
otherwise.
|
260
288
|
|
289
|
+
```ruby
|
261
290
|
# app/controllers/artists_controller.rb
|
262
291
|
class ArtistsController < ApplicationController
|
263
292
|
def index
|
264
|
-
|
265
|
-
|
266
|
-
|
293
|
+
@artists = Rack::Reducer.call(params, dataset: Artist.all, filters: [
|
294
|
+
->(genre:) { where(genre: genre) },
|
295
|
+
->(order: 'name') { order(order.to_sym) }
|
296
|
+
])
|
267
297
|
@artists.to_json
|
268
298
|
end
|
269
299
|
end
|
270
300
|
```
|
271
301
|
|
272
|
-
|
273
302
|
#### Dynamically setting Reducer's initial dataset
|
274
|
-
Rack::Reducer's mixin style only lets you target one dataset for
|
275
|
-
If you need different initial
|
276
|
-
|
303
|
+
Rack::Reducer's mixin style only lets you target one initial dataset for
|
304
|
+
reduction. If you need different initial datasets in different contexts, use
|
305
|
+
the functional style:
|
277
306
|
|
278
307
|
```ruby
|
279
308
|
# app/controllers/artists_controller.rb
|
@@ -290,31 +319,42 @@ class ArtistsController < ApplicationController
|
|
290
319
|
end
|
291
320
|
```
|
292
321
|
|
293
|
-
####
|
294
|
-
|
295
|
-
|
296
|
-
present.
|
322
|
+
#### Chaining reduce with other ActiveRecord query methods
|
323
|
+
In the mixin-style, you can chain `Model.reduce` with other ActiveRecord
|
324
|
+
queries, as long as `reduce` is the first call in the chain:
|
297
325
|
|
298
|
-
|
299
|
-
|
326
|
+
```ruby
|
327
|
+
# app/models/artist.rb
|
328
|
+
class Artist < ApplicationRecord
|
329
|
+
extend Rack::Reducer
|
330
|
+
reduces self.all, filters: [
|
331
|
+
# filters get instance_exec'd against the initial dataset,
|
332
|
+
# in this case `self.all`, so filters can use query methods, scopes, etc
|
333
|
+
->(name:) { by_name(name) },
|
334
|
+
->(genre:) { where(genre: genre) },
|
335
|
+
->(order:) { order(order.to_sym) }
|
336
|
+
]
|
300
337
|
|
301
|
-
|
302
|
-
|
338
|
+
scope :by_name, lambda { |name|
|
339
|
+
where('lower(name) like ?', "%#{name.downcase}%")
|
340
|
+
}
|
341
|
+
|
342
|
+
# here's a scope we're not using in our Reducer filters,
|
343
|
+
# but will use in our controller
|
344
|
+
scope :signed, lambda { where(signed: true) }
|
345
|
+
end
|
303
346
|
|
304
|
-
```ruby
|
305
347
|
# app/controllers/artists_controller.rb
|
306
348
|
class ArtistsController < ApplicationController
|
307
349
|
def index
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
])
|
350
|
+
# you can chain reduce with other ActiveRecord queries,
|
351
|
+
# as long as reduce is first in the chain
|
352
|
+
@artists = Artist.reduce(params).signed
|
312
353
|
@artists.to_json
|
313
354
|
end
|
314
355
|
end
|
315
356
|
```
|
316
357
|
|
317
|
-
|
318
358
|
How Rack::Reducer Works
|
319
359
|
--------------------------------------
|
320
360
|
Rack::Reducer takes a dataset, a params hash, and an array of lambda functions.
|
@@ -338,11 +378,11 @@ Mongoid, etc -- it just `instance_exec`s your own code against your own dataset.
|
|
338
378
|
Rack::Reducer claims to "safely" map URL params to filters, but it accepts an
|
339
379
|
unfiltered params hash. What gives?
|
340
380
|
|
341
|
-
By using keyword arguments in your filter lambdas, you are
|
381
|
+
By using keyword arguments in your filter lambdas, you are explicitly naming
|
342
382
|
the params you'll accept into your filters. Params that aren't keywords never
|
343
383
|
get evaluated.
|
344
384
|
|
345
|
-
For
|
385
|
+
For extra safety, you can typecast the params in your filters. Most ORMs
|
346
386
|
handle this for you, but as an example:
|
347
387
|
|
348
388
|
```ruby
|
@@ -358,25 +398,51 @@ FILTERS = [
|
|
358
398
|
```
|
359
399
|
|
360
400
|
### 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
|
401
|
+
According to `spec/benchmarks.rb`, Rack::Reducer executes about 90% as quickly
|
402
|
+
as a set of hard-coded conditional filters. It is unlikely to be a
|
363
403
|
bottleneck in your application.
|
364
404
|
|
405
|
+
Alternatives
|
406
|
+
-------------------
|
407
|
+
If you're working in Rails, Platformatec's excellent [HasScope][has_scope] has
|
408
|
+
been solving this problem since 2013. I prefer keeping my query logic all in one
|
409
|
+
place, though, instead of spreading it across my controllers and models.
|
365
410
|
|
411
|
+
[Periscope][periscope], by laserlemon, seems like another good Rails option, and
|
412
|
+
though it's Rails only, it supports more than just ActiveRecord.
|
413
|
+
|
414
|
+
For Sinatra, Simon Courtois has a [Sinatra port of has_scope][sin_has_scope].
|
415
|
+
It depends on ActiveRecord.
|
366
416
|
|
367
417
|
Contributing
|
368
418
|
-------------------------------
|
369
419
|
### Bugs
|
370
|
-
|
420
|
+
Please open [an issue](https://github.com/chrisfrank/rack-reducer/issues) on
|
421
|
+
Github.
|
371
422
|
|
372
423
|
### Pull Requests
|
373
424
|
Please include tests, following the style of the specs in `spec/*_spec.rb`.
|
374
425
|
|
426
|
+
License
|
427
|
+
----------
|
428
|
+
### MIT
|
429
|
+
|
430
|
+
Copyright 2018 Chris Frank
|
431
|
+
|
432
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
433
|
+
|
434
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
435
|
+
|
436
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
437
|
+
|
375
438
|
|
376
439
|
|
377
440
|
[has_scope]: https://github.com/plataformatec/has_scope
|
441
|
+
[sin_has_scope]: https://github.com/simonc/sinatra-has_scope
|
378
442
|
[sinatra]: https://github.com/sinatra/sinatra
|
379
443
|
[sequel]: https://github.com/jeremyevans/sequel
|
380
444
|
[roda]: https://github.com/jeremyevans/roda
|
381
445
|
[reduce]: http://ruby-doc.org/core-2.5.0/Enumerable.html#method-i-reduce
|
382
446
|
[keywords]: https://robots.thoughtbot.com/ruby-2-keyword-arguments
|
447
|
+
[query_obj]: https://robots.thoughtbot.com/using-yieldself-for-composable-activerecord-relations
|
448
|
+
[periscope]: https://github.com/laserlemon/periscope
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'reduction'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
module Reducer
|
7
|
+
# Mount Rack::Reducer as middleware
|
8
|
+
class Middleware
|
9
|
+
def initialize(app, options = {})
|
10
|
+
@app = app
|
11
|
+
@key = options[:key] || 'rack.reduction'
|
12
|
+
@props = options
|
13
|
+
end
|
14
|
+
|
15
|
+
# Call the next app in the middleware stack, with env[key] set
|
16
|
+
# to the ouput of a reduction
|
17
|
+
def call(env)
|
18
|
+
params = Rack::Request.new(env).params
|
19
|
+
reduction = Reduction.new(@props.merge(params: params)).reduce
|
20
|
+
@app.call env.merge(@key => reduction)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -13,20 +13,12 @@ module Rack
|
|
13
13
|
DEFAULTS = {
|
14
14
|
dataset: [],
|
15
15
|
filters: [],
|
16
|
-
key: 'rack.reduction',
|
17
16
|
params: nil
|
18
17
|
}.freeze
|
19
18
|
|
20
|
-
def initialize(
|
21
|
-
@
|
22
|
-
@
|
23
|
-
end
|
24
|
-
|
25
|
-
# when mounted as middleware, set env[@props[:key]] to the output
|
26
|
-
# of self.reduce, then call the next app in the middleware stack
|
27
|
-
def call(env)
|
28
|
-
@params = Rack::Request.new(env).params.symbolize_keys
|
29
|
-
@app.call env.merge(@props[:key] => reduce)
|
19
|
+
def initialize(options)
|
20
|
+
@props = DEFAULTS.merge(options)
|
21
|
+
@params = Parser.call(@props[:params]).symbolize_keys
|
30
22
|
end
|
31
23
|
|
32
24
|
def reduce
|
@@ -35,14 +27,10 @@ module Rack
|
|
35
27
|
|
36
28
|
private
|
37
29
|
|
38
|
-
def params
|
39
|
-
@params ||= Parser.call(@props[:params]).symbolize_keys
|
40
|
-
end
|
41
|
-
|
42
30
|
def apply_filter(data, fn)
|
43
31
|
requirements = fn.required_argument_names.to_set
|
44
|
-
return data unless params.satisfies?(requirements)
|
45
|
-
data.instance_exec(params.slice(*fn.all_argument_names), &fn)
|
32
|
+
return data unless @params.satisfies?(requirements)
|
33
|
+
data.instance_exec(@params.slice(*fn.all_argument_names), &fn)
|
46
34
|
end
|
47
35
|
end
|
48
36
|
end
|
data/lib/rack/reducer.rb
CHANGED
@@ -2,25 +2,26 @@
|
|
2
2
|
|
3
3
|
require 'rack/request'
|
4
4
|
require_relative 'reducer/reduction'
|
5
|
+
require_relative 'reducer/middleware'
|
5
6
|
|
6
7
|
module Rack
|
7
|
-
#
|
8
|
+
# Use request params to apply filters to a dataset
|
8
9
|
module Reducer
|
9
|
-
#
|
10
|
+
# Call Rack::Reducer as a function
|
10
11
|
def self.call(params, dataset:, filters:)
|
11
12
|
Reduction.new(
|
12
|
-
nil, # first arg to Reduction is `app`, which is for middleware only
|
13
13
|
params: params,
|
14
14
|
filters: filters,
|
15
|
-
dataset: dataset
|
15
|
+
dataset: dataset
|
16
16
|
).reduce
|
17
17
|
end
|
18
18
|
|
19
|
+
# Mount Rack::Reducer as middleware
|
19
20
|
def self.new(app, options = {})
|
20
|
-
|
21
|
+
Middleware.new(app, options)
|
21
22
|
end
|
22
23
|
|
23
|
-
#
|
24
|
+
# Extend Rack::Reducer to get `reduce` and `reduces` as class-methods
|
24
25
|
#
|
25
26
|
# class Artist < SomeORM::Model
|
26
27
|
# extend Rack::Reducer
|
@@ -31,7 +32,6 @@ module Rack
|
|
31
32
|
# end
|
32
33
|
def reduce(params)
|
33
34
|
Reduction.new(
|
34
|
-
nil,
|
35
35
|
params: params,
|
36
36
|
filters: @rack_reducer_filters,
|
37
37
|
dataset: @rack_reducer_dataset
|