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 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