Last active
June 19, 2020 01:04
-
-
Save dudo/72782d0c9e96f52264324042d3c1c992 to your computer and use it in GitHub Desktop.
Simple Concern to allow chain-able filtering of ActiveRecord scopes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Example < ActiveRecord::Base | |
include Filterable | |
filterable scopes: %i(search bar) | |
scope :foo, ->(q) { where(foo: q) } | |
scope :bar, ->(q) { where(bar: q) } | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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