activerecord_finder 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/README.md ADDED
@@ -0,0 +1,95 @@
1
+ Arel Finders
2
+ ============
3
+
4
+ The problem with ActiveRecord today is that there is no way to
5
+ construct queries outside "attribute = value" or using "OR" operator.
6
+ The solutions so far are not so easy to do, and usualy involves creating
7
+ fragments of SQL in string form.
8
+
9
+ But we have Arel. So, this must be solved, right?
10
+
11
+ Unfortunately, no. Arel's API is quite dificult to use, and its API is quite
12
+ inconsistent and changes over time, resulting in difficult code to mantain.
13
+
14
+ ## ActiveRecord::Finder
15
+
16
+ So, the solution was to create an object, an "adapter-kind" of object.
17
+ ActiveRecord::Finder is a kind of object that contains all attributes
18
+ from a table, and allows you to `OR` or `AND` queries.
19
+
20
+ ```ruby
21
+ finder = ActiveRecord::Finder.new(Arel::Table.new(:users))
22
+ query = finder.age > 10 | finder.name == "Foo"
23
+ query.arel.to_sql
24
+ #Returns: users.age > 10 OR users.name = 'Foo'
25
+ ```
26
+
27
+ But it is kind of tedious to do this everytime you want to search for users,
28
+ so you can use ActiveRecord::Base#restrict with a block to construct your queries.
29
+
30
+ ## ActiveRecord's Finders
31
+
32
+ Integrating with ActiveRecord, it is possible to use `restrict` to find records on a ActiveRecord object. The syntax is quite simple:
33
+
34
+ ```ruby
35
+ User.restrict { |f| f.age > 18 }
36
+ #or
37
+ User.restrict { age > 18 }
38
+ ```
39
+
40
+ ANDs work on the same way, too:
41
+
42
+ ```ruby
43
+ User.restrict { age > 18 & age < 20 }
44
+ ```
45
+
46
+ ### All finders, so far:
47
+
48
+ ```ruby
49
+ #WHERE NOT (users.age < 10)
50
+ User.restrict { !(age < 10) }
51
+
52
+ #Comparission
53
+ User.restrict { age == 20 }
54
+ User.restrict { age >= 20 }
55
+ User.restrict { age <= 20 }
56
+ User.restrict { age != 20 }
57
+ User.restrict { age > 20 }
58
+ User.restrict { age < 20 }
59
+
60
+ #IN
61
+ User.restrict { age.in?([10, 20, 30]) }
62
+
63
+ #LIKE
64
+ User.restrict { name =~ "Foo%" }
65
+ User.restrict { name !~ "Foo%" }
66
+
67
+ #AND, and OR
68
+ User.restrict { (name == "Foo") & (age < 20) }
69
+ User.restrict { (name == "Foo") | (age < 30) }
70
+ User.restrict do
71
+ ( (name == "Foo") & (age < 20) ) |
72
+ ( (name == "Bar") | (age > 20) )
73
+ end
74
+ ```
75
+
76
+ Plase note that because of operator precedence in Ruby, we must add a parenthesis over the comparission before using `&` or `|`.
77
+
78
+ ## Constructing Queries
79
+
80
+ To avoid "merging" ActiveRecord::Relation's, there is a specific syntax to construct a complex query on ActiveRecord::Finder.
81
+
82
+ ```ruby
83
+ def search(name_to_search, params = {})
84
+ query = User.new_finder { name == name_to_search }
85
+ query = query & User.new_finder { age > params[:age] } if params.include?(:age)
86
+ query = query | User.new_finder { age <= 10 } if params[:include_all_childs]
87
+ User.restrict(query)
88
+ end
89
+ ```
90
+
91
+ ### Using "joins"
92
+
93
+ To be simple, the `Finder` simply has the attributes from the table that it was created. So, to be able to find a record using attributes from one of the joined tables, it is possible to use `ActiveRecord::Base#merge`
94
+
95
+ ##
@@ -0,0 +1,34 @@
1
+ module ActiveRecordFinder
2
+ class Comparator
3
+ def initialize(finder, field)
4
+ @finder = finder
5
+ @field = field
6
+ end
7
+
8
+ def self.convert_to_arel(operation, arel_method)
9
+ define_method(operation) do |other|
10
+ arel_clause = @field.send(arel_method, other)
11
+ Finder.new(@finder.table, arel_clause)
12
+ end
13
+ end
14
+
15
+ def nil?
16
+ self == nil
17
+ end
18
+
19
+ def in?(other)
20
+ other = other.arel if other.respond_to?(:arel)
21
+ arel_clause = @field.in(other)
22
+ Finder.new(@finder.table, arel_clause)
23
+ end
24
+
25
+ convert_to_arel :==, :eq
26
+ convert_to_arel '!=', :not_eq
27
+ convert_to_arel :>, :gt
28
+ convert_to_arel :>=, :gteq
29
+ convert_to_arel :<, :lt
30
+ convert_to_arel :<=, :lteq
31
+ convert_to_arel :=~, :matches
32
+ convert_to_arel '!~', :does_not_match
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ module ActiveRecordFinder
2
+ class Finder
3
+ attr_reader :table, :arel
4
+
5
+ def initialize(arel_table, operation=nil)
6
+ @table = arel_table
7
+ @arel = operation
8
+ @table.columns.each { |a| define_attribute_method a }
9
+ end
10
+
11
+ def define_attribute_method(arel_attribute)
12
+ singleton_class.send :define_method, arel_attribute.name do
13
+ Comparator.new(self, arel_attribute)
14
+ end
15
+ end
16
+ private :define_attribute_method
17
+
18
+ def |(other)
19
+ other = other.arel if other.respond_to?(:arel)
20
+ Finder.new(table, arel.or(other))
21
+ end
22
+
23
+ def &(other)
24
+ other = other.arel if other.respond_to?(:arel)
25
+ Finder.new(table, arel.and(other))
26
+ end
27
+
28
+ def !@
29
+ Finder.new(table, Arel::Nodes::Not.new(arel))
30
+ end
31
+
32
+ undef_method :==
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ module ActiveRecordFinder
2
+ module Where
3
+ def restrict(finder = nil, &block)
4
+ raise ArgumentError, 'wrong number of arguments (0 for 1)' if block.nil? && finder.nil?
5
+ if block
6
+ where(new_finder(&block).arel)
7
+ else
8
+ where(finder.arel)
9
+ end
10
+ end
11
+ alias :condition :restrict
12
+
13
+ def new_finder(&block)
14
+ activerecord_finder = ActiveRecordFinder::Finder.new(arel_table)
15
+ if block.arity == 1
16
+ block.call(activerecord_finder)
17
+ else
18
+ activerecord_finder.instance_eval(&block)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "activerecord_finder/finder"
2
+ require_relative "activerecord_finder/comparator"
3
+ require_relative "activerecord_finder/where"
4
+
5
+ ActiveRecord::Base.extend ActiveRecordFinder::Where
@@ -0,0 +1,44 @@
1
+ require_relative "../helper"
2
+
3
+ describe ActiveRecordFinder::Comparator do
4
+ subject { ActiveRecordFinder::Comparator.new(finder, table[:name]) }
5
+ let(:finder) { ActiveRecordFinder::Finder.new(table) }
6
+ let(:table) { Person.arel_table }
7
+
8
+ it 'should be able to find by equality' do
9
+ (subject == 'foo').should be_a_kind_of(ActiveRecordFinder::Finder)
10
+ (subject == 'foo').arel.should be_equivalent_to(table[:name].eq('foo'))
11
+ end
12
+
13
+ it 'should be able to find by inequality' do
14
+ (subject != 'foo').arel.should be_equivalent_to(table[:name].not_eq('foo'))
15
+ end
16
+
17
+ it 'should be able to verify if an attribute is nil' do
18
+ (subject.nil?).arel.should be_equivalent_to(table[:name].eq(nil))
19
+ end
20
+
21
+ it 'should be able to find by greater than' do
22
+ (subject > 'foo').arel.should be_equivalent_to table[:name].gt('foo')
23
+ (subject >= 'foo').arel.should be_equivalent_to table[:name].gteq('foo')
24
+ end
25
+
26
+ it 'should be able to find by lower than' do
27
+ (subject < 'foo').arel.should be_equivalent_to table[:name].lt('foo')
28
+ (subject <= 'foo').arel.should be_equivalent_to table[:name].lteq('foo')
29
+ end
30
+
31
+ it 'finds with IN' do
32
+ subject.in?(['foo']).arel.should be_equivalent_to table[:name].in(['foo'])
33
+ end
34
+
35
+ it 'finds with subselects' do
36
+ result = subject.in?(Person.where(age: 10))
37
+ result.arel.to_sql.should match(/select/i)
38
+ end
39
+
40
+ it 'should be able to find with LIKE' do
41
+ (subject =~ 'foo').arel.should be_equivalent_to table[:name].matches('foo')
42
+ (subject !~ 'foo').arel.should be_equivalent_to table[:name].does_not_match('foo')
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ require_relative "../helper"
2
+
3
+ describe ActiveRecordFinder::Finder do
4
+ subject { ActiveRecordFinder::Finder.new(table) }
5
+ let(:table) { Person.arel_table }
6
+
7
+ it 'should return a empty arel if not being able to convert' do
8
+ subject.arel.should be_nil
9
+ end
10
+
11
+ it 'should not calculate equality' do
12
+ proc { subject == subject }.should raise_error(NoMethodError)
13
+ proc { subject != subject }.should raise_error(NoMethodError)
14
+ end
15
+
16
+ it 'should be able to "or" two conditions' do
17
+ c1 = table[:id].eq(1)
18
+ c2 = table[:name].eq(2)
19
+ ((subject.id == 1) | (subject.name == 2)).arel.should be_equivalent_to c1.or(c2)
20
+ end
21
+
22
+ it 'should be able to "and" two conditions' do
23
+ c1 = table[:id].eq(1)
24
+ c2 = table[:name].eq(2)
25
+ ((subject.id == 1) & (subject.name == 2)).arel.should be_equivalent_to c1.and(c2)
26
+ end
27
+
28
+ it 'should be able to negate the find' do
29
+ result = subject.name == 'foo'
30
+ (!result).arel.should be_equivalent_to Arel::Nodes::Not.new(table[:name].eq('foo'))
31
+ end
32
+ end
@@ -0,0 +1,38 @@
1
+ require_relative "../helper"
2
+
3
+ describe ActiveRecordFinder::Where do
4
+ let!(:bar) { Person.create! :name => 'Bar', :age => 17 }
5
+ let!(:seventeen) { Person.create! :name => 'Foo', :age => 17 }
6
+ let!(:eighteen) { Person.create! :name => 'Foo', :age => 18 }
7
+
8
+ after do
9
+ Person.delete_all
10
+ end
11
+
12
+ it 'finds using a block with one parameter' do
13
+ result = Person.restrict { |p| (p.name == 'Foo') & (p.age > 17) }
14
+ result.should == [eighteen]
15
+ end
16
+
17
+ it 'finds using a block with no parameters' do
18
+ result = Person.restrict { (name == 'Foo') & (age > 17) }
19
+ result.should == [eighteen]
20
+ end
21
+
22
+ it 'concatenates conditions' do
23
+ finder1 = Person.new_finder { age == 17 }
24
+ finder2 = Person.new_finder { name == "Foo" }
25
+ result = Person.restrict(finder1 & finder2)
26
+ result.should == [seventeen]
27
+ end
28
+
29
+ it 'concatenates arel conditions' do
30
+ pending
31
+ finder1 = Person.where(name: "Foo").arel
32
+ finder2 = Person.new_finder { age == 17 }
33
+ result = Person.where((finder2 & finder1).arel)
34
+ result.should == [seventeen]
35
+ result = Person.where((finder1 & finder2).arel)
36
+ result.should == [seventeen]
37
+ end
38
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord_finder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Maurício Szabo
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-07-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '3.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ description:
31
+ email: mauricio.szabo@gmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files:
35
+ - README.md
36
+ files:
37
+ - lib/activerecord_finder/where.rb
38
+ - lib/activerecord_finder/finder.rb
39
+ - lib/activerecord_finder/comparator.rb
40
+ - lib/activerecord_finder.rb
41
+ - README.md
42
+ - spec/activerecord_finder/finder_spec.rb
43
+ - spec/activerecord_finder/comparator_spec.rb
44
+ - spec/activerecord_finder/where_spec.rb
45
+ homepage: http://github.com/mauricioszabo/arel_operators
46
+ licenses: []
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubyforge_project:
65
+ rubygems_version: 1.8.25
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: Better finder syntax (|, &, >=, <=) for ActiveRecord.
69
+ test_files:
70
+ - spec/activerecord_finder/finder_spec.rb
71
+ - spec/activerecord_finder/comparator_spec.rb
72
+ - spec/activerecord_finder/where_spec.rb
73
+ has_rdoc: true