rack-reducer 1.1.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f873c82d2086af18eb08d88accb164a6694ff5be7c88f468db013ba068ae009
4
- data.tar.gz: e0ebf95f94eedc38f61da779e578844c0d7645f725871713af73759ce9817ef2
3
+ metadata.gz: 89560b1f9fb58a56452e6e9cbba2a9dd5e0159b938c804628750549da310c543
4
+ data.tar.gz: f7e9667193f0e4e6e5b413ceec8506919f784604d5be97746239688f3074ab8b
5
5
  SHA512:
6
- metadata.gz: 18b2af15129314633b9c3e29a5045bf4cf7927e8b6cae72b4cb160129e0edc55b4f231312481098ba654d30d3b0e464fd9c3b272f1dd2d736622e43d4d866fce
7
- data.tar.gz: be9d27ca83200a17a6462f0f637d7c95ad1c55d17327e606c60c1733d8e52b6b6790d57a5b70732261d393a6ba3fa91740e2ccc0bd6fca5b9ea8df746944cf6c
6
+ metadata.gz: fa641c8d24f2bd6a52f523e447c43d1714c92db33175750b4728512bcdca9f5668a018a4be1b5a504fb3ad0188ace6e5e3911503c6a06d48cbf3d9a64aecf489
7
+ data.tar.gz: d60cbdae9d7d225024680262be9be5b8b92a923ad224452dfd7e383410e75d5ee717ac3842b9f0ed95ebb8b3789ef36bc3880794b62577f1162a6d67816df77b
data/README.md CHANGED
@@ -2,6 +2,7 @@ Rack::Reducer
2
2
  ==========================================
3
3
  [![Build Status](https://travis-ci.org/chrisfrank/rack-reducer.svg?branch=master)](https://travis-ci.org/chrisfrank/rack-reducer)
4
4
  [![Maintainability](https://api.codeclimate.com/v1/badges/675e7a654c7e11c24b9f/maintainability)](https://codeclimate.com/github/chrisfrank/rack-reducer/maintainability)
5
+ [![Version](https://img.shields.io/gem/v/rack-reducer.svg)](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: Create a reducer
38
- ArtistReducer = Rack::Reducer.create(
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.create(
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.create(
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.create(
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 creating a reducer via
207
- `::create` and just call Rack::Reducer as a function. This can be useful when
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.create(...etc etc)
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/reduction'
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
- module Reducer
10
- # Create a Reduction object that can filter +dataset+ via +#apply+.
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
- # MyReducer = Rack::Reducer.create(
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 self.create(dataset, *filters)
29
- Reduction.new(dataset, *filters)
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
- # Filter a dataset without creating a Reducer first.
33
- # Note that this approach is a bit slower and less memory-efficient than
34
- # creating a Reducer via ::create. Use ::create when you can.
35
- #
36
- # @param params [Hash] Rack-compatible URL params
37
- # @param dataset [Object] A dataset, e.g. one of your App's models
38
- # @param filters [Array<Proc>] An array of lambdas with keyword arguments
39
- # @example Call Rack::Reducer as a function in a Sinatra app
40
- # get '/artists' do
41
- # @artists = Rack::Reducer.call(params, dataset: Artist.all, filters: [
42
- # lambda { |name:| where(name: name) },
43
- # lambda { |genre:| where(genre: genre) },
44
- # ])
45
- # end
46
- def self.call(params, dataset:, filters:)
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
- # Mount Rack::Reducer as middleware
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
- # Extend Rack::Reducer to get +reduce+ and +reduces+ as class-methods
61
- #
62
- # @example Make an "Artists" model reducible
63
- # class Artist < SomeORM::Model
64
- # extend Rack::Reducer
65
- # reduces self.all, filters: [
66
- # lambda { |name:| where(name: name) },
67
- # lambda { |genre:| where(genre: genre) },
68
- # ]
69
- # end
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
- module Reducer
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.create(options[:dataset], *options[:filters])
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
- module Reducer
5
- # refine Proc and hash in this scope only
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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rack
4
- module Reducer
5
- VERSION = '1.1.2'
4
+ class Reducer
5
+ VERSION = '2.0.0'
6
6
  end
7
7
  end
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.create(DB[:artists], *FILTERS)
25
+ ArtistReducer = Rack::Reducer.new(DB[:artists], *FILTERS)
29
26
  end
30
27
 
@@ -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, middleware_class: Rack::Reducer::Middleware)
7
+ def self.create(key: nil)
8
8
  Rack::Builder.new do
9
9
  use(
10
- middleware_class,
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
- it 'applies default filters' do
68
- get '/artists' do |response|
69
- name = response.json[0]['name']
70
- expect(name).to eq('Björk')
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
- it 'can override default params' do
75
- get '/artists?sort=genre' do |response|
76
- genre = response.json[0]['genre']
77
- expect(genre).to eq('alt-soul')
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
- describe 'mixin-style' do
95
- before { @warnings = [] }
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: 1.1.2
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-04-24 00:00:00.000000000 Z
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