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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +170 -104
  3. data/lib/rack/reducer/middleware.rb +24 -0
  4. data/lib/rack/reducer/reduction.rb +5 -17
  5. data/lib/rack/reducer.rb +7 -7
  6. data/spec/_hanami_example/apps/web/application.rb +326 -0
  7. data/spec/_hanami_example/apps/web/config/routes.rb +4 -0
  8. data/spec/_hanami_example/apps/web/controllers/artists/index.rb +12 -0
  9. data/spec/_hanami_example/apps/web/views/application_layout.rb +7 -0
  10. data/spec/_hanami_example/config/boot.rb +2 -0
  11. data/spec/_hanami_example/config/environment.rb +29 -0
  12. data/spec/_hanami_example/lib/hanami_example/entities/artist.rb +2 -0
  13. data/spec/_hanami_example/lib/hanami_example/repositories/artist_repository.rb +9 -0
  14. data/spec/_hanami_example/lib/hanami_example.rb +5 -0
  15. data/spec/{rails_example → _rails_example}/app/channels/application_cable/channel.rb +0 -0
  16. data/spec/{rails_example → _rails_example}/app/channels/application_cable/connection.rb +0 -0
  17. data/spec/{rails_example → _rails_example}/app/controllers/application_controller.rb +0 -0
  18. data/spec/_rails_example/app/controllers/artists_controller.rb +8 -0
  19. data/spec/{rails_example → _rails_example}/app/jobs/application_job.rb +0 -0
  20. data/spec/{rails_example → _rails_example}/app/mailers/application_mailer.rb +0 -0
  21. data/spec/{rails_example → _rails_example}/app/models/application_record.rb +0 -0
  22. data/spec/_rails_example/app/models/rails_example/artist.rb +20 -0
  23. data/spec/{rails_example → _rails_example}/config/application.rb +0 -0
  24. data/spec/{rails_example → _rails_example}/config/boot.rb +0 -0
  25. data/spec/{rails_example → _rails_example}/config/environment.rb +0 -0
  26. data/spec/{rails_example → _rails_example}/config/environments/development.rb +0 -0
  27. data/spec/{rails_example → _rails_example}/config/environments/production.rb +0 -0
  28. data/spec/{rails_example → _rails_example}/config/environments/test.rb +0 -0
  29. data/spec/{rails_example → _rails_example}/config/initializers/application_controller_renderer.rb +0 -0
  30. data/spec/{rails_example → _rails_example}/config/initializers/backtrace_silencers.rb +0 -0
  31. data/spec/{rails_example → _rails_example}/config/initializers/cors.rb +0 -0
  32. data/spec/{rails_example → _rails_example}/config/initializers/filter_parameter_logging.rb +0 -0
  33. data/spec/{rails_example → _rails_example}/config/initializers/inflections.rb +0 -0
  34. data/spec/{rails_example → _rails_example}/config/initializers/mime_types.rb +0 -0
  35. data/spec/{rails_example → _rails_example}/config/initializers/wrap_parameters.rb +0 -0
  36. data/spec/{rails_example → _rails_example}/config/puma.rb +0 -0
  37. data/spec/{rails_example → _rails_example}/config/routes.rb +0 -0
  38. data/spec/{rails_example → _rails_example}/db/seeds.rb +0 -0
  39. data/spec/behavior.rb +2 -2
  40. data/spec/hanami_spec.rb +6 -0
  41. data/spec/middleware_spec.rb +16 -8
  42. data/spec/rails_spec.rb +1 -2
  43. data/spec/roda_spec.rb +15 -0
  44. data/spec/sinatra_functional_spec.rb +3 -9
  45. data/spec/sinatra_mixin_spec.rb +0 -2
  46. data/spec/spec_helper.rb +12 -1
  47. metadata +121 -62
  48. data/spec/fixtures.rb +0 -19
  49. data/spec/rails_example/app/controllers/artists_controller.rb +0 -53
  50. data/spec/rails_example/app/models/artist.rb +0 -18
  51. data/spec/rails_example/config/initializers/schema.rb +0 -13
  52. 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: 1fd376a5091b8dac2c04e97b36e3e8880e7cfaecf01e1ef25d69580b30ba0bc4
