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 Reducer
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
0 commit comments