aub-record_filter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +24 -0
- data/VERSION.yml +4 -0
- data/lib/record_filter.rb +10 -0
- data/lib/record_filter/active_record.rb +24 -0
- data/lib/record_filter/conjunctions.rb +96 -0
- data/lib/record_filter/dsl.rb +6 -0
- data/lib/record_filter/dsl/conjunction.rb +47 -0
- data/lib/record_filter/dsl/conjunction_dsl.rb +40 -0
- data/lib/record_filter/dsl/dsl.rb +42 -0
- data/lib/record_filter/dsl/join.rb +12 -0
- data/lib/record_filter/dsl/limit.rb +12 -0
- data/lib/record_filter/dsl/order.rb +12 -0
- data/lib/record_filter/dsl/restriction.rb +20 -0
- data/lib/record_filter/filter.rb +46 -0
- data/lib/record_filter/join.rb +17 -0
- data/lib/record_filter/join_table.rb +0 -0
- data/lib/record_filter/order.rb +24 -0
- data/lib/record_filter/query.rb +20 -0
- data/lib/record_filter/restrictions.rb +69 -0
- data/lib/record_filter/table.rb +93 -0
- data/spec/implicit_join_spec.rb +191 -0
- data/spec/limits_and_ordering_spec.rb +164 -0
- data/spec/models/blog.rb +5 -0
- data/spec/models/comment.rb +5 -0
- data/spec/models/photo.rb +3 -0
- data/spec/models/post.rb +8 -0
- data/spec/models/tag.rb +3 -0
- data/spec/named_filter_spec.rb +160 -0
- data/spec/restrictions_spec.rb +77 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/test.db +0 -0
- metadata +93 -0
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
ENV['RUBYOPT'] = '-W1'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
FileList['tasks/**/*.rake'].each { |file| load file }
|
8
|
+
|
9
|
+
task :default => :spec
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gemspec|
|
14
|
+
gemspec.name = 'record_filter'
|
15
|
+
gemspec.summary = 'Pure-ruby criteria API for building complex queries in ActiveRecord'
|
16
|
+
gemspec.email = 'mat@patch.com'
|
17
|
+
gemspec.homepage = 'http://github.com/outoftime/record_filter/tree/master'
|
18
|
+
gemspec.description = 'Pure-ruby criteria API for building complex queries in ActiveRecord'
|
19
|
+
gemspec.authors = ['Mat Brown', 'Aubrey Holland']
|
20
|
+
end
|
21
|
+
rescue LoadError
|
22
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
23
|
+
end
|
24
|
+
|
data/VERSION.yml
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'activerecord', '~> 2.2'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
%w(active_record query table conjunctions restrictions filter join order dsl).each do |file|
|
6
|
+
require File.join(File.dirname(__FILE__), 'record_filter', file)
|
7
|
+
end
|
8
|
+
|
9
|
+
module RecordFilter
|
10
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
module ActiveRecordExtension
|
3
|
+
module ClassMethods
|
4
|
+
|
5
|
+
def filter(&block)
|
6
|
+
Filter.new(self, nil, nil, &block)
|
7
|
+
end
|
8
|
+
|
9
|
+
def named_filter(name, &block)
|
10
|
+
DSL::DSL::subclass(self).module_eval do
|
11
|
+
define_method(name, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
(class << self; self; end).instance_eval do
|
15
|
+
define_method(name.to_s) do |*args|
|
16
|
+
Filter.new(self, name, nil, *args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
ActiveRecord::Base.send(:extend, RecordFilter::ActiveRecordExtension::ClassMethods)
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
module Conjunctions
|
3
|
+
class Base
|
4
|
+
attr_reader :table_name, :limit, :offset
|
5
|
+
|
6
|
+
def self.create_from(dsl_conjunction, table)
|
7
|
+
result = case dsl_conjunction.type
|
8
|
+
when :any_of: AnyOf.new(table)
|
9
|
+
when :all_of: AllOf.new(table)
|
10
|
+
end
|
11
|
+
|
12
|
+
dsl_conjunction.steps.each do |step|
|
13
|
+
case step
|
14
|
+
when DSL::Restriction
|
15
|
+
result.add_restriction(step.column, step.operator, step.value, :negated => step.negated)
|
16
|
+
when DSL::Conjunction
|
17
|
+
result.add_conjunction(create_from(step, table))
|
18
|
+
when DSL::Join
|
19
|
+
join = result.add_join_on_association(step.association)
|
20
|
+
result.add_conjunction(create_from(step.conjunction, join.right_table))
|
21
|
+
when DSL::Limit
|
22
|
+
result.add_limit_and_offset(step.limit, step.offset)
|
23
|
+
when DSL::Order
|
24
|
+
result.add_order(step.column, step.direction)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(table, restrictions = nil, joins = nil)
|
31
|
+
@table = table
|
32
|
+
@table_name = table.table_alias
|
33
|
+
@restrictions = restrictions || []
|
34
|
+
@joins = joins || []
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_restriction(column_name, operator, value, options={})
|
38
|
+
restriction_class = "RecordFilter::Restrictions::#{operator.to_s.camelize}".constantize
|
39
|
+
restriction = restriction_class.new("#{@table_name}.#{column_name}", value, options)
|
40
|
+
self << restriction
|
41
|
+
restriction
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_conjunction(conjunction)
|
45
|
+
self << conjunction
|
46
|
+
conjunction
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_join_on_association(association_name)
|
50
|
+
@table.join_association(association_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_order(column, direction)
|
54
|
+
@table.order_column(column, direction)
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_limit_and_offset(limit, offset)
|
58
|
+
@limit, @offset = limit, offset
|
59
|
+
end
|
60
|
+
|
61
|
+
def <<(restriction)
|
62
|
+
@restrictions << restriction
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_conditions
|
66
|
+
if @restrictions.empty?
|
67
|
+
nil
|
68
|
+
elsif @restrictions.length == 1
|
69
|
+
@restrictions.first.to_conditions
|
70
|
+
else
|
71
|
+
@restrictions.map do |restriction|
|
72
|
+
conditions = restriction.to_conditions
|
73
|
+
conditions[0] = "(#{conditions[0]})"
|
74
|
+
conditions
|
75
|
+
end.inject do |conditions, new_conditions|
|
76
|
+
conditions.first << " #{conjunctor} #{new_conditions.shift}"
|
77
|
+
conditions.concat(new_conditions)
|
78
|
+
conditions
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class AnyOf < Base
|
85
|
+
def conjunctor
|
86
|
+
'OR'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class AllOf < Base
|
91
|
+
def conjunctor
|
92
|
+
'AND'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
module DSL
|
3
|
+
class Conjunction
|
4
|
+
|
5
|
+
attr_reader :type, :steps
|
6
|
+
|
7
|
+
DEFAULT_VALUE = Object.new
|
8
|
+
|
9
|
+
def initialize(type=:all_of)
|
10
|
+
@type, @steps = type, []
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_restriction(column, value, negated)
|
14
|
+
@steps << (restriction = Restriction.new(column, negated))
|
15
|
+
if value == DEFAULT_VALUE
|
16
|
+
return restriction
|
17
|
+
elsif value.nil?
|
18
|
+
restriction.is_null
|
19
|
+
else
|
20
|
+
restriction.equal_to(value)
|
21
|
+
end
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_conjunction(type, &block)
|
26
|
+
dsl = ConjunctionDSL.new(Conjunction.new(type))
|
27
|
+
dsl.instance_eval(&block) if block
|
28
|
+
@steps << dsl.conjunction
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_join(association, &block)
|
32
|
+
dsl = ConjunctionDSL.new
|
33
|
+
dsl.instance_eval(&block) if block
|
34
|
+
@steps << (join = Join.new(association, dsl.conjunction))
|
35
|
+
join
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_limit(limit, offset)
|
39
|
+
@steps << Limit.new(limit, offset)
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_order(column, direction)
|
43
|
+
@steps << Order.new(column, direction)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
module DSL
|
3
|
+
class ConjunctionDSL
|
4
|
+
|
5
|
+
attr_reader :conjunction
|
6
|
+
|
7
|
+
def initialize(conjunction=Conjunction.new(:all_of))
|
8
|
+
@conjunction = conjunction
|
9
|
+
end
|
10
|
+
|
11
|
+
# restriction
|
12
|
+
def with(column, value=Conjunction::DEFAULT_VALUE)
|
13
|
+
return @conjunction.add_restriction(column, value, false) # using return just to make it explicit
|
14
|
+
end
|
15
|
+
|
16
|
+
# restriction
|
17
|
+
def without(column, value=Conjunction::DEFAULT_VALUE)
|
18
|
+
return @conjunction.add_restriction(column, value, true) # using return just to make it explicit
|
19
|
+
end
|
20
|
+
|
21
|
+
# conjunction
|
22
|
+
def any_of(&block)
|
23
|
+
@conjunction.add_conjunction(:any_of, &block)
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# conjunction
|
28
|
+
def all_of(&block)
|
29
|
+
@conjunction.add_conjunction(:all_of, &block)
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
# join
|
34
|
+
def having(association, &block)
|
35
|
+
join = @conjunction.add_join(association, &block)
|
36
|
+
ConjunctionDSL.new(join.conjunction)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
module DSL
|
3
|
+
class DSL < ConjunctionDSL
|
4
|
+
|
5
|
+
SUBCLASSES = Hash.new do |h, k|
|
6
|
+
h[k] = Class.new(DSL)
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def create(clazz)
|
11
|
+
subclass(clazz).new
|
12
|
+
end
|
13
|
+
|
14
|
+
def subclass(clazz)
|
15
|
+
SUBCLASSES[clazz.name.to_sym]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# This method can take two forms:
|
20
|
+
# limit(offset, limit), or
|
21
|
+
# limit(limit)
|
22
|
+
def limit(offset_or_limit, limit=nil)
|
23
|
+
if limit
|
24
|
+
@conjunction.add_limit(limit, offset_or_limit)
|
25
|
+
else
|
26
|
+
@conjunction.add_limit(offset_or_limit, nil)
|
27
|
+
end
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# This method can take two forms, as shown below.
|
32
|
+
# order :permalink
|
33
|
+
# order :permalink, :desc
|
34
|
+
# order :photo => :path, :desc
|
35
|
+
# order :photo => { :comment => :id }, :asc
|
36
|
+
def order(column, direction=:asc)
|
37
|
+
@conjunction.add_order(column, direction)
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
module DSL
|
3
|
+
class Restriction
|
4
|
+
|
5
|
+
attr_reader :column, :negated, :operator, :value
|
6
|
+
|
7
|
+
def initialize(column, negated)
|
8
|
+
@column, @negated, @operator = column, negated, nil
|
9
|
+
end
|
10
|
+
|
11
|
+
[:equal_to, :is_null, :less_than, :greater_than, :in, :between].each do |operator|
|
12
|
+
define_method(operator) do |*args|
|
13
|
+
@value = args[0]
|
14
|
+
@operator = operator
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
class Filter
|
3
|
+
|
4
|
+
delegate :inspect, :to => :loaded_data
|
5
|
+
|
6
|
+
def initialize(clazz, named_filter, combine_conjunction, *args, &block)
|
7
|
+
@clazz = clazz
|
8
|
+
|
9
|
+
@dsl = dsl_for_named_filter(@clazz, named_filter)
|
10
|
+
@dsl.instance_eval(&block) if block
|
11
|
+
@dsl.send(named_filter, *args) if named_filter && @dsl.respond_to?(named_filter)
|
12
|
+
@dsl.conjunction.steps.unshift(combine_conjunction.steps).flatten! if combine_conjunction
|
13
|
+
end
|
14
|
+
|
15
|
+
def filter(&block)
|
16
|
+
Filter.new(@clazz, nil, @dsl.conjunction, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(method, *args, &block)
|
20
|
+
if @clazz.respond_to?(method) # UGLY, we need to only pass through things that are named filters.
|
21
|
+
Filter.new(@clazz, method, @dsl.conjunction, *args)
|
22
|
+
else
|
23
|
+
loaded_data.send(method, *args, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def loaded_data
|
30
|
+
@loaded_data ||= begin
|
31
|
+
query = Query.new(@clazz, @dsl.conjunction)
|
32
|
+
@clazz.scoped(query.to_find_params)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def dsl_for_named_filter(clazz, named_filter)
|
37
|
+
return DSL::DSL.create(clazz) if named_filter.blank?
|
38
|
+
while (clazz)
|
39
|
+
dsl = DSL::DSL::SUBCLASSES.has_key?(clazz.name.to_sym) ? DSL::DSL::SUBCLASSES[clazz.name.to_sym] : nil
|
40
|
+
return DSL::DSL.create(clazz) if dsl && dsl.instance_methods(false).include?(named_filter.to_s)
|
41
|
+
clazz = clazz.superclass
|
42
|
+
end
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
class Join
|
3
|
+
attr_reader :left_table, :right_table
|
4
|
+
|
5
|
+
def initialize(left_table, right_table, join_predicate)
|
6
|
+
@left_table, @right_table, @join_predicate =
|
7
|
+
left_table, right_table, join_predicate
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_sql
|
11
|
+
predicate_sql = @join_predicate.map do |left_column, right_column|
|
12
|
+
"#{@left_table.table_alias}.#{left_column} = #{@right_table.table_alias}.#{right_column}"
|
13
|
+
end * ' AND '
|
14
|
+
"INNER JOIN #{@right_table.table_name} AS #{@right_table.table_alias} ON #{predicate_sql}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
File without changes
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
class Order
|
3
|
+
attr_reader :column, :direction, :table
|
4
|
+
|
5
|
+
def initialize(column, direction, table)
|
6
|
+
@column, @direction, @table = column, direction, table
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_sql
|
10
|
+
dir = case @direction
|
11
|
+
when :asc then 'ASC'
|
12
|
+
when :desc then 'DESC'
|
13
|
+
end
|
14
|
+
|
15
|
+
table, column = @table, @column
|
16
|
+
while column.is_a?(Hash)
|
17
|
+
table = table.join_association(column.keys[0]).right_table
|
18
|
+
column = column.values[0]
|
19
|
+
end
|
20
|
+
|
21
|
+
"#{table.table_name}.#{column} #{dir}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|