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 +20 -0
- data/README.md +144 -0
- data/Rakefile +27 -0
- data/lib/numbered_relationships.rb +53 -0
- data/lib/numbered_relationships/constructor.rb +45 -0
- data/lib/numbered_relationships/constructor_finder.rb +19 -0
- data/lib/numbered_relationships/has_and_belongs_to_many.rb +23 -0
- data/lib/numbered_relationships/has_many.rb +23 -0
- data/lib/numbered_relationships/has_many_through.rb +12 -0
- data/lib/numbered_relationships/version.rb +3 -0
- data/lib/tasks/numbered_relationships_tasks.rake +4 -0
- metadata +121 -0
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
|
+
[](https://codeclimate.com/github/clekstro/numbered_relationships)
|
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
|
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: []
|