aub-record_filter 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.
- 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
|