rack-reducer 1.0.1 → 1.1.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 +112 -252
- data/lib/rack/reducer/middleware.rb +17 -2
- data/lib/rack/reducer/reduction.rb +19 -16
- data/lib/rack/reducer/refinements.rb +13 -1
- data/lib/rack/reducer/version.rb +3 -1
- data/lib/rack/reducer/warnings.rb +27 -0
- data/lib/rack/reducer.rb +51 -20
- data/spec/benchmarks.rb +51 -21
- data/spec/fixtures.rb +30 -0
- data/spec/middleware_spec.rb +55 -23
- data/spec/rails_spec.rb +33 -3
- data/spec/reducer_spec.rb +104 -0
- data/spec/spec_helper.rb +6 -15
- metadata +34 -136
- data/lib/rack/reducer/parser.rb +0 -26
- data/spec/_hanami_example/apps/web/application.rb +0 -326
- data/spec/_hanami_example/apps/web/config/routes.rb +0 -4
- data/spec/_hanami_example/apps/web/controllers/artists/index.rb +0 -12
- data/spec/_hanami_example/apps/web/views/application_layout.rb +0 -7
- data/spec/_hanami_example/config/boot.rb +0 -2
- data/spec/_hanami_example/config/environment.rb +0 -29
- data/spec/_hanami_example/lib/hanami_example/entities/artist.rb +0 -2
- data/spec/_hanami_example/lib/hanami_example/repositories/artist_repository.rb +0 -9
- data/spec/_hanami_example/lib/hanami_example.rb +0 -5
- data/spec/_rails_example/app/channels/application_cable/channel.rb +0 -4
- data/spec/_rails_example/app/channels/application_cable/connection.rb +0 -4
- data/spec/_rails_example/app/controllers/application_controller.rb +0 -2
- data/spec/_rails_example/app/controllers/artists_controller.rb +0 -8
- data/spec/_rails_example/app/jobs/application_job.rb +0 -2
- data/spec/_rails_example/app/mailers/application_mailer.rb +0 -4
- data/spec/_rails_example/app/models/application_record.rb +0 -3
- data/spec/_rails_example/app/models/rails_example/artist.rb +0 -21
- data/spec/_rails_example/config/application.rb +0 -35
- data/spec/_rails_example/config/boot.rb +0 -3
- data/spec/_rails_example/config/environment.rb +0 -5
- data/spec/_rails_example/config/environments/development.rb +0 -47
- data/spec/_rails_example/config/environments/production.rb +0 -83
- data/spec/_rails_example/config/environments/test.rb +0 -42
- data/spec/_rails_example/config/initializers/application_controller_renderer.rb +0 -8
- data/spec/_rails_example/config/initializers/backtrace_silencers.rb +0 -7
- data/spec/_rails_example/config/initializers/cors.rb +0 -16
- data/spec/_rails_example/config/initializers/filter_parameter_logging.rb +0 -4
- data/spec/_rails_example/config/initializers/inflections.rb +0 -16
- data/spec/_rails_example/config/initializers/mime_types.rb +0 -4
- data/spec/_rails_example/config/initializers/wrap_parameters.rb +0 -14
- data/spec/_rails_example/config/puma.rb +0 -56
- data/spec/_rails_example/config/routes.rb +0 -4
- data/spec/_rails_example/db/seeds.rb +0 -7
- data/spec/behavior.rb +0 -51
- data/spec/hanami_spec.rb +0 -6
- data/spec/roda_spec.rb +0 -13
- data/spec/sinatra_functional_spec.rb +0 -26
- data/spec/sinatra_mixin_spec.rb +0 -20
| @@ -0,0 +1,27 @@ | |
| 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
         | 
    
        data/lib/rack/reducer.rb
    CHANGED
    
    | @@ -1,38 +1,64 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require_relative 'reducer/reduction'
         | 
| 2 4 | 
             
            require_relative 'reducer/middleware'
         | 
| 5 | 
            +
            require_relative 'reducer/warnings'
         | 
| 3 6 |  | 
| 4 7 | 
             
            module Rack
         | 
