Skip to content

Instantly share code, notes, and snippets.

@dudo
Last active June 19, 2020 01:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dudo/72782d0c9e96f52264324042d3c1c992 to your computer and use it in GitHub Desktop.
Save dudo/72782d0c9e96f52264324042d3c1c992 to your computer and use it in GitHub Desktop.
Simple Concern to allow chain-able filtering of ActiveRecord scopes
class Example < ActiveRecord::Base
include Filterable
filterable scopes: %i(search bar)
scope :foo, ->(q) { where(foo: q) }
scope :bar, ->(q) { where(bar: q) }
end
# frozen_string_literal: true
module Filterable
extend ActiveSupport::Concern
included do
@available_filters ||= []
@default_filters ||= {}
end
class_methods do
attr_accessor :available_filters
attr_accessor :default_filters
def filter_by(params: {})
params ||= {}
scope = all
filterable_params(params).each do |filter_name, filter_param|
scope = scope.public_send(filter_name.downcase, filter_param)
end
scope
end
def filterable(scopes: [], defaults: {})
scopes ||= []
defaults ||= {}
@available_filters = Array.wrap(scopes).map(&:to_s)
@default_filters = defaults.stringify_keys
end
def filterable_params(params)
default_filters
.merge(params.stringify_keys.select { |_, v| v.present? || [true, false].include?(v) })
.keep_if { |k, v| available_filters.include?(k.downcase) && v.present? }
end
end
end
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Filterable, type: :model do
before(:all) { create_mock_table }
after(:all) { drop_mock_table }
let(:mock_class) do
Class.new(ApplicationRecord) do
self.table_name = :mock_table
reset_column_information
include Filterable
scope :active, ->(value) { where(active: value) }
scope :by_name, ->(value) { where(name: value) }
scope :by_description, ->(value) { where('description like ?', "%#{value}%") }
filterable scopes: %i[active by_name], defaults: { active: true }
end
end
describe '.available_filters' do
subject(:available_filters) { mock_class.available_filters }
it 'is set as string through configuration' do
expect(available_filters).to match_array %w[active by_name]
end
end
describe '.default_filters' do
subject(:available_filters) { mock_class.default_filters }
it 'is set through configuration w/ string keys' do
expect(available_filters).to eq('active' => true)
end
end
describe '.filter_by' do
let!(:record1) { mock_class.create!(name: Faker::Name.name, active: true) }
let!(:record2) { mock_class.create!(name: Faker::Name.name, active: false) }
let!(:record3) { mock_class.create!(active: true, description: 'foo') }
subject(:filter) { mock_class.filter_by(params: params) }
context 'with nil params' do
let(:params) { nil }
it 'returns records matching the Filterable defaults' do
expect(filter).to match_array [record1, record3]
end
end
context 'without any params' do
let(:params) { {} }
it 'returns records matching the Filterable defaults' do
expect(filter).to match_array [record1, record3]
end
end
context 'with partial params' do
let(:params) { { by_name: record1.name } }
it 'returns records matching the Filterable defaults with additional param filtering' do
expect(filter).to match_array [record1]
end
end
context 'with empty params' do
let(:params) { { by_name: ' ' } }
it 'converts to nil and searches for nil' do
expect(filter).to match_array [record1, record3]
end
end
context 'with overriding default params' do
let(:params) { { active: false } }
it 'uses new value as filter param' do
expect(filter).to match_array [record2]
end
end
context 'with empty param overriding default params' do
let(:params) { { name: '' } }
it 'overrides default options' do
expect(filter).to match_array [record1, record3]
end
end
context 'with multiple params' do
let(:params) { { active: false, by_name: record2.name } }
it 'returns matching records' do
expect(filter).to match_array [record2]
end
end
context 'with params referencing a non-filterable scope' do
let(:params) { { by_description: record3.description } }
it 'ignores and it falls back to default params' do
expect(filter).to match_array [record1, record3]
end
end
end
def create_mock_table
ActiveRecord::Base.connection.create_table :mock_table do |t|
t.boolean :active
t.string :name
t.text :description
end
end
def drop_mock_table
ActiveRecord::Base.connection.drop_table :mock_table
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment