numbered_relationships 0.2

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 Curtis Ekstrom
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # NumberedRelationships
2
+
3
+ This gem implements basic filtering of ActiveRecord models on the class and instance level based
4
+ on the amount of relationships belonging to the object or its associations.
5
+
6
+ ## Benefits/ Raison d'Etre
7
+ Defining a scope or model method that implements such basic having/count functionality on an
8
+ ActiveRecord model is trivial:
9
+
10
+ ```ruby
11
+ class Joker << ActiveRecord::Base
12
+ has_many :jokes
13
+
14
+ def self.with_at_least_n_jokes(n)
15
+ self.joins(:jokes).group("jokes.id HAVING count(joker_id) >= n")
16
+ end
17
+ end
18
+ ```
19
+ But what if we want to filter a jester's jokes as well? ActiveSupport defines concerns to make module inclusion really simple:
20
+ ```ruby
21
+ module Humor
22
+ extend ActiveSupport::Concern
23
+ included do
24
+ has_many :jokes
25
+ def with_at_least_n_jokes(n)
26
+ self.joins(:jokes).group("jokes.id HAVING count(joker_id) >= n")
27
+ end
28
+ end
29
+ end
30
+
31
+ class Joker < ActiveRecord::Base
32
+ include Humor
33
+ end
34
+ ```
35
+ Great! We can find the really humurous
36
+ ```ruby
37
+ Joker.with_at_least_n_jokes(200)
38
+ ```
39
+ But the following might break, as the hard-coded foreign key "joker_id" might not exist in the
40
+ jesters table.
41
+ ```ruby
42
+ class Jester < ActiveRecord::Base
43
+ include Humor
44
+ end
45
+ ```
46
+ The problem is quite clear: the scopes define queries using specific implementation details,
47
+ making reuse nearly impossible.
48
+
49
+ But there's also a larger issue here: jokes are only one possible association. If we want the
50
+ Jester model to include, say, music_instruments -- and to be able to filter on their amount --
51
+ we're back at square one, writing another module for each association.
52
+
53
+ This gem provides relief. Instead of defining modules with hard-coded method names and queries, it uses ActiveRecord's reflection capabilities to enable amount-based filtering on all models and their associations:
54
+ ```ruby
55
+ # Class-based filters
56
+ Joker.with_at_least(2, :music_instruments)
57
+ Joker.with_at_least(200, :jokes)
58
+ ```
59
+
60
+ ```ruby
61
+ # Instance-based association filters
62
+ @first_joker = Joker.find(1)
63
+ @first_joker.jokes.with_at_least(10, :tomatoes)
64
+ ```
65
+ These also call scoped() on all queries and return an instance of ActiveRecord::Relation, meaning that these methods are chainable:
66
+ ```ruby
67
+ @first_joker.performances.with_at_least(10, :dramatic_moments).where(:duration > 10)
68
+ ```
69
+ ## Installation
70
+
71
+ Add this line to your application's Gemfile:
72
+
73
+ gem 'numbered_relationships'
74
+
75
+ And then execute:
76
+
77
+ $ bundle
78
+
79
+ Or install it yourself as:
80
+
81
+ $ gem install numbered_relationships
82
+
83
+ ## Usage
84
+ This gem extends ActiveRecord, meaning the installation immediately offers the following methods:
85
+
86
+ ```ruby
87
+ Joker.with_at_least(1, :joke)
88
+ Joker.with_at_most(2, :jokes)
89
+ Joker.with_exactly(2, :jokes)
90
+ Joker.without(2, :jokes)
91
+ Joker.with_more_than(2, :jokes)
92
+ Joker.with_less_than(2, :jokes)
93
+
94
+ j = Joker.find(123)
95
+ j.jokes.with_at_least(1, :laugh)
96
+ j.jokes.with_at_most(2, :laughs)
97
+ j.jokes.with_exactly(2, :laughs)
98
+ j.jokes.without(11, :laughs)
99
+ j.jokes.with_more_than(18, :laughs)
100
+ j.jokes.with_less_than(2, :laughs)
101
+ ```
102
+ It's also possible to use class methods or scopes defined on the association class:
103
+ ```ruby
104
+ Joker.with_at_least(1, [:dirty], :joke)
105
+
106
+ ```
107
+ As long as the class method or scope :dirty is defined on Joker:
108
+ ```ruby
109
+ class Joke
110
+ def self.dirty
111
+ where(funny: true)
112
+ end
113
+ end
114
+ ```
115
+ this code will properly filter the results before attempting to group them. It outputs the following SQL:
116
+ ```sql
117
+ SELECT jesters.* FROM jesters
118
+ INNER JOIN jokes ON jokes.jester_id = jesters.id
119
+ WHERE jokes.funny = 't'
120
+ GROUP BY jesters.id
121
+ HAVING count(jokes.id) >= 1
122
+ ```
123
+ These methods, like all other class methods, are chainable, meaning the following would also work:
124
+ ```ruby
125
+ Joker.with_at_least(1, [:dirty, :insulting, :crying_on_the_floor], :joke)
126
+ ```
127
+
128
+ A call to a non-existent association will -- at least for the moment -- simply return:
129
+ ```ruby
130
+ []
131
+ ```
132
+
133
+ ## TODO:
134
+ 1. Improve exception handling.
135
+
136
+ ## Contributing
137
+
138
+ 1. Fork it
139
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
140
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
141
+ 4. Push to the branch (`git push origin my-new-feature`)
142
+ 5. Create new Pull Request
143
+
144
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/clekstro/numbered_relationships)![Travis CI Status](https://secure.travis-ci.org/clekstro/numbered_relationships.png)
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'NumberedRelationships'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
@@ -0,0 +1,53 @@
1
+ require_relative 'numbered_relationships/constructor_finder'
2
+
3
+ module NumberedRelationships
4
+ module AmountFilters
5
+ extend ActiveSupport::Concern
6
+
7
+ def with_at_least(n, filters=[], assoc)
8
+ find_related_objects(n, assoc, '>=', filters, self)
9
+ end
10
+
11
+ def with_at_most(n, filters=[], assoc)
12
+ find_related_objects(n, assoc, '<=', filters, self)
13
+ end
14
+
15
+ def with_exactly(n, filters=[], assoc)
16
+ find_related_objects(n, assoc, '=', filters, self)
17
+ end
18
+
19
+ def without(n, filters=[], assoc)
20
+ find_related_objects(n, assoc, '<>', filters, self)
21
+ end
22
+
23
+ def with_more_than(n, filters=[], assoc)
24
+ find_related_objects(n, assoc, '>', filters, self)
25
+ end
26
+
27
+ def with_less_than(n, filters=[], assoc)
28
+ find_related_objects(n, assoc, '<', filters, self)
29
+ end
30
+
31
+ private
32
+
33
+ def find_related_objects(n, assoc, operator, filters, klass)
34
+ reflection = determine_reflection(klass, assoc)
35
+ return klass.scoped unless reflection
36
+
37
+ ConstructorFinder::find({
38
+ n: n,
39
+ operator: operator,
40
+ filters: filters,
41
+ klass: klass,
42
+ reflection: reflection
43
+ }).construct
44
+ end
45
+
46
+ def determine_reflection(klass, assoc)
47
+ klass.reflect_on_association(assoc) || klass.reflect_on_association(assoc.to_s.tableize.to_sym)
48
+ end
49
+
50
+ end
51
+ end
52
+
53
+ ActiveRecord::Base.send :extend, NumberedRelationships::AmountFilters
@@ -0,0 +1,45 @@
1
+ module NumberedRelationships
2
+ class Constructor
3
+ def initialize(options={})
4
+ @n = options[:n]
5
+ @operator = options[:operator]
6
+ @filters = options[:filters]
7
+ @klass = options[:klass]
8
+ @reflection = options[:reflection]
9
+ end
10
+
11
+ def construct
12
+ return @klass.scoped unless @reflection
13
+ end
14
+
15
+ private
16
+
17
+ def table
18
+ @klass.name.tableize
19
+ end
20
+
21
+ def foreign_key
22
+ @reflection.foreign_key
23
+ end
24
+
25
+ def association
26
+ @reflection.name
27
+ end
28
+
29
+ def join_table
30
+ @klass.reflect_on_association(association.to_sym).options[:join_table]
31
+ end
32
+
33
+ def through_model
34
+ @reflection.options[:through]
35
+ end
36
+
37
+ def constantize_klass
38
+ association.to_s.classify.constantize
39
+ end
40
+
41
+ def chain_symbols(symbols)
42
+ symbols.map{ |s| s.to_s }.join('.')
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,19 @@
1
+ require_relative 'has_many'
2
+ require_relative 'has_many_through'
3
+ require_relative 'has_and_belongs_to_many'
4
+
5
+ module NumberedRelationships
6
+ module ConstructorFinder
7
+ def self.find(options={})
8
+ reflection = options[:reflection]
9
+ case reflection.macro
10
+ when :has_and_belongs_to_many
11
+ HasAndBelongsToManyConstructor.new(options)
12
+ when :has_many
13
+ thru = reflection.options[:through] if reflection
14
+ return HasManyConstructor.new(options) unless thru
15
+ HasManyThroughConstructor.new(options)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'constructor'
2
+
3
+ module NumberedRelationships
4
+ class HasAndBelongsToManyConstructor < Constructor
5
+ def construct
6
+ super
7
+ return construct_habtm_with_join_table if join_table
8
+ construct_habtm_without_join_table
9
+ end
10
+
11
+ def construct_habtm_with_join_table
12
+ @klass.joins(association)
13
+ .group("#{table}.id")
14
+ .having("count(#{join_table}.#{foreign_key}) #{@operator} #{@n}")
15
+ end
16
+
17
+ def construct_habtm_without_join_table
18
+ @klass.joins(association)
19
+ .group("#{table}.id")
20
+ .having("count(#{foreign_key}) #{@operator} #{@n}")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'constructor'
2
+
3
+ module NumberedRelationships
4
+ class HasManyConstructor < Constructor
5
+ def construct
6
+ super
7
+ return construct_has_many_filtered unless @filters.empty?
8
+ construct_has_many_unfiltered
9
+ end
10
+ def construct_has_many_unfiltered
11
+ @klass.joins(association)
12
+ .group("#{table}.id")
13
+ .having("count(#{association.to_s}.id) #{@operator} #{@n}")
14
+ end
15
+
16
+ def construct_has_many_filtered
17
+ @klass.joins(association)
18
+ .merge(eval("#{constantize_klass}.#{chain_symbols(@filters)}"))
19
+ .group("#{table}.id")
20
+ .having("count(#{association.to_s}.id) #{@operator} #{@n}")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'constructor'
2
+
3
+ module NumberedRelationships
4
+ class HasManyThroughConstructor < Constructor
5
+ def construct
6
+ super
7
+ @klass.joins(through_model, association)
8
+ .group("#{table}.id")
9
+ .having("count(#{association.to_s}.id) #{@operator} #{@n}")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module NumberedRelationships
2
+ VERSION = "0.2"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :numbered_relationships do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: numbered_relationships
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.2'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Curtis Ekstrom
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.7
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.7
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec-rails
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: factory_girl_rails
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: sqlite3
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Amount-based filtering for AR Models and Associations
79
+ email:
80
+ - ce@canvus.io
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - lib/numbered_relationships/constructor.rb
86
+ - lib/numbered_relationships/constructor_finder.rb
87
+ - lib/numbered_relationships/has_and_belongs_to_many.rb
88
+ - lib/numbered_relationships/has_many.rb
89
+ - lib/numbered_relationships/has_many_through.rb
90
+ - lib/numbered_relationships/version.rb
91
+ - lib/numbered_relationships.rb
92
+ - lib/tasks/numbered_relationships_tasks.rake
93
+ - MIT-LICENSE
94
+ - Rakefile
95
+ - README.md
96
+ homepage: https://github.com/clekstro/numbered_relationships
97
+ licenses: []
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ! '>='
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ! '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubyforge_project:
116
+ rubygems_version: 1.8.24
117
+ signing_key:
118
+ specification_version: 3
119
+ summary: Extend ActiveRecord models with amount-based filters for models and their
120
+ associations
121
+ test_files: []