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 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,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
@@ -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,6 @@
1
+ %w(conjunction conjunction_dsl dsl join limit order restriction).each { |file| require File.join(File.dirname(__FILE__), 'dsl', file) }
2
+
3
+ module RecordFilter
4
+ module DSL
5
+ end
6
+ 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,12 @@
1
+ module RecordFilter
2
+ module DSL
3
+ class Join
4
+
5
+ attr_reader :association, :conjunction
6
+
7
+ def initialize(association, conjunction)
8
+ @association, @conjunction = association, conjunction
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module RecordFilter
2
+ module DSL
3
+ class Limit
4
+
5
+ attr_reader :limit, :offset
6
+
7
+ def initialize(limit, offset)
8
+ @limit, @offset = limit, offset
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module RecordFilter
2
+ module DSL
3
+ class Order
4
+
5
+ attr_reader :column, :direction
6
+
7
+ def initialize(column, direction)
8
+ @column, @direction = column, direction
9
+ end
10
+ end
11
+ end
12
+ 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