4
- data.tar.gz: 7511b21435b309a104381dc2a5a3ccaad4b3b916d937de986c7087b7632ebe95
3
+ metadata.gz: 1877d2e46030a53f725c98db2652b4523d292c8f2d069633e7ea41a03effc211
4
+ data.tar.gz: 4681a5249f40b6669c4750ea8cfd7788c4f3cc01440d334aebe48b3b2cdf05c3
5
5
  SHA512:
6
- metadata.gz: 482102225a82151f8bbb0036ad4234f9caa741d112477f367e36c63e5c86e688defff43c130f70a0e88b1d006673e42d095228eeb376a86acd0d327c441a14e1
7
- data.tar.gz: af0473c9840adfea73479dffe3def6c96ca46fa9b1452bbefd9c680b1231f767ff9179dcedfa30a501b10b5726f77e677e500ff666db4ce73b70b128049e788f
6
+ metadata.gz: b02f1193154c6ae870fb0de14d0876e50adae875189e698b10532569601495465afdfb3a218ab6b04ac4831fdab1ff5d334e17a3bdf806c87f517962885ef963
7
+ data.tar.gz: ca39507186ea87517fa1148f29b30338b8326a79dc37f945823caecfbd06bf27e7bdd1a306fe05ce2c1e22dbbbe597c6dfa5928bc96ae2f68d50dce13f4076dd
data/README.md CHANGED
@@ -1,12 +1,17 @@
1
1
  Rack::Reducer
2
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.
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
- 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.
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 you
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 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.
57
+ Rack::Reducer helps you clean this mess up, in your choice of two styles: mixin
58
+ or functional.
56
59
 
57
- ### Cleaned up by extending Rack::Reducer
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 < ApplicatonController
65
+ class ArtistsController < ApplicationController
63
66
  def index
64
67
  @artists = Artist.reduce(params)
65
- @artists.all.to_json
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
- # configure by calling
80
+ # Configure by calling
77
81
  # `reduces(some_initial_scope, filters: [an, array, of, lambdas])`
78
82
  #
79
- # filters can use any methods your initial dataset understands.
80
- # here it's an ActiveRecord query, so filters use AR query methods
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 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.
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
- def index
99
- @artists = Rack::Reducer.call(params, dataset: Artist.all, filters: [
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
- @artists.all.to_json
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. Both styles are supported, tested, and
111
- handle requests identically. In the examples above:
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 abitrary, just to
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
- - [Rails](#railsadvanced)
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
- #### Mixin-style
158
+ #### Functional-style
149
159
  ```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: [
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
- end
160
-
172
+ }
173
+
161
174
  get '/artists' do
162
- @artists = Artist.reduce(params)
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
- #### Functional style
181
+ #### Mixin-style
169
182
  ```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: [
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
- ### Rails/Advanced
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
- #### 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
- ]
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
- scope :by_name, lambda { |name|
253
- where('lower(name) like ?', "%#{name.downcase}%")
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
- # 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
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
- # you can chain reduce with other ActiveRecord queries,
265
- # as long as reduce is first in the chain
266
- @artists = Artist.reduce(params).signed
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 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:
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
- #### 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.
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
- 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.
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
- The code below will order by `params[:order]` when it exists, and by name
302
- otherwise.
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
- @artists = Rack::Reducer.call(params, dataset: Artist.all, filters: [
309
- ->(genre:) { where(genre: genre) },
310
- ->(order: 'name') { order(order.to_sym) }
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 *explicitly* naming
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 even more security, you can typecast the params in your filters. Most ORMs
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 extremly unlikely to be a
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
- Open [an issue](https://github.com/chrisfrank/rack-reducer/issues) on Github.
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(app, props)
21
- @app = app
22
- @props = DEFAULTS.merge(props)
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
- # use request params to apply filters to a dataset
8
+ # Use request params to apply filters to a dataset
8
9
  module Reducer
9
- # call Rack::Reducer as a function, instead of mounting it as middleware
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
- Reduction.new(app, options)
21
+ Middleware.new(app, options)
21
22
  end
22
23
 
23
- # extend Rack::Reducer to get `reduce` and `reduces` as class-methods
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