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 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