| 5 | 
            -
              #  | 
| 8 | 
            +
              # Declaratively filter data via URL params, in any Rack app.
         | 
| 6 9 | 
             
              module Reducer
         | 
| 7 | 
            -
                #  | 
| 10 | 
            +
                # Create a Reduction object that can filter +dataset+ via +#apply+.
         | 
| 11 | 
            +
                # @param [Object] dataset an ActiveRecord::Relation, Sequel::Dataset,
         | 
| 12 | 
            +
                #   or other class with chainable methods
         | 
| 13 | 
            +
                # @param [Array<Proc>] filters  An array of lambdas whose keyword arguments
         | 
| 14 | 
            +
                #   name the URL params you will use as filters
         | 
| 15 | 
            +
                # @return Rack::Reducer::Reduction
         | 
| 16 | 
            +
                # @example Create a reducer and use it in a Sinatra app
         | 
| 17 | 
            +
                #   DB = Sequel.connect(ENV['DATABASE_URL'])
         | 
| 18 | 
            +
                #   MyReducer = Rack::Reducer.create(
         | 
| 19 | 
            +
                #     DB[:artists],
         | 
| 20 | 
            +
                #     lambda { |name:| where(name: name) },
         | 
| 21 | 
            +
                #     lambda { |genre:| where(genre: genre) },
         | 
| 22 | 
            +
                #   )
         | 
| 23 | 
            +
                #
         | 
| 24 | 
            +
                #   get '/artists' do
         | 
| 25 | 
            +
                #     @artists = MyReducer.apply(params)
         | 
| 26 | 
            +
                #     @artists.to_json
         | 
| 27 | 
            +
                #   end
         | 
| 28 | 
            +
                def self.create(dataset, *filters)
         | 
| 29 | 
            +
                  Reduction.new(dataset, *filters)
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 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 | 
            +
                #
         | 
| 8 36 | 
             
                # @param params [Hash] Rack-compatible URL params
         | 
| 9 37 | 
             
                # @param dataset [Object] A dataset, e.g. one of your App's models
         | 
| 10 38 | 
             
                # @param filters [Array<Proc>] An array of lambdas with keyword arguments
         | 
| 11 39 | 
             
                # @example Call Rack::Reducer as a function in a Sinatra app
         | 
| 12 | 
            -
                #    | 
| 13 | 
            -
                #     dataset: Artist,
         | 
| 14 | 
            -
                #     filters: [
         | 
| 40 | 
            +
                #   get '/artists' do
         | 
| 41 | 
            +
                #     @artists = Rack::Reducer.call(params, dataset: Artist.all, filters: [
         | 
| 15 42 | 
             
                #       lambda { |name:| where(name: name) },
         | 
| 16 43 | 
             
                #       lambda { |genre:| where(genre: genre) },
         | 
| 17 | 
            -
                #     ]
         | 
| 18 | 
            -
                #   }
         | 
| 19 | 
            -
                #   get '/artists' do
         | 
| 20 | 
            -
                #     @artists = Rack::Reducer.call(params, ArtistReducer)
         | 
| 44 | 
            +
                #     ])
         | 
| 21 45 | 
             
                #   end
         | 
| 22 46 | 
             
                def self.call(params, dataset:, filters:)
         | 
| 23 | 
            -
                  Reduction.new(
         | 
| 24 | 
            -
                    params: params,
         | 
| 25 | 
            -
                    filters: filters,
         | 
| 26 | 
            -
                    dataset: dataset
         | 
| 27 | 
            -
                  ).reduce
         | 
| 47 | 
            +
                  Reduction.new(dataset, *filters).apply(params)
         | 
| 28 48 | 
             
                end
         | 
| 29 49 |  | 
| 30 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"
         | 
| 31 55 | 
             
                def self.new(app, options = {})
         | 
| 56 | 
            +
                  warn "#{caller(1..1).first}}\n#{Warnings[:new]}"
         | 
| 32 57 | 
             
                  Middleware.new(app, options)
         | 
| 33 58 | 
             
                end
         | 
