filter_me 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 48be03e95db1c79700cb86c0e4d7d5fc3bb0105f
4
+ data.tar.gz: 1dadc42cd98cf3c249139deff59d5bb381e02f93
5
+ SHA512:
6
+ metadata.gz: 0176700ff8445af02e848c65c8ad4e9018bce1c863ad8dab7932d349c1a5574a67c68190a0e21f28eacf320d82ef6b27c2d7a3e6414ebe7d4c656600b153d764
7
+ data.tar.gz: ef3971291473338f4d2728d9fa99d11e12be7f78b3bc962da835567b2f64bea9ea6e728ba404aa3027dd10458395d18d2fefd3c72a48165fe17414f5d7089d15
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+ spec/internal/db/*sqlite
3
+ pkg/
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ script: bundle exec rspec spec
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENCE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Sam Clopton
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ [![Code Climate](https://codeclimate.com/github/Samsinite/filter_me.png)](https://codeclimate.com/github/Samsinite/filter_me)
2
+ filter_me
3
+ =========
4
+
5
+ A Rails/ActiveRecord filtering gem
6
+
7
+ ### Coming Soon to a application near you!
8
+
9
+
10
+ ## License
11
+ Copyright (c) 2014, Filter Me is developed and maintained by Sam Clopton, and is released under the open MIT Licence.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'bundler'
2
+
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ task :console do
6
+ require 'irb'
7
+ require 'irb/completion'
8
+ require 'filter_me' # You know what to do.
9
+ ARGV.clear
10
+ IRB.start
11
+ end
12
+
data/config.ru ADDED
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.require :default, :development
5
+
6
+ Combustion.initialize!
7
+ run Combustion::Application
data/filter_me.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'filter_me/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = 'filter_me'
9
+ s.version = FilterMe::VERSION
10
+ s.authors = ['Sam Clopton']
11
+ s.email = ['samsinite@gmail.com']
12
+ s.homepage = 'https://github.com/samsinite/filter_me'
13
+ s.summary = 'Filter Me'
14
+ s.description = 'This friendly library gives you ActiveRecord/Arel filtering in your Rails app.'
15
+ s.license = 'MIT'
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {spec}/*`.split("\n")
19
+ s.require_paths = ['lib']
20
+
21
+ s.add_runtime_dependency 'activerecord', '>= 3.2.0'
22
+ s.add_runtime_dependency 'activesupport', '>= 3.2.0'
23
+ #s.add_runtime_dependency 'activemodel', '>= 3.2.0'
24
+ s.add_development_dependency 'combustion', '~> 0.5.1'
25
+ s.add_development_dependency 'rspec-rails', '~> 2.13'
26
+ s.add_development_dependency 'sqlite3', '~> 1.3.7'
27
+ end
@@ -0,0 +1,51 @@
1
+ module FilterMe
2
+ class Filter
3
+ class ArelFieldFilter
4
+ attr_accessor :filters, :configuration
5
+
6
+ def initialize(filters, configuration)
7
+ @filters = filters
8
+ @configuration = configuration
9
+
10
+ unless validator.valid_filters?(filters)
11
+ raise FiltersNotWhiteListedError, "The filter types #{validator.invalid_filters(filters)} are not allowed."
12
+ end
13
+ end
14
+
15
+ def filter(relation)
16
+ arel_filters.inject { |arel_relation, filter| filter.and(arel_relation) }
17
+ end
18
+
19
+ private
20
+
21
+ def arel_filters
22
+ self.filters.map { |filter_array| build_filters_from_filter_array(filter_array) }.flatten
23
+ end
24
+
25
+ def build_filters_from_filter_array(filter_array)
26
+ type = filter_array[0]
27
+ filter_array[1].map { |value| build_arel_filter(type, value) }
28
+ end
29
+
30
+ def build_arel_filter(type, value)
31
+ arel_filter = arel_table[field].send(type, value)
32
+ end
33
+
34
+ def arel_table
35
+ model_class.arel_table
36
+ end
37
+
38
+ def model_class
39
+ configuration[:model_class]
40
+ end
41
+
42
+ def validator
43
+ configuration[:validator]
44
+ end
45
+
46
+ def field
47
+ configuration[:field].to_sym
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,67 @@
1
+ module FilterMe
2
+ class Filter
3
+
4
+ ###
5
+ # DSL Syntax (early version, highly subject to change):
6
+ #
7
+ # class UserFilter < FilterMe::ArelFilter
8
+ # model User
9
+ #
10
+ # association :account
11
+ # association :company, filter_class: SuperDuperCompanyFilter
12
+ #
13
+ # field :login, [:matches]
14
+ # field :created_at, [:gt, :gteq, :lt, :lteq, :eq]
15
+ # field :email, :all
16
+ # ...
17
+ # end
18
+ ###
19
+ class ArelDSL
20
+ attr_reader :filter_class
21
+
22
+ extend Forwardable
23
+
24
+ def initialize(filter_class)
25
+ @filter_class = filter_class
26
+ end
27
+
28
+ def model(klass)
29
+ @filter_class._model = klass
30
+ end
31
+
32
+ def filter_for_name(name)
33
+ "#{name.to_s.pluralize.camelize}Filter".constantize
34
+ end
35
+
36
+ def field(name, filter_types)
37
+ field_validator = filter_types == :all ? AllValidator.new : FieldValidator.new(filter_types)
38
+ filter(name, ArelFieldFilter, { :field => name,
39
+ :validator => field_validator,
40
+ :model_class => self.filter_class._model })
41
+ end
42
+
43
+ def association(name, options={})
44
+ association_filter_class = options.fetch(:filter_class) { filter_for_name(name) }
45
+ configuration = options.fetch(:configuration, {})
46
+ filter_class._associations[name] = association_filter_class
47
+
48
+ filter(name, association_filter_class, configuration, filter_class._model)
49
+ end
50
+
51
+ private
52
+
53
+ def filter(name, klass, configuration, association = nil)
54
+ this = self
55
+ filter_class.send(:define_method, name) do |relation, filters|
56
+ filter = klass.new(filters, configuration)
57
+ if association
58
+ relation = relation.joins(name.to_sym)
59
+ filter.filter(relation)
60
+ else
61
+ relation.where(filter.filter(relation))
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,32 @@
1
+ module FilterMe
2
+ class Filter
3
+ class AllValidator
4
+ def valid_filters?(filters)
5
+ true
6
+ end
7
+
8
+ def invalid_filters(filters)
9
+ []
10
+ end
11
+
12
+ def whitelisted_filters
13
+ :all
14
+ end
15
+ end
16
+ class FieldValidator
17
+ attr_reader :whitelisted_filters
18
+
19
+ def initialize(whitelisted_filters)
20
+ @whitelisted_filters = whitelisted_filters
21
+ end
22
+
23
+ def valid_filters?(filters)
24
+ filters.all? { |filter| whitelisted_filters.include? filter[0] }
25
+ end
26
+
27
+ def invalid_filters(filters)
28
+ filters.select { |filter| !(whitelisted_filters.include? filter[0]) }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,73 @@
1
+ require "forwardable"
2
+
3
+ require 'filter_me/filterable'
4
+ require 'filter_me/filter/field_validator'
5
+ require 'filter_me/filter/arel_field_filter'
6
+ require 'filter_me/filter/dsl'
7
+
8
+ module FilterMe
9
+ class ActiveRecordFilter
10
+ include Filterable
11
+
12
+ class << self
13
+ def inherited(subclass)
14
+ subclass._associations = (_associations || {}).dup
15
+ end
16
+
17
+ def filter_for(relation)
18
+ "#{relation.klass.name.pluralize}Filter".constantize
19
+ end
20
+
21
+ attr_accessor :_associations, :_model
22
+
23
+ extend Forwardable
24
+
25
+ def_delegators :dsl, :field, :association, :model
26
+
27
+ private
28
+
29
+ def dsl
30
+ @dsl ||= Filter::ArelDSL.new(self)
31
+ end
32
+ end
33
+
34
+ attr_accessor :configuration, :filters
35
+
36
+ # Filters are expected to be an array of arrays where the first value is
37
+ # the filter type and the remaining values are the values to filter by. Or if the
38
+ # filter type is a relation, the rest are sub filters (sub-array).
39
+ #
40
+ ### Ex:
41
+ # class AccountsFilter < FilterMe::ActiveRecordFilter
42
+ # model Account
43
+ #
44
+ # field :type, [:matches, :eq, :not_eq]
45
+ # end
46
+ # class UsersFilter < FilterMe::ActiveRecordFilter
47
+ # model User
48
+ #
49
+ # association :account, :filter_class => AccountsFilter
50
+ # field :username, [:matches, :eq, :not_eq]
51
+ # end
52
+ #
53
+ # then the 'filters' param for an initialization of a user filter which would then
54
+ # filter users by accounts of types that match '%paid%' would look like:
55
+ # [
56
+ # # filters on the associaton account
57
+ # [:account, [
58
+ # [:type, [
59
+ # [:matches, ["%paid%"]]
60
+ # ]]
61
+ # ]],
62
+ # # filters on field username
63
+ # [:username, [
64
+ # [:not_eq, ["user1", "user2"]],
65
+ # [:matches, ["%user_%"]]
66
+ # ]]
67
+ # ]
68
+ def initialize(filters, configuration)
69
+ @filters = filters
70
+ @configuration = configuration
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,9 @@
1
+ module FilterMe
2
+ module Filterable
3
+ def filter(relation)
4
+ self.filters.inject(relation) do |relation, filter|
5
+ self.send filter[0], relation, filter[1]
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module FilterMe
2
+ VERSION = '0.1.0'
3
+ end
data/lib/filter_me.rb ADDED
@@ -0,0 +1,88 @@
1
+ require 'active_support/concern'
2
+
3
+ require 'filter_me/filter'
4
+ require 'filter_me/version'
5
+
6
+ module FilterMe
7
+ class FiltersNotWhiteListedError < StandardError; end
8
+
9
+ extend ActiveSupport::Concern
10
+
11
+ class << self
12
+ def normalize_param(param)
13
+ param.inject([]) do |filter, (k, v)|
14
+ case v
15
+ when Hash
16
+ filter.push([k, FilterMe.normalize_param(v)])
17
+ when Array
18
+ filter.push([k, v])
19
+ else
20
+ filter.push([k, [v]])
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ included do
27
+ if respond_to?(:helper_method)
28
+ helper_method :filter_me
29
+ end
30
+
31
+ if respond_to?(:hide_action)
32
+ hide_action :filter_me
33
+ hide_action :filter_configuration
34
+ hide_action :build_filter
35
+ hide_action :filter_params
36
+ end
37
+ end
38
+
39
+ def filter_me(relation, filter_class=nil)
40
+ klass = filter_class || ActiveRecordFilter.filter_for(relation)
41
+ filter = build_filter(klass)
42
+
43
+ filter.filter(relation)
44
+ end
45
+
46
+ def filter_configuration
47
+ {}
48
+ end
49
+
50
+ def build_filter(filter_class)
51
+ filter_class.new(filter_params, filter_configuration)
52
+ end
53
+
54
+ # Normalized Filter params Example:
55
+ # [
56
+ # # filters on the associaton account
57
+ # [:account, [
58
+ # [:type, [
59
+ # [:eq, ["admin"]]
60
+ # ]]
61
+ # ]],
62
+ # # filters on field username
63
+ # [:username, [
64
+ # [:not_in_any, ["sam", "samsinite"]],
65
+ # [:matches, ["sam%"]]
66
+ # ]],
67
+ # [:company, [
68
+ # [:job, [
69
+ # [:name, [
70
+ # [:matches, ["%software%"]]
71
+ # ]]
72
+ # ]]
73
+ # ]]
74
+ # ]
75
+
76
+ # FilterMe default params example (Seems straight forward for Rails to parse):
77
+ # {filters: {
78
+ # username: {matches: "sam%", not_in_any: ["sam", "samsinite"]},
79
+ # account: {type: {eq: "admin"}},
80
+ # company: {job: {name: {matches: "%software%"}}}
81
+ # }}
82
+ #
83
+ # Instead of Django Tastypie's style (original inspiration):
84
+ # [{username__matches: "%sam%"}, {account__type__eq: "admin"}]
85
+ def filter_params
86
+ FilterMe.normalize_param(params.fetch(:filters, {}))
87
+ end
88
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ describe AccountsFilter do
4
+ before(:each) do
5
+ load_accounts
6
+ end
7
+
8
+ let(:mock_controller_class) do
9
+ controller = Class.new do
10
+ include FilterMe
11
+
12
+ attr_accessor :params
13
+
14
+ def index
15
+ filter_me(Account.all)
16
+ end
17
+ end
18
+ end
19
+
20
+ let(:mock_controller) do
21
+ mock_controller_class.new
22
+ end
23
+
24
+ it "returns all accounts without any filtering" do
25
+ mock_controller.params = {}
26
+
27
+ expect(mock_controller.index.length).to eq(Account.all.length)
28
+ end
29
+
30
+ it "returns accounts that cost less than 100000 and greater than 10000" do
31
+ mock_controller.params = {:filters => {
32
+ :cost => {:lt => 100000, :gt => 10000}
33
+ }}
34
+
35
+ mock_controller.index.each do |account|
36
+ expect(account.cost).to be > 10000
37
+ expect(account.cost).to be < 100000
38
+ end
39
+ end
40
+
41
+ it "returns accounts that cost less than or equal to 100000 and greater than 10000" do
42
+ mock_controller.params = {:filters => {
43
+ :cost => {:lteq => 100000, :gt => 10000}
44
+ }}
45
+
46
+ mock_controller.index.each do |account|
47
+ expect(account.cost).to be > 10000
48
+ expect(account.cost).to be <= 100000
49
+ end
50
+ end
51
+
52
+ it "returns accounts that cost less than 100000 and greater than or equal to 10000" do
53
+ mock_controller.params = {:filters => {
54
+ :cost => {:lt => 100000, :gteq => 10000}
55
+ }}
56
+
57
+ mock_controller.index.each do |account|
58
+ expect(account.cost).to be >= 10000
59
+ expect(account.cost).to be < 100000
60
+ end
61
+ end
62
+
63
+ it "returns accounts that cost less than or equal to 100000 and greater than or equal to 10000" do
64
+ mock_controller.params = {:filters => {
65
+ :cost => {:lteq => 100000, :gteq => 10000}
66
+ }}
67
+
68
+ mock_controller.index.each do |account|
69
+ expect(account.cost).to be >= 10000
70
+ expect(account.cost).to be <= 100000
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe UsersFilter do
4
+ before(:each) do
5
+ load_users
6
+ end
7
+
8
+ let(:mock_controller_class) do
9
+ controller = Class.new do
10
+ include FilterMe
11
+
12
+ attr_accessor :params
13
+
14
+ def index
15
+ filter_me(User.all)
16
+ end
17
+ end
18
+ end
19
+
20
+ let(:mock_controller) do
21
+ mock_controller_class.new
22
+ end
23
+
24
+ it "returns all users without any filtering" do
25
+ mock_controller.params = {}
26
+
27
+ expect(mock_controller.index.length).to eq(User.all.length)
28
+ end
29
+
30
+ it "returns users with e-mails from the domain test.com" do
31
+ mock_controller.params = {:filters => {
32
+ :email => {:matches => "%test.com"}
33
+ }}
34
+
35
+ mock_controller.index.each do |user|
36
+ expect(user.email).to end_with "test.com"
37
+ end
38
+ end
39
+
40
+ it "returns users with e-mails from the domain test.com with accounts that cost less than 100000" do
41
+ mock_controller.params = {:filters => {
42
+ :email => {:matches => "%test.com"},
43
+ :account => {:cost => {:lt => 100000}}
44
+ }}
45
+
46
+ mock_controller.index.each do |user|
47
+ expect(user.email).to end_with "test.com"
48
+ expect(user.account.cost).to be < 100000
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ class AccountsFilter < FilterMe::ActiveRecordFilter
2
+ model Account
3
+
4
+ field :cost, [:lt, :lteq, :gt, :gteq, :eq]
5
+ end
@@ -0,0 +1,7 @@
1
+ class UsersFilter < FilterMe::ActiveRecordFilter
2
+ model User
3
+
4
+ association :account, :filter_class => AccountsFilter
5
+ field :email, :all
6
+ field :username, :matches
7
+ end
@@ -0,0 +1,3 @@
1
+ class Account < ActiveRecord::Base
2
+ belongs_to :user
3
+ end
@@ -0,0 +1,3 @@
1
+ class User < ActiveRecord::Base
2
+ has_one :account
3
+ end
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/combustion_test.sqlite
@@ -0,0 +1,17 @@
1
+ module Fixtures
2
+ module Accounts
3
+ def load_accounts
4
+ Account.create! do |account|
5
+ account.cost = 10000
6
+ end
7
+
8
+ Account.create! do |account|
9
+ account.cost = 50000
10
+ end
11
+
12
+ Account.create! do |account|
13
+ account.cost = 100000
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ module Fixtures
2
+ module Users
3
+ def load_users
4
+ User.create! do |user|
5
+ user.username = "test1"
6
+ user.email = "test2@test.com"
7
+
8
+ user.create_account!(:cost => 100000)
9
+ end
10
+
11
+ User.create! do |user|
12
+ user.username = "test2"
13
+ user.email = "test2@test.com"
14
+
15
+ user.create_account!(:cost => 50000)
16
+ end
17
+
18
+ User.create! do |user|
19
+ user.username = "test3"
20
+ user.email = "test3@spaz.com"
21
+
22
+ user.create_account!(:cost => 10000)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :users, :force => true do |t|
3
+ t.string :username
4
+ t.string :email
5
+ t.timestamps
6
+ end
7
+
8
+ create_table :accounts, :force => true do |t|
9
+ t.integer :user_id
10
+ t.integer :cost
11
+ t.timestamps
12
+ end
13
+ end
@@ -0,0 +1 @@
1
+ *.log
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'combustion'
4
+
5
+ Bundler.require :default, :development
6
+
7
+ # Add rails modules here to have combustion load:
8
+ Combustion.initialize! :active_record
9
+
10
+ require 'rspec/rails'
11
+
12
+ # Load fixture helpers for testing
13
+ Dir[File.join(File.dirname(__FILE__), 'internal', 'db', "fixtures", "**", '*.rb')].each { |file| require file }
14
+
15
+ RSpec.configure do |config|
16
+ config.use_transactional_fixtures = true
17
+
18
+ config.include Fixtures::Accounts
19
+ config.include Fixtures::Users
20
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe FilterMe::ActiveRecordFilter do
4
+ let(:filters) do
5
+ [
6
+ # filters on associaton account
7
+ [:account, [
8
+ [:type, [
9
+ [:matches, ["%paid%"]]
10
+ ]]
11
+ ]],
12
+ # filters on field username
13
+ [:username, [
14
+ [:not_eq, ["user1", "user2"]],
15
+ [:matches, ["%user_%"]]
16
+ ]]
17
+ ]
18
+ end
19
+
20
+ it "calls each filter method for every filter it is passed" do
21
+ relation_mock = double("relation")
22
+ model_filter = FilterMe::ActiveRecordFilter.new(filters, {})
23
+
24
+ expect(model_filter).to receive(:account).with(relation_mock, filters[0][1]) { relation_mock }
25
+ expect(model_filter).to receive(:username).with(relation_mock, filters[1][1]) { relation_mock }
26
+ model_filter.filter(relation_mock)
27
+ end
28
+ end
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+
3
+ describe FilterMe::Filter::ArelFieldFilter do
4
+ context "with only greater than and less than allowed" do
5
+ let(:field) { :test }
6
+ let(:arel_table) do
7
+ Arel::Table.new("model")
8
+ end
9
+
10
+ let(:filters) { [:lt, :gt] }
11
+
12
+ let(:model_class) do
13
+ model = double("model")
14
+ allow(model).to receive(:arel_table) { arel_table }
15
+
16
+ model
17
+ end
18
+
19
+ let(:configuration) do
20
+ {
21
+ :field => field,
22
+ :model_class => model_class,
23
+ :validator => FilterMe::Filter::FieldValidator.new(filters)
24
+ }
25
+ end
26
+
27
+ it "can create an arel field filter with a less than filter" do
28
+ field_filter = FilterMe::Filter::ArelFieldFilter.new([[:lt, [10]]], configuration)
29
+ end
30
+
31
+ it "can create an arel field filter with a greater than filter" do
32
+ field_filter = FilterMe::Filter::ArelFieldFilter.new([[:gt, [10]]], configuration)
33
+ end
34
+
35
+ it "can create an arel field filter with a greater than and less than filter" do
36
+ field_filter = FilterMe::Filter::ArelFieldFilter.new([[:gt, [10]], [:lt, [20]]], configuration)
37
+ end
38
+
39
+ it "cannot create an arel field filter with a equal filter" do
40
+ expect { FilterMe::Filter::ArelFieldFilter.new([[:eq, [10]]], configuration) }.to raise_error
41
+ end
42
+ end
43
+ context "with all types allowed" do
44
+ let(:field) { :test }
45
+ let(:arel_table) do
46
+ Arel::Table.new("model")
47
+ end
48
+
49
+ let(:model_class) do
50
+ model = double("model")
51
+ allow(model).to receive(:arel_table) { arel_table }
52
+
53
+ model
54
+ end
55
+
56
+ let(:configuration) do
57
+ {
58
+ :field => field,
59
+ :model_class => model_class,
60
+ :validator => FilterMe::Filter::AllValidator.new
61
+ }
62
+ end
63
+
64
+ it "can create a arel field filter" do
65
+ field_filter = FilterMe::Filter::ArelFieldFilter.new([[:lt, [10]], [:gt, [1]]], configuration)
66
+ end
67
+
68
+ it "builds the correct arel filter with one filter type of one filter value" do
69
+ field_filter = FilterMe::Filter::ArelFieldFilter.new([[:lt, [10]]], configuration)
70
+ relation_mock = double("relation")
71
+
72
+ arel_filter = arel_table[field].lt(10)
73
+ expect(field_filter.filter(relation_mock)).to eq(arel_filter)
74
+ end
75
+
76
+ it "builds the correct arel filter with one filter type of two filter values" do
77
+ field_filter = FilterMe::Filter::ArelFieldFilter.new([[:matches, ["%hi%", "%hey%"]]], configuration)
78
+ relation_mock = double("relation")
79
+
80
+ arel_filter = arel_table[field].matches("%hey%").and(arel_table[field].matches("%hi%"))
81
+ expect(field_filter.filter(relation_mock)).to eq(arel_filter)
82
+ end
83
+
84
+ it "builds the correct arel filter with two filter types of one filter value each" do
85
+ field_filter = FilterMe::Filter::ArelFieldFilter.new([[:gt, [1]], [:lt, [10]]], configuration)
86
+ relation_mock = double("relation")
87
+
88
+ arel_filter = arel_table[field].lt(10).and(arel_table[field].gt(1))
89
+ expect(field_filter.filter(relation_mock)).to eq(arel_filter)
90
+ end
91
+
92
+ it "builds the correct arel filter with two filter types of two filter value each" do
93
+ field_filter = FilterMe::Filter::ArelFieldFilter.new([[:matches, ["%hi%", "%hey%"]], [:lt, ["z", "Z"]]], configuration)
94
+ relation_mock = double("relation")
95
+
96
+ arel_filter = arel_table[field].lt("Z")
97
+ .and(arel_table[field].lt("z")
98
+ .and(arel_table[field].matches("%hey%")
99
+ .and(arel_table[field].matches("%hi%"))))
100
+ expect(field_filter.filter(relation_mock)).to eq(arel_filter)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,187 @@
1
+ require 'spec_helper'
2
+
3
+ describe FilterMe::Filter::ArelDSL do
4
+ it "creates a new arel field filter method named after the field" do
5
+ model = double("model")
6
+ filter_class = double("filter_class")
7
+ filter_class.stub(:_model).and_return(model)
8
+ filter_class.should_receive(:define_method) do |name, &block|
9
+ name.should eq(:test)
10
+ block.should_not be_nil
11
+ end
12
+
13
+ dsl = FilterMe::Filter::ArelDSL.new(filter_class)
14
+ dsl.field(:test, [:gt, :lt, :eq])
15
+ end
16
+
17
+ context "after defining a field filter the dynamically created filter method" do
18
+ it "initializes a ArelFieldFilter instance with the values to filter and the filter field configuration" do
19
+ filters = [:test1, :test2]
20
+ relation_mock = double("relation")
21
+ expect(relation_mock).to receive(:where) { |relation| relation }
22
+
23
+ model_mock = double("model")
24
+
25
+ FilterMe::Filter::FieldValidator.send(:define_method, "==") do |obj|
26
+ obj.whitelisted_filters == self.whitelisted_filters
27
+ end
28
+
29
+ field_filter_instance = double("field filter instance")
30
+ allow(field_filter_instance).to receive(:filter).once
31
+
32
+ field_filter_class = double("field filter class")
33
+ allow(field_filter_class).to receive(:new) do |filters, configuration|
34
+ expect(filters).to eq(filters)
35
+ expect(configuration).to eq({
36
+ :field => :test,
37
+ :validator => FilterMe::Filter::FieldValidator.new([:gt, :lt, :eq]),
38
+ :model_class => model_mock
39
+ })
40
+
41
+ field_filter_instance
42
+ end
43
+
44
+ stub_const("FilterMe::Filter::ArelFieldFilter", field_filter_class)
45
+
46
+ # We just need an object that supports and implements #define_method so the
47
+ # dynamic filter method can be created.
48
+ filter_class = Class.new do
49
+ end
50
+
51
+ allow(filter_class).to receive(:_model) { model_mock }
52
+
53
+ dsl = FilterMe::Filter::ArelDSL.new(filter_class)
54
+ dsl.field(:test, [:gt, :lt, :eq])
55
+
56
+ filter_instance = filter_class.new
57
+ filter_instance.test(relation_mock, filters)
58
+ end
59
+
60
+ it "calls filter on the initialized ArelFieldFilter instance with the relation" do
61
+ relation_mock = double("relation")
62
+ expect(relation_mock).to receive(:where) { |relation| relation }
63
+
64
+ model_mock = double("model")
65
+
66
+ field_filter_instance = double("field filter instance")
67
+ expect(field_filter_instance).to receive(:filter) { |relation| relation }
68
+
69
+ field_filter_class = double("field filter class")
70
+ allow(field_filter_class).to receive(:new) { |filters, configuration| field_filter_instance }
71
+
72
+ stub_const("FilterMe::Filter::ArelFieldFilter", field_filter_class)
73
+
74
+ # We just need an object that supports and implements #define_method so the
75
+ # dynamic filter method can be created.
76
+ filter_class = Class.new do
77
+ end
78
+
79
+ allow(filter_class).to receive(:_model) { model_mock }
80
+
81
+ dsl = FilterMe::Filter::ArelDSL.new(filter_class)
82
+ dsl.field(:test, [:gt, :lt, :eq])
83
+
84
+ filter_instance = filter_class.new
85
+
86
+ expect(filter_instance.test(relation_mock, [:test1, :test2])).to eq(relation_mock)
87
+ end
88
+ end
89
+
90
+
91
+ it "creates a new arel relation filter method named after the association" do
92
+ model = double("model")
93
+ filter_class = double("filter_class")
94
+ filter_class.stub(:_model).and_return(model)
95
+ filter_class.stub(:_associations).and_return({})
96
+ filter_class.should_receive(:define_method) do |name, &block|
97
+ name.should eq(:some_models)
98
+ block.should_not be_nil
99
+ end
100
+
101
+ dsl = FilterMe::Filter::ArelDSL.new(filter_class)
102
+ dsl.association(:some_models, {:filter_class => filter_class})
103
+ end
104
+
105
+ context "after defining an association filter the dynamically created filter method" do
106
+ it "initializes the association filter instance with the values to filter and the filter configuration" do
107
+ filters = [:test1, :test2]
108
+ relation_mock = double("relation")
109
+ allow(relation_mock).to receive(:joins) { relation_mock }
110
+ allow(relation_mock).to receive(:where) { relation_mock }
111
+
112
+ model = double("model")
113
+ allow(model).to receive(:name) { "Model" }
114
+
115
+ mock_association_filter = double("association filter")
116
+ allow(mock_association_filter).to receive(:filter) { relation_mock }
117
+
118
+ mock_association_filter_class = double("assocation filter class")
119
+ mock_association_filter_class.stub(:new).and_return(mock_association_filter)
120
+
121
+ expect(mock_association_filter_class).to receive(:new) do |filters, configuration|
122
+ expect(filters).to eq(filters)
123
+ expect(configuration).to eq({})
124
+ mock_association_filter
125
+ end
126
+
127
+ allow(mock_association_filter_class).to receive(:define_method) do |name, &block|
128
+ allow(mock_association_filter).to receive(name, &block)
129
+ end
130
+
131
+ mock_association_filter_class.stub(:_model).and_return(model)
132
+
133
+ # We just need an object that supports and implements #define_method so the
134
+ # dynamic filter method can be created.
135
+ filter_class = Class.new do
136
+ end
137
+
138
+ allow(filter_class).to receive(:_model) { model }
139
+ allow(filter_class).to receive(:_associations) { {} }
140
+
141
+ dsl = FilterMe::Filter::ArelDSL.new(filter_class)
142
+ dsl.association(:test, {:filter_class => mock_association_filter_class})
143
+
144
+ filter_instance = filter_class.new
145
+ filter_instance.test(relation_mock, filters)
146
+ end
147
+
148
+ it "calls filter on the association filter instance with the relation" do
149
+ filters = [:test1, :test2]
150
+ relation_mock = double("relation")
151
+ allow(relation_mock).to receive(:joins) { relation_mock }
152
+ allow(relation_mock).to receive(:where) { relation_mock }
153
+
154
+ model = double("model")
155
+ allow(model).to receive(:name) { "Model" }
156
+
157
+ mock_association_filter = double("association filter")
158
+ expect(mock_association_filter).to receive(:filter) do |relation|
159
+ expect(relation).to eq(relation_mock)
160
+
161
+ relation
162
+ end
163
+ mock_association_filter_class = double("assocation filter class")
164
+
165
+ mock_association_filter_class.stub(:new).and_return(mock_association_filter)
166
+
167
+ mock_association_filter_class.stub(:_model).and_return(model)
168
+ allow(mock_association_filter_class).to receive(:define_method) do |name, &block|
169
+ allow(mock_association_filter).to receive(name, &block)
170
+ end
171
+
172
+ # We just need an object that supports and implements #define_method so the
173
+ # dynamic filter method can be created.
174
+ filter_class = Class.new do
175
+ end
176
+
177
+ allow(filter_class).to receive(:_model) { model }
178
+ allow(filter_class).to receive(:_associations) { {} }
179
+
180
+ dsl = FilterMe::Filter::ArelDSL.new(filter_class)
181
+ dsl.association(:test, {:filter_class => mock_association_filter_class})
182
+
183
+ filter_instance = filter_class.new
184
+ filter_instance.test(relation_mock, filters)
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe FilterMe do
4
+ let(:class_with_module) do
5
+ Class.new do
6
+ include FilterMe
7
+
8
+ attr_accessor params
9
+ end
10
+ end
11
+
12
+ it "should normalize field params correctly" do
13
+ username_field_params = {:matches => "sam%", :not_in_any => ["sam", "samsinite"]}
14
+ username_params_normalized = [
15
+ [:matches, ["sam%"]],
16
+ [:not_in_any, ["sam", "samsinite"]]
17
+ ]
18
+
19
+ expect(FilterMe.normalize_param(username_field_params)).to eq(username_params_normalized)
20
+ end
21
+
22
+ it "should normalize shallow association params correctly" do
23
+ account_association_params = {:type => {:eq => "admin"}}
24
+ account_params_normalized = [[:type, [
25
+ [:eq, ["admin"]]
26
+ ]]]
27
+
28
+ expect(FilterMe.normalize_param(account_association_params)).to eq(account_params_normalized)
29
+ end
30
+
31
+ it "should normalize deeply nested association params correctly" do
32
+ company_association_with_nested_job_association_params = {:job => {:name => {:matches => "%software%"}}}
33
+ company_params_normalized = [[:job, [
34
+ [:name, [
35
+ [:matches, ["%software%"]]
36
+ ]]
37
+ ]]]
38
+
39
+ expect(FilterMe.normalize_param(company_association_with_nested_job_association_params)).to eq(company_params_normalized)
40
+ end
41
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: filter_me
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sam Clopton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 3.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 3.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 3.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 3.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: combustion
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 0.5.1
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.5.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.13'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '2.13'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: 1.3.7
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: 1.3.7
83
+ description: This friendly library gives you ActiveRecord/Arel filtering in your Rails
84
+ app.
85
+ email:
86
+ - samsinite@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - .gitignore
92
+ - .travis.yml
93
+ - Gemfile
94
+ - LICENCE
95
+ - README.md
96
+ - Rakefile
97
+ - config.ru
98
+ - filter_me.gemspec
99
+ - lib/filter_me.rb
100
+ - lib/filter_me/filter.rb
101
+ - lib/filter_me/filter/arel_field_filter.rb
102
+ - lib/filter_me/filter/dsl.rb
103
+ - lib/filter_me/filter/field_validator.rb
104
+ - lib/filter_me/filterable.rb
105
+ - lib/filter_me/version.rb
106
+ - spec/acceptance/filters/accounts_filter_spec.rb
107
+ - spec/acceptance/filters/users_filter_spec.rb
108
+ - spec/internal/app/filters/accounts_filter.rb
109
+ - spec/internal/app/filters/users_filter.rb
110
+ - spec/internal/app/models/account.rb
111
+ - spec/internal/app/models/user.rb
112
+ - spec/internal/config/database.yml
113
+ - spec/internal/db/fixtures/accounts.rb
114
+ - spec/internal/db/fixtures/users.rb
115
+ - spec/internal/db/schema.rb
116
+ - spec/internal/log/.gitignore
117
+ - spec/spec_helper.rb
118
+ - spec/unit/active_record_filter_spec.rb
119
+ - spec/unit/arel_field_filter_spec.rb
120
+ - spec/unit/filter_arel_dsl_spec.rb
121
+ - spec/unit/filter_me_spec.rb
122
+ homepage: https://github.com/samsinite/filter_me
123
+ licenses:
124
+ - MIT
125
+ metadata: {}
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - '>='
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - '>='
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubyforge_project:
142
+ rubygems_version: 2.0.3
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: Filter Me
146
+ test_files: []