numbered_relationships 0.2

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/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: []