| 34 59 |  | 
| 35 60 | 
             
                # Extend Rack::Reducer to get +reduce+ and +reduces+ as class-methods
         | 
| 61 | 
            +
                #
         | 
| 36 62 | 
             
                # @example Make an "Artists" model reducible
         | 
| 37 63 | 
             
                #   class Artist < SomeORM::Model
         | 
| 38 64 | 
             
                #     extend Rack::Reducer
         | 
| @@ -41,15 +67,20 @@ module Rack | |
| 41 67 | 
             
                #       lambda { |genre:| where(genre: genre) },
         | 
| 42 68 | 
             
                #     ]
         | 
| 43 69 | 
             
                #   end
         | 
| 44 | 
            -
                #
         | 
| 45 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)
         | 
| 46 79 | 
             
                def reduces(dataset, filters:)
         | 
| 80 | 
            +
                  warn "#{caller(1..1).first}}\n#{Warnings[:reduces]}"
         | 
| 81 | 
            +
                  reducer = Reduction.new(dataset, *filters)
         | 
| 47 82 | 
             
                  define_singleton_method :reduce do |params|
         | 
| 48 | 
            -
                     | 
| 49 | 
            -
                      params: params,
         | 
| 50 | 
            -
                      filters: filters,
         | 
| 51 | 
            -
                      dataset: dataset,
         | 
| 52 | 
            -
                    ).reduce
         | 
| 83 | 
            +
                    reducer.apply(params)
         | 
| 53 84 | 
             
                  end
         | 
| 54 85 | 
             
                end
         | 
| 55 86 | 
             
              end
         | 
    
        data/spec/benchmarks.rb
    CHANGED
    
    | @@ -1,40 +1,70 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
            require ' | 
| 1 | 
            +
            require 'rack'
         | 
| 2 | 
            +
            require 'pry'
         | 
| 3 3 | 
             
            require 'json'
         | 
| 4 | 
            +
            require_relative '../lib/rack/reducer'
         | 
| 4 5 | 
             
            require 'benchmark/ips'
         | 
| 6 | 
            +
            require_relative 'fixtures'
         | 
| 7 | 
            +
            require 'sequel'
         | 
| 5 8 |  | 
| 6 | 
            -
             | 
| 9 | 
            +
            DB = Sequel.sqlite.tap do |db|
         | 
| 10 | 
            +
              db.create_table(:artists) do
         | 
| 11 | 
            +
                primary_key :id
         | 
| 12 | 
            +
                String :name
         | 
| 13 | 
            +
                String :genre
         | 
| 14 | 
            +
                Integer :release_count
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
              Fixtures::DB[:artists].each { |row| db[:artists].insert(row) }
         | 
| 17 | 
            +
            end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            conditional_app = lambda do |env|
         | 
| 20 | 
            +
              params = Rack::Request.new(env).params
         | 
| 7 21 | 
             
              @artists = DB[:artists]
         | 
