diff --git a/Gemfile.lock b/Gemfile.lock index 9818515..2ec6dd9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - filterable-by (0.5.3) + filterable-by (0.6.0) activerecord activesupport diff --git a/README.md b/README.md index 269051c..db5145a 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,15 @@ class Comment < ActiveRecord::Base belongs_to :post filterable_by :post_id, :user_id - filterable_by :post_author_id do |scope, value| - scope.joins(:posts).where(:'posts.author_id' => value) + filterable_by :post_author_id do |value| + joins(:posts).where(:'posts.author_id' => value) end - filterable_by :only do |scope, value, **opts| + filterable_by :only do |value, **opts| case value when 'mine' - scope.where(user_id: opts[:user_id]) if opts[:user_id] + where(user_id: opts[:user_id]) if opts[:user_id] else - scope + all end end end @@ -39,6 +39,12 @@ Simple use cases: Comment.filter_by({ 'post_id' => '1' }) # => WHERE post_id = 1 +Comment.filter_by({ 'post_id' => ['1', '2'] }) +# => WHERE post_id IN (1, 2) + +Comment.filter_by({ 'post_id_not' => '3' }) +# => WHERE post_id != 3 + Comment.filter_by({ 'user_id' => '2', 'ignored' => '3' }) # => WHERE user_id = 2 diff --git a/filterable-by.gemspec b/filterable-by.gemspec index 9035cf7..df80ffe 100644 --- a/filterable-by.gemspec +++ b/filterable-by.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'filterable-by' - s.version = '0.5.3' + s.version = '0.6.0' s.authors = ['Dimitrij Denissenko'] s.email = ['dimitrij@blacksquaremedia.com'] s.summary = 'Generate white-listed filter scopes from URL parameter values' diff --git a/lib/filterable_by.rb b/lib/filterable_by.rb index 51e08ec..ff1fc05 100644 --- a/lib/filterable_by.rb +++ b/lib/filterable_by.rb @@ -13,6 +13,28 @@ def self.normalize(value) end end + def self.merge(scope, unscoped, hash, name, **opts, &block) + key = name + positive = normalize(hash[key]) if hash.key?(key) + if positive.present? + sub = block.arity == 2 ? yield(unscoped, positive, **opts) : yield(positive, **opts) + return nil unless sub + + scope = scope.merge(sub) + end + + key = "#{name}_not" + negative = normalize(hash[key]) if hash.key?(key) + if negative.present? + sub = block.arity == 2 ? yield(unscoped, negative, **opts) : yield(negative, **opts) + return nil unless sub + + scope = scope.merge(sub.invert_where) + end + + scope + end + module ClassMethods def self.extended(base) # :nodoc: base.class_attribute :_filterable_by_config, instance_accessor: false, instance_predicate: false @@ -26,8 +48,12 @@ def inherited(base) # :nodoc: end def filterable_by(*names, &block) + if block && block.arity > 1 + ActiveSupport::Deprecation.warn('using scope in filterable_by blocks is deprecated. Please use filterable_by(:x) {|val| where(field: val) } instead.') + end + names.each do |name| - _filterable_by_config[name.to_s] = block || ->(scope, value, **) { scope.where(name.to_sym => value) } + _filterable_by_config[name.to_s] = block || ->(value, **) { where(name.to_sym => value) } end end @@ -38,16 +64,13 @@ def filter_by(hash = nil, **opts) hash = opts opts = {} end + scope = all return scope unless hash.respond_to?(:key?) && hash.respond_to?(:[]) _filterable_by_config.each do |name, block| - next unless hash.key?(name) - - value = FilterableBy.normalize(hash[name]) - next if value.blank? - - scope = block.call(scope, value, **opts) + scope = FilterableBy.merge(scope, unscoped, hash, name, **opts, &block) + break unless scope end scope || none diff --git a/spec/active_record/filterable_by_spec.rb b/spec/active_record/filterable_by_spec.rb index 737c675..5f7c0f9 100644 --- a/spec/active_record/filterable_by_spec.rb +++ b/spec/active_record/filterable_by_spec.rb @@ -8,8 +8,8 @@ let(:bpost) { POSTS[:bobs] } it 'has config' do - expect(Comment.send(:_filterable_by_config).count).to eq(3) - expect(Rating.send(:_filterable_by_config).count).to eq(2) + expect(Comment.send(:_filterable_by_config).count).to eq(5) + expect(Rating.send(:_filterable_by_config).count).to eq(4) expect(Post.send(:_filterable_by_config).count).to eq(2) end @@ -50,6 +50,14 @@ expect(scope.pluck(:title)).to match_array(%w[AB BB]) end + it 'generates negated scopes' do + expect(Comment.filter_by('author_id_not' => alice.id).pluck(:title)).to match_array(%w[BA BB]) + expect(Comment.filter_by('author_id_not' => [alice.id, bob.id]).pluck(:title)).to match_array(%w[]) + expect(Comment.filter_by('post_id_not' => apost.id).pluck(:title)).to match_array(%w[AB BB]) + expect(Comment.filter_by('post_author_id_not' => alice.id).pluck(:title)).to match_array(%w[AB BB]) + expect(Comment.filter_by('author_id' => bob.id, 'post_id_not' => bpost.id).pluck(:title)).to match_array(['BA']) + end + it 'combines with other scopes' do scope = Comment.where(author_id: alice.id).filter_by('post_id' => apost.id) expect(scope.pluck(:title)).to match_array(['AA']) @@ -77,4 +85,10 @@ expect(Post.filter_by('post_id' => bpost.id).count).to eq(2) expect(Rating.filter_by('post_author_id' => bob.id).count).to eq(1) end + + it 'supports deprecated scoping' do + expect(Comment.filter_by('deprecated' => alice.id).pluck(:title)).to match_array(%w[AA AB]) + expect(Comment.filter_by('deprecated_with_opts' => alice.id).pluck(:title)).to match_array(%w[AA AB]) + expect(Comment.filter_by('deprecated_not' => alice.id).pluck(:title)).to match_array(%w[BA BB]) + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6470e6d..f4a4e7c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -28,12 +28,12 @@ class Post < ActiveRecord::Base belongs_to :author filterable_by :author_id - filterable_by :only do |scope, value, **opts| + filterable_by :only do |value, **opts| case value when 'me' - scope.where(author_id: opts[:user_id]) if opts[:user_id] + where(author_id: opts[:user_id]) if opts[:user_id] else - scope + all end end end @@ -43,11 +43,20 @@ class Feedback < ActiveRecord::Base belongs_to :post filterable_by :post_id, :author_id + + ActiveSupport::Deprecation.silence do + filterable_by :deprecated do |scope, value| + scope.where(author_id: value) + end + filterable_by :deprecated_with_opts do |scope, value, **_opts| + scope.where(author_id: value) + end + end end class Comment < Feedback - filterable_by :post_author_id do |scope, value| - scope.joins(:post).where(Post.arel_table[:author_id].eq(value)) + filterable_by :post_author_id do |value| + joins(:post).where(Post.arel_table[:author_id].eq(value)) end end