rack-reducer 1.1.2 → 2.0.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 +14 -8
- data/lib/rack/reducer.rb +53 -58
- data/lib/rack/reducer/middleware.rb +3 -4
- data/lib/rack/reducer/refinements.rb +8 -2
- data/lib/rack/reducer/version.rb +2 -2
- data/spec/fixtures.rb +1 -4
- data/spec/middleware_spec.rb +2 -18
- data/spec/reducer_spec.rb +26 -26
- metadata +2 -4
- data/lib/rack/reducer/reduction.rb +0 -46
- data/lib/rack/reducer/warnings.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89560b1f9fb58a56452e6e9cbba2a9dd5e0159b938c804628750549da310c543
|
4
|
+
data.tar.gz: f7e9667193f0e4e6e5b413ceec8506919f784604d5be97746239688f3074ab8b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa641c8d24f2bd6a52f523e447c43d1714c92db33175750b4728512bcdca9f5668a018a4be1b5a504fb3ad0188ace6e5e3911503c6a06d48cbf3d9a64aecf489
|
7
|
+
data.tar.gz: d60cbdae9d7d225024680262be9be5b8b92a923ad224452dfd7e383410e75d5ee717ac3842b9f0ed95ebb8b3789ef36bc3880794b62577f1162a6d67816df77b
|
data/README.md
CHANGED
@@ -2,6 +2,7 @@ Rack::Reducer
|
|
2
2
|
==========================================
|
3
3
|
[](https://travis-ci.org/chrisfrank/rack-reducer)
|
4
4
|
[](https://codeclimate.com/github/chrisfrank/rack-reducer/maintainability)
|
5
|
+
[](https://rubygems.org/gems/rack-reducer)
|
5
6
|
|
6
7
|
Declaratively filter data via URL params, in any Rack app, with any ORM.
|
7
8
|
|
@@ -34,8 +35,8 @@ data. Here’s how you might use it in a Rails controller:
|
|
34
35
|
# app/controllers/artists_controller.rb
|
35
36
|
class ArtistsController < ApplicationController
|
36
37
|
|
37
|
-
# Step 1:
|
38
|
-
ArtistReducer = Rack::Reducer.
|
38
|
+
# Step 1: Instantiate a reducer
|
39
|
+
ArtistReducer = Rack::Reducer.new(
|
39
40
|
Artist.all,
|
40
41
|
->(name:) { where('lower(name) like ?', "%#{name.downcase}%") },
|
41
42
|
->(genre:) { where(genre: genre) },
|
@@ -91,7 +92,7 @@ class SinatraExample < Sinatra::Base
|
|
91
92
|
DB = Sequel.connect ENV['DATABASE_URL']
|
92
93
|
|
93
94
|
# dataset is a Sequel::Dataset, so filters use Sequel query methods
|
94
|
-
ArtistReducer = Rack::Reducer.
|
95
|
+
ArtistReducer = Rack::Reducer.new(
|
95
96
|
DB[:artists],
|
96
97
|
->(genre:) { where(genre: genre) },
|
97
98
|
->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
|
@@ -155,7 +156,7 @@ more sense to keep your reducers in your models instead.
|
|
155
156
|
class Artist < ApplicationRecord
|
156
157
|
# filters get instance_exec'd against the dataset you provide -- in this case
|
157
158
|
# it's `self.all` -- so filters can use query methods, scopes, etc
|
158
|
-
Reducer = Rack::Reducer.
|
159
|
+
Reducer = Rack::Reducer.new(
|
159
160
|
self.all,
|
160
161
|
->(name:) { by_name(name) },
|
161
162
|
->(genre:) { where(genre: genre) },
|
@@ -188,7 +189,7 @@ it exists, and by name otherwise.
|
|
188
189
|
|
189
190
|
```ruby
|
190
191
|
class ArtistsController < ApplicationController
|
191
|
-
ArtistReducer = Rack::Reducer.
|
192
|
+
ArtistReducer = Rack::Reducer.new(
|
192
193
|
Artist.all,
|
193
194
|
->(genre:) { where(genre: genre) },
|
194
195
|
->(sort: 'name') { order(sort.to_sym) }
|
@@ -203,8 +204,8 @@ end
|
|
203
204
|
|
204
205
|
Calling Rack::Reducer as a function
|
205
206
|
-------------------------------------------
|
206
|
-
For a slight performance penalty (~5%), you can skip
|
207
|
-
`::
|
207
|
+
For a slight performance penalty (~5%), you can skip instantiating a reducer via
|
208
|
+
`::new` and just call Rack::Reducer as a function. This can be useful when
|
208
209
|
prototyping, mostly because you don't need to think about naming anything.
|
209
210
|
|
210
211
|
```ruby
|
@@ -271,7 +272,7 @@ instead if you want to handle parameterless requests at top speed.
|
|
271
272
|
```ruby
|
272
273
|
# app/controllers/artists_controller.rb
|
273
274
|
class ArtistController < ApplicationController
|
274
|
-
# ArtistReducer = Rack::Reducer.
|
275
|
+
# ArtistReducer = Rack::Reducer.new(...etc etc)
|
275
276
|
|
276
277
|
def index
|
277
278
|
@artists = ArtistReducer.apply(request.query_parameters)
|
@@ -292,6 +293,11 @@ It is Rails-only, but it supports more than just ActiveRecord.
|
|
292
293
|
For Sinatra, Simon Courtois has a [Sinatra port of has_scope][sin_has_scope].
|
293
294
|
It depends on ActiveRecord.
|
294
295
|
|
296
|
+
Contributors
|
297
|
+
---------------
|
298
|
+
Thank you @danielpuglisi, @nicolasleger, @jeremyshearer, and @shanecav84 for
|
299
|
+
helping improve Rack::Reducer!
|
300
|
+
|
295
301
|
Contributing
|
296
302
|
-------------------------------
|
297
303
|
### Bugs
|
data/lib/rack/reducer.rb
CHANGED
@@ -1,21 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'reducer/
|
3
|
+
require_relative 'reducer/refinements'
|
4
4
|
require_relative 'reducer/middleware'
|
5
|
-
require_relative 'reducer/warnings'
|
6
5
|
|
7
6
|
module Rack
|
8
|
-
# Declaratively filter data via URL params, in any Rack app.
|
9
|
-
|
10
|
-
|
7
|
+
# Declaratively filter data via URL params, in any Rack app, with any ORM.
|
8
|
+
class Reducer
|
9
|
+
using Refinements
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# make ::create an alias of ::new, for compatibility with v1
|
13
|
+
alias create new
|
14
|
+
|
15
|
+
# Call Rack::Reducer as a function instead of creating a named reducer
|
16
|
+
def call(params, dataset:, filters:)
|
17
|
+
new(dataset, *filters).apply(params)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Instantiate a Reducer that can filter `dataset` via `#apply`.
|
11
22
|
# @param [Object] dataset an ActiveRecord::Relation, Sequel::Dataset,
|
12
23
|
# or other class with chainable methods
|
13
24
|
# @param [Array<Proc>] filters An array of lambdas whose keyword arguments
|
14
25
|
# name the URL params you will use as filters
|
15
|
-
# @return Rack::Reducer::Reduction
|
16
26
|
# @example Create a reducer and use it in a Sinatra app
|
17
27
|
# DB = Sequel.connect(ENV['DATABASE_URL'])
|
18
|
-
#
|
28
|
+
#
|
29
|
+
# MyReducer = Rack::Reducer.new(
|
19
30
|
# DB[:artists],
|
20
31
|
# lambda { |name:| where(name: name) },
|
21
32
|
# lambda { |genre:| where(genre: genre) },
|
@@ -25,63 +36,47 @@ module Rack
|
|
25
36
|
# @artists = MyReducer.apply(params)
|
26
37
|
# @artists.to_json
|
27
38
|
# end
|
28
|
-
def
|
29
|
-
|
39
|
+
def initialize(dataset, *filters)
|
40
|
+
@dataset = dataset
|
41
|
+
@filters = filters
|
42
|
+
@default_filters = filters.select do |filter|
|
43
|
+
filter.required_argument_names.empty?
|
44
|
+
end
|
30
45
|
end
|
31
46
|
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
Reduction.new(dataset, *filters).apply(params)
|
48
|
-
end
|
47
|
+
# Run `@filters` against `url_params`
|
48
|
+
# @param [Hash, ActionController::Parameters, nil] url_params
|
49
|
+
# a Rack-compatible params hash
|
50
|
+
# @return `@dataset` with the matching filters applied
|
51
|
+
def apply(url_params)
|
52
|
+
if url_params.empty?
|
53
|
+
# Return early with the unfiltered dataset if no default filters exist
|
54
|
+
return @dataset if @default_filters.empty?
|
55
|
+
|
56
|
+
# Run only the default filters
|
57
|
+
filters, params = @default_filters, EMPTY_PARAMS
|
58
|
+
else
|
59
|
+
# This request really does want filtering; run a full reduction
|
60
|
+
filters, params = @filters, url_params.to_unsafe_h.symbolize_keys
|
61
|
+
end
|
49
62
|
|
50
|
-
|
51
|
-
# @deprecated
|
52
|
-
# Rack::Reducer.new will become an alias of ::create in v2.0.
|
53
|
-
# To mount middleware that will still work in 2.0, write
|
54
|
-
# "use Rack::Reducer::Middleware" instead of "use Rack::Reducer"
|
55
|
-
def self.new(app, options = {})
|
56
|
-
warn "#{caller(1..1).first}}\n#{Warnings[:new]}"
|
57
|
-
Middleware.new(app, options)
|
63
|
+
reduce(params, filters)
|
58
64
|
end
|
59
65
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
# Artist.reduce(params)
|
71
|
-
#
|
72
|
-
# @deprecated
|
73
|
-
# Rack::Reducer's mixin-style is deprecated and may be removed in 2.0.
|
74
|
-
# To keep using Rack::Reducer in your models, create a Reducer constant.
|
75
|
-
# class MyModel < ActiveRecord::Base
|
76
|
-
# MyReducer = Rack::Reducer.create(dataset, *filter_functions)
|
77
|
-
# end
|
78
|
-
# MyModel::MyReducer.call(params)
|
79
|
-
def reduces(dataset, filters:)
|
80
|
-
warn "#{caller(1..1).first}}\n#{Warnings[:reduces]}"
|
81
|
-
reducer = Reduction.new(dataset, *filters)
|
82
|
-
define_singleton_method :reduce do |params|
|
83
|
-
reducer.apply(params)
|
66
|
+
private
|
67
|
+
|
68
|
+
def reduce(params, filters)
|
69
|
+
filters.reduce(@dataset) do |data, filter|
|
70
|
+
next data unless filter.satisfies?(params)
|
71
|
+
|
72
|
+
data.instance_exec(
|
73
|
+
**params.slice(*filter.all_argument_names),
|
74
|
+
&filter
|
75
|
+
)
|
84
76
|
end
|
85
77
|
end
|
78
|
+
|
79
|
+
EMPTY_PARAMS = {}.freeze
|
80
|
+
private_constant :EMPTY_PARAMS
|
86
81
|
end
|
87
82
|
end
|
@@ -1,10 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rack/request'
|
4
|
-
require_relative 'reduction'
|
5
4
|
|
6
5
|
module Rack
|
7
|
-
|
6
|
+
class Reducer
|
8
7
|
# Mount Rack::Reducer as middleware
|
9
8
|
# @example A microservice that filters artists
|
10
9
|
# ArtistService = Rack::Builder.new do
|
@@ -23,10 +22,10 @@ module Rack
|
|
23
22
|
def initialize(app, options = {})
|
24
23
|
@app = app
|
25
24
|
@key = options[:key] || 'rack.reduction'
|
26
|
-
@reducer = Rack::Reducer.
|
25
|
+
@reducer = Rack::Reducer.new(options[:dataset], *options[:filters])
|
27
26
|
end
|
28
27
|
|
29
|
-
# Call the next app in the middleware stack, with env[key] set
|
28
|
+
# Call the next app in the middleware stack, with `env[key]` set
|
30
29
|
# to the ouput of a reduction
|
31
30
|
def call(env)
|
32
31
|
params = Rack::Request.new(env).params
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Rack
|
4
|
-
|
5
|
-
#
|
4
|
+
class Reducer
|
5
|
+
# Refine a few core classes in Rack::Reducer's scope only
|
6
6
|
module Refinements
|
7
7
|
refine Proc do
|
8
8
|
def required_argument_names
|
@@ -37,6 +37,12 @@ module Rack
|
|
37
37
|
|
38
38
|
alias_method :to_unsafe_h, :to_h
|
39
39
|
end
|
40
|
+
|
41
|
+
refine NilClass do
|
42
|
+
def empty?
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
40
46
|
end
|
41
47
|
|
42
48
|
private_constant :Refinements
|
data/lib/rack/reducer/version.rb
CHANGED
data/spec/fixtures.rb
CHANGED
@@ -17,14 +17,11 @@ module Fixtures
|
|
17
17
|
->(name:) {
|
18
18
|
select { |item| item[:name].match(/#{name}/i) }
|
19
19
|
},
|
20
|
-
->(sort: 'name') {
|
21
|
-
sort_by { |item| item[sort.to_sym] }
|
22
|
-
},
|
23
20
|
->(releases:) {
|
24
21
|
select { |item| item[:release_count].to_i == releases.to_i }
|
25
22
|
},
|
26
23
|
]
|
27
24
|
|
28
|
-
ArtistReducer = Rack::Reducer.
|
25
|
+
ArtistReducer = Rack::Reducer.new(DB[:artists], *FILTERS)
|
29
26
|
end
|
30
27
|
|
data/spec/middleware_spec.rb
CHANGED
@@ -4,10 +4,10 @@ require_relative 'fixtures'
|
|
4
4
|
RSpec.describe Rack::Reducer::Middleware do
|
5
5
|
using SpecRefinements
|
6
6
|
module AppFactory
|
7
|
-
def self.create(key: nil
|
7
|
+
def self.create(key: nil)
|
8
8
|
Rack::Builder.new do
|
9
9
|
use(
|
10
|
-
|
10
|
+
Rack::Reducer::Middleware,
|
11
11
|
dataset: Fixtures::DB[:artists],
|
12
12
|
filters: Fixtures::FILTERS,
|
13
13
|
key: key
|
@@ -44,20 +44,4 @@ RSpec.describe Rack::Reducer::Middleware do
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
47
|
-
|
48
|
-
describe 'using Rack::Reducer instead of Rack::Reducer::Middleware' do
|
49
|
-
before do
|
50
|
-
@warnings = []
|
51
|
-
allow(Rack::Reducer).to receive(:warn) do |msg|
|
52
|
-
@warnings << msg
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
let(:app) { AppFactory.create(middleware_class: Rack::Reducer) }
|
57
|
-
|
58
|
-
it 'emits a deprecation warning' do
|
59
|
-
get('/')
|
60
|
-
expect(@warnings.last).to include('alias of ::create')
|
61
|
-
end
|
62
|
-
end
|
63
47
|
end
|
data/spec/reducer_spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require_relative 'fixtures'
|
3
3
|
|
4
|
-
RSpec.describe Rack::Reducer do
|
4
|
+
RSpec.describe 'Rack::Reducer' do
|
5
5
|
using SpecRefinements
|
6
6
|
|
7
7
|
let(:app) do
|
@@ -64,17 +64,31 @@ RSpec.describe Rack::Reducer do
|
|
64
64
|
expect(Fixtures::ArtistReducer.apply(nil)).to be_truthy
|
65
65
|
end
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
67
|
+
describe 'with default filters' do
|
68
|
+
let(:app) do
|
69
|
+
sort = ->(sort: 'name') { sort_by { |item| item[sort.to_sym] } }
|
70
|
+
filters = Fixtures::FILTERS + [sort]
|
71
|
+
reducer = Rack::Reducer.new(Fixtures::DB[:artists], *filters)
|
72
|
+
|
73
|
+
lambda do |env|
|
74
|
+
req = Rack::Request.new(env)
|
75
|
+
res = reducer.apply(req.params).to_json
|
76
|
+
[200, { 'Content-Type' => 'application/json' }, [res]]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'applies default filters' do
|
81
|
+
get '/artists' do |response|
|
82
|
+
name = response.json[0]['name']
|
83
|
+
expect(name).to eq('Björk')
|
84
|
+
end
|
71
85
|
end
|
72
|
-
end
|
73
86
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
87
|
+
it 'overrides default filters with values from params' do
|
88
|
+
get '/artists?sort=genre' do |response|
|
89
|
+
genre = response.json[0]['genre']
|
90
|
+
expect(genre).to eq('alt-soul')
|
91
|
+
end
|
78
92
|
end
|
79
93
|
end
|
80
94
|
|
@@ -91,22 +105,8 @@ RSpec.describe Rack::Reducer do
|
|
91
105
|
end
|
92
106
|
end
|
93
107
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
let(:model) do
|
98
|
-
dataset = Fixtures::DB[:artists].dup
|
99
|
-
allow(dataset).to(receive(:warn)) { |msg| @warnings << msg }
|
100
|
-
dataset.extend Rack::Reducer
|
101
|
-
dataset.reduces dataset, filters: Fixtures::FILTERS
|
102
|
-
dataset
|
103
|
-
end
|
104
|
-
|
105
|
-
it 'is still supported, but with a deprecation warning' do
|
106
|
-
params = { 'genre' => 'electronic', 'name' => 'blake' }
|
107
|
-
expect(model.reduce(params).count).to eq(1)
|
108
|
-
expect(@warnings.first).to include('mixin-style is deprecated')
|
109
|
-
end
|
108
|
+
it 'aliases ::create and ::new' do
|
109
|
+
expect(Rack::Reducer.create({}, -> { 'hi' })).to be_a(Rack::Reducer)
|
110
110
|
end
|
111
111
|
|
112
112
|
it 'accepts nested params' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-reducer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Frank
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-05-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -222,10 +222,8 @@ files:
|
|
222
222
|
- README.md
|
223
223
|
- lib/rack/reducer.rb
|
224
224
|
- lib/rack/reducer/middleware.rb
|
225
|
-
- lib/rack/reducer/reduction.rb
|
226
225
|
- lib/rack/reducer/refinements.rb
|
227
226
|
- lib/rack/reducer/version.rb
|
228
|
-
- lib/rack/reducer/warnings.rb
|
229
227
|
- spec/benchmarks.rb
|
230
228
|
- spec/fixtures.rb
|
231
229
|
- spec/middleware_spec.rb
|
@@ -1,46 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'refinements'
|
4
|
-
|
5
|
-
module Rack
|
6
|
-
module Reducer
|
7
|
-
# call `reduce` on a params hash, filtering data via lambdas with
|
8
|
-
# matching keyword arguments
|
9
|
-
class Reduction
|
10
|
-
using Refinements # define Proc#required_argument_names, #satisfies?, etc
|
11
|
-
|
12
|
-
def initialize(dataset, *filters)
|
13
|
-
@dataset = dataset
|
14
|
-
@filters = filters
|
15
|
-
@default_filters = filters.select do |filter|
|
16
|
-
filter.required_argument_names.empty?
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
# Run +@filters+ against the params argument
|
21
|
-
# @param [Hash, ActionController::Parameters, nil] params
|
22
|
-
# a Rack-compatible params hash
|
23
|
-
# @return +@dataset+ with the matching filters applied
|
24
|
-
def apply(params)
|
25
|
-
if !params || params.empty?
|
26
|
-
return @dataset if @default_filters.empty?
|
27
|
-
|
28
|
-
filters = @default_filters
|
29
|
-
symbolized_params = {}
|
30
|
-
else
|
31
|
-
filters = @filters
|
32
|
-
symbolized_params = params.to_unsafe_h.symbolize_keys
|
33
|
-
end
|
34
|
-
|
35
|
-
filters.reduce(@dataset) do |data, filter|
|
36
|
-
next data unless filter.satisfies?(symbolized_params)
|
37
|
-
|
38
|
-
data.instance_exec(
|
39
|
-
**symbolized_params.slice(*filter.all_argument_names),
|
40
|
-
&filter
|
41
|
-
)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
@@ -1,27 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Rack
|
4
|
-
module Reducer
|
5
|
-
module Warnings
|
6
|
-
MESSAGES = {
|
7
|
-
new: [
|
8
|
-
'Rack::Reducer.new will become an alias of ::create in v2.',
|
9
|
-
'To mount middleware that will still work in 2.0, write',
|
10
|
-
'"use Rack::Reducer::Middleware" instead of "use Rack::Reducer"',
|
11
|
-
],
|
12
|
-
reduces: [
|
13
|
-
'Rack::Reducer’s mixin-style is deprecated and may be removed in v2.',
|
14
|
-
'To keep using Rack::Reducer in your models, use a Reducer constant.',
|
15
|
-
'class MyModel',
|
16
|
-
' MyReducer = Rack::Reducer.create(dataset, *filter_functions)',
|
17
|
-
'end',
|
18
|
-
'MyModel::MyReducer.call(params)',
|
19
|
-
]
|
20
|
-
}.freeze
|
21
|
-
|
22
|
-
def self.[](key)
|
23
|
-
MESSAGES.fetch(key, []).join("\n")
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|