| 8 | 
            -
              if (genre = params[ | 
| 9 | 
            -
                @artists = @artists. | 
| 22 | 
            +
              if (genre = params['genre'])
         | 
| 23 | 
            +
                @artists = @artists.where(genre: genre.to_s)
         | 
| 10 24 | 
             
              end
         | 
| 11 | 
            -
              if (name = params[ | 
| 25 | 
            +
              if (name = params['name'])
         | 
| 12 26 | 
             
                @artists = @artists.grep(:name, "%#{name}%", case_insensitive: true)
         | 
| 13 27 | 
             
              end
         | 
| 14 | 
            -
             | 
| 15 | 
            -
              @artists.to_json
         | 
| 28 | 
            +
              Rack::Response.new(@artists).finish
         | 
| 16 29 | 
             
            end
         | 
| 17 30 |  | 
| 18 | 
            -
             | 
| 19 | 
            -
               | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 31 | 
            +
            TestReducer = Rack::Reducer.create(
         | 
| 32 | 
            +
              DB[:artists],
         | 
| 33 | 
            +
              ->(genre:) { where(genre: genre.to_s) },
         | 
| 34 | 
            +
              ->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
         | 
| 35 | 
            +
            )
         | 
| 23 36 |  | 
| 24 | 
            -
             | 
| 37 | 
            +
            reducer_app = lambda do |env|
         | 
| 38 | 
            +
              params = Rack::Request.new(env).params
         | 
| 39 | 
            +
              @artists = TestReducer.apply(params)
         | 
| 40 | 
            +
              Rack::Response.new(@artists).finish
         | 
| 25 41 | 
             
            end
         | 
| 26 42 |  | 
| 27 | 
            -
            Benchmark.ips | 
| 28 | 
            -
               | 
| 43 | 
            +
            Benchmark.ips do |bm|
         | 
| 44 | 
            +
              env = {
         | 
| 45 | 
            +
                'REQUEST_METHOD' => 'GET',
         | 
| 46 | 
            +
                'PATH_INFO' => '/',
         | 
| 47 | 
            +
                'rack.input' => StringIO.new('')
         | 
| 48 | 
            +
              }
         | 
| 29 49 |  | 
| 30 | 
            -
               | 
| 50 | 
            +
              query = {
         | 
| 51 | 
            +
                'QUERY_STRING' => 'name=blake&genre=electronic',
         | 
| 52 | 
            +
              }
         | 
| 53 | 
            +
             | 
| 54 | 
            +
              bm.report('Conditionals (full)') do
         | 
| 55 | 
            +
                conditional_app.call env.merge(query)
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
              bm.report('Reducer (full)') do
         | 
| 59 | 
            +
                reducer_app.call env.merge(query)
         | 
| 60 | 
            +
              end
         | 
| 31 61 |  | 
| 32 | 
            -
              bm.report(' | 
| 33 | 
            -
                 | 
| 62 | 
            +
              bm.report('Conditionals (empty)') do
         | 
| 63 | 
            +
                conditional_app.call env.dup
         | 
| 34 64 | 
             
              end
         | 
| 35 65 |  | 
| 36 | 
            -
              bm.report(' | 
| 37 | 
            -
                 | 
| 66 | 
            +
              bm.report('Reducer (empty)') do
         | 
| 67 | 
            +
                reducer_app.call env.dup
         | 
| 38 68 | 
             
              end
         | 
| 39 69 |  | 
| 40 70 | 
             
              bm.compare!
         | 
    
        data/spec/fixtures.rb
    ADDED
    
    | @@ -0,0 +1,30 @@ | |
| 1 | 
            +
            module Fixtures
         | 
| 2 | 
            +
              DB = {
         | 
| 3 | 
            +
                artists: [
         | 
| 4 | 
            +
                  { name: 'Blake Mills', genre: 'alternative', release_count: 3 },
         | 
| 5 | 
            +
                  { name: 'Björk', genre: 'electronic', release_count: 3 },
         | 
| 6 | 
            +
                  { name: 'James Blake', genre: 'electronic', release_count: 3 },
         | 
| 7 | 
            +
                  { name: 'Janelle Monae', genre: 'alt-soul', release_count: 3 },
         | 
| 8 | 
            +
                  { name: 'SZA', genre: 'alt-soul', release_count: 3 },
         | 
| 9 | 
            +
                  { name: 'Chris Frank', genre: 'alt-soul', release_count: nil },
         | 
| 10 | 
            +
                ]
         | 
| 11 | 
            +
              }
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              FILTERS = [
         | 
| 14 | 
            +
                ->(genre:) {
         | 
| 15 | 
            +
                  select { |item| item[:genre].match(/#{genre}/i) }
         | 
| 16 | 
            +
                },
         | 
| 17 | 
            +
                ->(name:) {
         | 
| 18 | 
            +
                  select { |item| item[:name].match(/#{name}/i) }
         | 
| 19 | 
            +
                },
         | 
| 20 | 
            +
                ->(sort:) {
         | 
| 21 | 
            +
                  sort_by { |item| item[sort.to_sym] }
         | 
| 22 | 
            +
                },
         | 
| 23 | 
            +
                ->(releases:) {
         | 
| 24 | 
            +
                  select { |item| item[:release_count].to_i == releases.to_i }
         | 
| 25 | 
            +
                },
         | 
| 26 | 
            +
              ]
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              ArtistReducer = Rack::Reducer.create(DB[:artists], *FILTERS)
         | 
| 29 | 
            +
            end
         | 
| 30 | 
            +
             | 
    
        data/spec/middleware_spec.rb
    CHANGED
    
    | @@ -1,31 +1,63 @@ | |
| 1 1 | 
             
            require 'spec_helper'
         | 
| 2 | 
            -
             | 
| 2 | 
            +
            require_relative 'fixtures'
         | 
| 3 3 |  | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
            module  | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
              }
         | 
| 4 | 
            +
            RSpec.describe Rack::Reducer::Middleware do
         | 
| 5 | 
            +
              using SpecRefinements
         | 
| 6 | 
            +
              module AppFactory
         | 
| 7 | 
            +
                def self.create(key: nil, middleware_class: Rack::Reducer::Middleware)
         | 
| 8 | 
            +
                  Rack::Builder.new do
         | 
| 9 | 
            +
                    use(
         | 
| 10 | 
            +
                      middleware_class,
         | 
| 11 | 
            +
                      dataset: Fixtures::DB[:artists],
         | 
| 12 | 
            +
                      filters: Fixtures::FILTERS,
         | 
| 13 | 
            +
                      key: key
         | 
| 14 | 
            +
                    )
         | 
| 15 | 
            +
                    run ->(env) {  [200, {}, [env.to_json]] }
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              describe 'without a key set' do
         | 
| 21 | 
            +
                let(:app) { AppFactory.create }
         | 
| 22 | 
            +
                it 'responds with unfiltered data when filter params are empty' do
         | 
| 23 | 
            +
                  get('/') do |res|
         | 
| 24 | 
            +
                    reduction = res.json['rack.reduction']
         | 
| 25 | 
            +
                    expect(reduction.count).to eq(Fixtures::DB[:artists].count)
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                end
         | 
| 16 28 |  | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 29 | 
            +
                it 'filters by a single param, e.g. name' do
         | 
| 30 | 
            +
                  get('/artists?name=Blake') do |res|
         | 
| 31 | 
            +
                    reduction = res.body
         | 
| 32 | 
            +
                    expect(reduction).to include('Blake Mills')
         | 
| 33 | 
            +
                    expect(reduction).to include('James Blake')
         | 
| 34 | 
            +
                    expect(reduction).not_to include('SZA')
         | 
| 35 | 
            +
                  end
         | 
| 21 36 | 
             
                end
         | 
| 22 37 | 
             
              end
         | 
| 23 | 
            -
            end
         | 
| 24 38 |  | 
| 25 | 
            -
            describe  | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 39 | 
            +
              describe 'with a custom key' do
         | 
| 40 | 
            +
                let(:app) { AppFactory.create(key: 'custom_key') }
         | 
| 41 | 
            +
                it 'stores reducer data at env[custom_key]' do
         | 
| 42 | 
            +
                  get('/') do |res|
         | 
| 43 | 
            +
                    expect(res.json['custom_key'].class).to eq(Array)
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 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
         | 
| 28 55 |  | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 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
         | 
| 31 63 | 
             
            end
         | 
    
        data/spec/rails_spec.rb
    CHANGED
    
    | @@ -1,6 +1,36 @@ | |
| 1 1 | 
             
            require 'spec_helper'
         | 
| 2 | 
            -
            require_relative ' | 
| 2 | 
            +
            require_relative 'fixtures'
         | 
| 3 | 
            +
            require 'action_controller/railtie'
         | 
| 4 | 
            +
            require 'securerandom'
         | 
| 3 5 |  | 
| 4 | 
            -
             | 
| 5 | 
            -
               | 
| 6 | 
            +
            class RailsApp < Rails::Application
         | 
| 7 | 
            +
              routes.append do
         | 
| 8 | 
            +
                get "/", to: "artists#index"
         | 
| 9 | 
            +
                get "/query", to: "artists#query"
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              config.api_only = true
         | 
| 13 | 
            +
              config.eager_load = true
         | 
| 14 | 
            +
              config.secret_key_base = SecureRandom.hex(64)
         | 
| 15 | 
            +
            end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            class ArtistsController < ActionController::API
         | 
| 18 | 
            +
              def index
         | 
| 19 | 
            +
                @artists = Fixtures::ArtistReducer.apply(params)
         | 
| 20 | 
            +
                render json: @artists
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              def query
         | 
| 24 | 
            +
                @artists = Fixtures::ArtistReducer.apply(request.query_parameters)
         | 
| 25 | 
            +
                render json: @artists
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            RSpec.describe RailsApp do
         | 
| 30 | 
            +
              let(:app) { RailsApp.initialize! }
         | 
| 31 | 
            +
             | 
| 32 | 
            +
              it 'works with ActionController::Parameters and a plain hash' do
         | 
| 33 | 
            +
                get('/') { |res| expect(res.status).to eq(200) }
         | 
| 34 | 
            +
                get('/query') { |res| expect(res.status).to eq(200) }
         | 
| 35 | 
            +
              end
         | 
| 6 36 | 
             
            end
         | 
| @@ -0,0 +1,104 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
            require_relative 'fixtures'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            RSpec.describe Rack::Reducer do
         | 
| 5 | 
            +
              using SpecRefinements
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              let(:app) do
         | 
| 8 | 
            +
                lambda do |env|
         | 
| 9 | 
            +
                  req = Rack::Request.new(env)
         | 
| 10 | 
            +
                  res = Fixtures::ArtistReducer.apply(req.params).to_json
         | 
| 11 | 
            +
                  [200, { 'Content-Type' => 'application/json' }, [res]]
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              it 'responds with unfiltered data when filter params are empty' do
         | 
| 16 | 
            +
                get('/') do |res|
         | 
| 17 | 
            +
                  expect(res.json.count).to eq(Fixtures::DB[:artists].count)
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              it 'filters by a single param, e.g. name' do
         | 
| 22 | 
            +
                get('/artists?name=Blake') do |response|
         | 
| 23 | 
            +
                  expect(response.body).to include('Blake Mills')
         | 
| 24 | 
            +
                  expect(response.body).to include('James Blake')
         | 
| 25 | 
            +
                  expect(response.body).not_to include('SZA')
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              it 'resets state between requests' do
         | 
| 30 | 
            +
                get('/artists?name=Blake')
         | 
| 31 | 
            +
                get('/artists') do |res|
         | 
| 32 | 
            +
                  expect(res.json.count).to eq(Fixtures::DB[:artists].count)
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              it 'filters by a single param, e.g. genre' do
         | 
| 37 | 
            +
                get('/artists?genre=electronic') do |response|
         | 
| 38 | 
            +
                  expect(response.body).to include('Björk')
         | 
| 39 | 
            +
                  expect(response.body).to include('James Blake')
         | 
| 40 | 
            +
                  expect(response.body).not_to include('Blake Mills')
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                get '/artists?genre=soul' do |response|
         | 
| 44 | 
            +
                  expect(response.body).to include('Janelle Monae')
         | 
| 45 | 
            +
                  expect(response.body).not_to include('Björk')
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              it 'chains multiple filters' do
         | 
| 50 | 
            +
                get('/artists?genre=electronic&name=blake') do |response|
         | 
| 51 | 
            +
                  expect(response.body).to include('James Blake')
         | 
| 52 | 
            +
                  expect(response.body).not_to include('Blake Mills')
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              it 'handles falsy values' do
         | 
| 57 | 
            +
                get('/artists?releases=0') do |response|
         | 
| 58 | 
            +
                  expect(response.body).to include('Chris Frank')
         | 
| 59 | 
            +
                  expect(JSON.parse(response.body).length).to eq(1)
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
              end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
              it 'accepts nil as params' do
         | 
| 64 | 
            +
                expect(Fixtures::ArtistReducer.apply(nil)).to be_truthy
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
              it 'can sort' do
         | 
| 68 | 
            +
                get '/artists?order=genre' do |response|
         | 
| 69 | 
            +
                  genre = response.json[0]['genre']
         | 
| 70 | 
            +
                  expect(genre).to eq('alternative')
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
              end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
              describe 'ad-hoc style via ::call' do
         | 
| 75 | 
            +
                let(:params) { { 'genre' => 'electronic', 'name' => 'blake' } }
         | 
| 76 | 
            +
                it 'works just like the primary style, but slower' do
         | 
| 77 | 
            +
                  result = Rack::Reducer.call(
         | 
| 78 | 
            +
                    params,
         | 
| 79 | 
            +
                    dataset: Fixtures::DB[:artists],
         | 
| 80 | 
            +
                    filters: Fixtures::FILTERS,
         | 
| 81 | 
            +
                  )
         | 
| 82 | 
            +
                  expect(result.count).to eq(1)
         | 
| 83 | 
            +
                  expect(result[0][:name]).to eq('James Blake')
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
              end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
              describe 'mixin-style' do
         | 
| 88 | 
            +
                before { @warnings = [] }
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                let(:model) do
         | 
| 91 | 
            +
                  dataset = Fixtures::DB[:artists].dup
         | 
| 92 | 
            +
                  allow(dataset).to(receive(:warn)) { |msg| @warnings << msg }
         | 
| 93 | 
            +
                  dataset.extend Rack::Reducer
         | 
| 94 | 
            +
                  dataset.reduces dataset, filters: Fixtures::FILTERS
         | 
| 95 | 
            +
                  dataset
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                it 'is still supported, but with a deprecation warning' do
         | 
| 99 | 
            +
                  params = { 'genre' => 'electronic', 'name' => 'blake' }
         | 
| 100 | 
            +
                  expect(model.reduce(params).count).to eq(1)
         | 
| 101 | 
            +
                  expect(@warnings.first).to include('mixin-style is deprecated')
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
              end
         | 
| 104 | 
            +
            end
         | 
    
        data/spec/spec_helper.rb
    CHANGED
    
    | @@ -1,26 +1,17 @@ | |
| 1 1 | 
             
            require 'bundler/setup'
         | 
| 2 2 | 
             
            Bundler.setup
         | 
| 3 | 
            -
            require 'rspec'
         | 
| 4 3 | 
             
            require 'pry'
         | 
| 5 4 | 
             
            require 'rack/test'
         | 
| 6 5 | 
             
            require 'rack/reducer'
         | 
| 7 | 
            -
            require 'sequel'
         | 
| 8 | 
            -
            require_relative 'behavior'
         | 
| 9 | 
            -
            ENV['RACK_ENV'] = ENV['RAILS_ENV'] = 'test'
         | 
| 10 | 
            -
            DB = Sequel.connect "sqlite://#{__dir__}/fixtures.sqlite"
         | 
| 11 | 
            -
             | 
| 12 | 
            -
            SEQUEL_QUERY = {
         | 
| 13 | 
            -
              dataset: DB[:artists],
         | 
| 14 | 
            -
              filters: [
         | 
| 15 | 
            -
                ->(genre:) { grep(:genre, "%#{genre}%", case_insensitive: true) },
         | 
| 16 | 
            -
                ->(name:) { grep(:name, "%#{name}%", case_insensitive: true) },
         | 
| 17 | 
            -
                ->(order: 'genre') { order(order.to_sym) },
         | 
| 18 | 
            -
                ->(releases: ) { where(release_count: releases.to_i) },
         | 
| 19 | 
            -
              ]
         | 
| 20 | 
            -
            }.freeze
         | 
| 21 6 |  | 
| 22 7 | 
             
            RSpec.configure do |config|
         | 
| 23 8 | 
             
              config.color = true
         | 
| 24 9 | 
             
              config.order = :random
         | 
| 25 10 | 
             
              config.include Rack::Test::Methods
         | 
| 26 11 | 
             
            end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            module SpecRefinements
         | 
| 14 | 
            +
              refine Rack::MockResponse do
         | 
| 15 | 
            +
                define_method(:json) { JSON.parse(body) }
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         |