Skip to content

Commit 9fb5710

Browse files
authored
2.0.0 (#8)
This commit drops the deprecated methods in v1.1, in preparation for a 2.0 release. * Refactor Reduction#apply for readability * Make RR a class, not a Module that acts like one * Use ::new instead of ::create in the README * Update CHANGELOG
1 parent 9845b5b commit 9fb5710

13 files changed

+131
-195
lines changed

.rubocop.yml

+3
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ Style/TrailingCommaInHashLiteral:
2323
EnforcedStyleForMultiline: consistent_comma
2424
Style/Documentation:
2525
Enabled: false
26+
Style/ParallelAssignment:
27+
Enabled: false
28+

.yardopts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
--readme README.md
2+
--title 'Rack::Reducer API Documentation'
3+
--charset utf-8
4+
--markup markdown
5+
lib/rack/reducer/middleware.rb
6+
lib/rack/reducer.rb

CHANGELOG.md

+14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## 2.0.0 - Unreleased
8+
9+
### Removed
10+
- Drop the deprecated 'mixin-style' API.
11+
- Drop the deprecated middleware API. Middleware remains supported via
12+
`use Rack::Reducer::Middleware`.
13+
14+
### Changed
15+
- Update `Rack::Reducer.new` to instantiate a reducer, instead of reserving it
16+
for the old Middleware API.
17+
- Refer to `::new` intead of `::create` in the docs. Note that `::create` remains
18+
supported as an alias of `::new`.
19+
20+
721
## 1.1.2 - 2019-04-24
822

923
### Fixed

README.md

+13-8
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ data. Here’s how you might use it in a Rails controller:
3434
# app/controllers/artists_controller.rb
3535
class ArtistsController < ApplicationController
3636

37-
# Step 1: Create a reducer
38-
ArtistReducer = Rack::Reducer.create(
37+
# Step 1: Instantiate a reducer
38+
ArtistReducer = Rack::Reducer.new(
3939
Artist.all,
4040
->(name:) { where('lower(name) like ?', "%#{name.downcase}%") },
4141
->(genre:) { where(genre: genre) },
@@ -91,7 +91,7 @@ class SinatraExample < Sinatra::Base
9191
DB = Sequel.connect ENV['DATABASE_URL']
9292

9393
# dataset is a Sequel::Dataset, so filters use Sequel query methods
94-
ArtistReducer = Rack::Reducer.create(
94+
ArtistReducer = Rack::Reducer.new(
9595
DB[:artists],
9696
->(genre:) { where(genre: genre) },
9797
->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
@@ -155,7 +155,7 @@ more sense to keep your reducers in your models instead.
155155
class Artist < ApplicationRecord
156156
# filters get instance_exec'd against the dataset you provide -- in this case
157157
# it's `self.all` -- so filters can use query methods, scopes, etc
158-
Reducer = Rack::Reducer.create(
158+
Reducer = Rack::Reducer.new(
159159
self.all,
160160
->(name:) { by_name(name) },
161161
->(genre:) { where(genre: genre) },
@@ -188,7 +188,7 @@ it exists, and by name otherwise.
188188

189189
```ruby
190190
class ArtistsController < ApplicationController
191-
ArtistReducer = Rack::Reducer.create(
191+
ArtistReducer = Rack::Reducer.new(
192192
Artist.all,
193193
->(genre:) { where(genre: genre) },
194194
->(sort: 'name') { order(sort.to_sym) }
@@ -203,8 +203,8 @@ end
203203

204204
Calling Rack::Reducer as a function
205205
-------------------------------------------
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
206+
For a slight performance penalty (~5%), you can skip instantiating a reducer via
207+
`::new` and just call Rack::Reducer as a function. This can be useful when
208208
prototyping, mostly because you don't need to think about naming anything.
209209

210210
```ruby
@@ -271,7 +271,7 @@ instead if you want to handle parameterless requests at top speed.
271271
```ruby
272272
# app/controllers/artists_controller.rb
273273
class ArtistController < ApplicationController
274-
# ArtistReducer = Rack::Reducer.create(...etc etc)
274+
# ArtistReducer = Rack::Reducer.new(...etc etc)
275275

276276
def index
277277
@artists = ArtistReducer.apply(request.query_parameters)
@@ -292,6 +292,11 @@ It is Rails-only, but it supports more than just ActiveRecord.
292292
For Sinatra, Simon Courtois has a [Sinatra port of has_scope][sin_has_scope].
293293
It depends on ActiveRecord.
294294

295+
Contributors
296+
---------------
297+
Thank you @danielpuglisi, @nicolasleger, @jeremyshearer, and @shanecav84 for
298+
helping improve Rack::Reducer!
299+
295300
Contributing
296301
-------------------------------
297302
### Bugs

lib/rack/reducer.rb

+53-58
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
# frozen_string_literal: true
22

3-
require_relative 'reducer/reduction'
3+
require_relative 'reducer/refinements'
44
require_relative 'reducer/middleware'
5-
require_relative 'reducer/warnings'
65

76
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`.
1122
# @param [Object] dataset an ActiveRecord::Relation, Sequel::Dataset,
1223
# or other class with chainable methods
1324
# @param [Array<Proc>] filters An array of lambdas whose keyword arguments
1425
# name the URL params you will use as filters
15-
# @return Rack::Reducer::Reduction
1626
# @example Create a reducer and use it in a Sinatra app
1727
# DB = Sequel.connect(ENV['DATABASE_URL'])
18-
# MyReducer = Rack::Reducer.create(
28+
#
29+
# MyReducer = Rack::Reducer.new(
1930
# DB[:artists],
2031
# lambda { |name:| where(name: name) },
2132
# lambda { |genre:| where(genre: genre) },
@@ -25,63 +36,47 @@ module Reducer
2536
# @artists = MyReducer.apply(params)
2637
# @artists.to_json
2738
# 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
3045
end
3146

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
4962

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)
5864
end
5965

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+
)
8476
end
8577
end
78+
79+
EMPTY_PARAMS = {}.freeze
80+
private_constant :EMPTY_PARAMS
8681
end
8782
end

lib/rack/reducer/middleware.rb

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
# frozen_string_literal: true
22

33
require 'rack/request'
4-
require_relative 'reduction'
54

65
module Rack
7-
module Reducer
6+
class Reducer
87
# Mount Rack::Reducer as middleware
98
# @example A microservice that filters artists
109
# ArtistService = Rack::Builder.new do
@@ -23,10 +22,10 @@ class Middleware
2322
def initialize(app, options = {})
2423
@app = app
2524
@key = options[:key] || 'rack.reduction'
26-
@reducer = Rack::Reducer.create(options[:dataset], *options[:filters])
25+
@reducer = Rack::Reducer.new(options[:dataset], *options[:filters])
2726
end
2827

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
3029
# to the ouput of a reduction
3130
def call(env)
3231
params = Rack::Request.new(env).params

lib/rack/reducer/reduction.rb

-46
This file was deleted.

lib/rack/reducer/refinements.rb

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# frozen_string_literal: true
22

33
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
66
module Refinements
77
refine Proc do
88
def required_argument_names
@@ -37,6 +37,12 @@ def symbolize_keys
3737

3838
alias_method :to_unsafe_h, :to_h
3939
end
40+
41+
refine NilClass do
42+
def empty?
43+
true
44+
end
45+
end
4046
end
4147

4248
private_constant :Refinements

lib/rack/reducer/version.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
module Rack
4-
module Reducer
5-
VERSION = '1.1.2'
4+
class Reducer
5+
VERSION = '2.0.0'
66
end
77
end

lib/rack/reducer/warnings.rb

-27
This file was deleted.

spec/fixtures.rb

+1-4
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,11 @@ module Fixtures
1717
->(name:) {
1818
select { |item| item[:name].match(/#{name}/i) }
1919
},
20-
->(sort: 'name') {
21-
sort_by { |item| item[sort.to_sym] }
22-
},
2320
->(releases:) {
2421
select { |item| item[:release_count].to_i == releases.to_i }
2522
},
2623
]
2724

28-
ArtistReducer = Rack::Reducer.create(DB[:artists], *FILTERS)
25+
ArtistReducer = Rack::Reducer.new(DB[:artists], *FILTERS)
2926
end
3027

0 commit comments

Comments
 (0)