filter_me 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []