association_scope 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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +78 -0
- data/Rakefile +15 -0
- data/lib/association_scope.rb +29 -0
- data/lib/association_scope/errors/association_missing_error.rb +17 -0
- data/lib/association_scope/scope.rb +20 -0
- data/lib/association_scope/scope/belongs_to_reflection.rb +41 -0
- data/lib/association_scope/scope/has_and_belongs_to_many_reflection.rb +25 -0
- data/lib/association_scope/scope/has_many_reflection.rb +24 -0
- data/lib/association_scope/scope/has_one_reflection.rb +8 -0
- data/lib/association_scope/scope/through_reflection.rb +34 -0
- data/lib/association_scope/version.rb +5 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3cdd5ffca0653bd30600b32060a42de12505e41af7beae5a4faf750e95189925
|
4
|
+
data.tar.gz: ce11c4a40bf02a01ca03e7d06ababdda30bc7c01012cd7b91241a2101ec19d70
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8e380d107ae796069965a993e4ef4c375caea859450e018dffd0f0f53cea917696f7e4fcfd34f556d5b0e0d805e33ad4590cb1acdba115e76691f96884d43f8a
|
7
|
+
data.tar.gz: b54e0f129fb76f8815c19997e7fdfe3b130bd316c38d4085f566592bfa10d2b3b1b37ecc4a416faa35e8d3d7e681ed7e2849b588955b7c207b5e380ee9403301
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2021 datae
|
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,78 @@
|
|
1
|
+
[](https://github.com/datae95/association_scope/actions/workflows/rspec.yml)
|
2
|
+
[](https://github.com/testdouble/standard)
|
3
|
+
|
4
|
+
# AssociationScope
|
5
|
+
I always wondered, why there was no functionality to use associations not only on `ActiveRecord` objects but on scopes as well.
|
6
|
+
When I have
|
7
|
+
```ruby
|
8
|
+
current_user.topics # => #<ActiveRecord::Relation [...]>
|
9
|
+
```
|
10
|
+
why can't I use the same construction on a collection of users like
|
11
|
+
```ruby
|
12
|
+
current_user.friends.topics # => #<ActiveRecord::Relation [...]>
|
13
|
+
```
|
14
|
+
and retrieve the collection of topics, my friends posted?
|
15
|
+
Instead I wrote weird scopes like
|
16
|
+
```ruby
|
17
|
+
class Topic < ApplicationRecord
|
18
|
+
belongs_to :user
|
19
|
+
scope :of_users, -> (users) { joins(:user).where(users: users) }
|
20
|
+
end
|
21
|
+
```
|
22
|
+
over and over again across all of my models to write `Topic.of_users(current_user.friends)` when I wanted to write `current_user.friends.topics` instead.
|
23
|
+
And `belongs_to` is the easiest part.
|
24
|
+
|
25
|
+
When you have this problem, the AssociationScope gem is for you!
|
26
|
+
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
Add this line to your application's Gemfile:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
gem 'association_scope', git: 'https://github.com/datae95/association_scope', branch: :main
|
33
|
+
```
|
34
|
+
|
35
|
+
And then execute:
|
36
|
+
```bash
|
37
|
+
$ bundle
|
38
|
+
```
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
After installation you can use `acts_as_association_scope` in your models:
|
42
|
+
```ruby
|
43
|
+
class Topic < ApplicationRecord
|
44
|
+
belongs_to :user
|
45
|
+
acts_as_association_scope
|
46
|
+
end
|
47
|
+
```
|
48
|
+
Now you can use your associations as scopes and chain other scopes with them.
|
49
|
+
When you have the classes `User` with many `Topic`s and every `Topic` has many `Post`s with many `Comment`s and all of them use `acts_as_association_scope`, you can write
|
50
|
+
```ruby
|
51
|
+
User.first.topics.posts.comments
|
52
|
+
```
|
53
|
+
to retrieve all comments of all posts of all topics of your first user.
|
54
|
+
|
55
|
+
## Known Issues
|
56
|
+
* This gem works with `reflections`.
|
57
|
+
To make this work, the `acts_as_association_scope` call has to be below your association definitions.
|
58
|
+
```ruby
|
59
|
+
# won't work
|
60
|
+
class Topic
|
61
|
+
acts_as_association_scope
|
62
|
+
belongs_to :user
|
63
|
+
end
|
64
|
+
|
65
|
+
# works
|
66
|
+
class Topic
|
67
|
+
belongs_to :user
|
68
|
+
acts_as_association_scope
|
69
|
+
end
|
70
|
+
```
|
71
|
+
* Database views do not have a primary key.
|
72
|
+
To use `distinct` on rows, all values of this row must be of types other than json.
|
73
|
+
Workaround: Migrate JSON columns to JSONB.
|
74
|
+
* Error messages are not raised during application start, but on first instantiation, because of the order in which classes are loaded.
|
75
|
+
|
76
|
+
## Development
|
77
|
+
Clone the app and run `bundle`.
|
78
|
+
To use `rails console` you have to navigate to the dummy application `cd spec/dummy`.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
|
5
|
+
require "bundler/gem_tasks"
|
6
|
+
|
7
|
+
require "rake/testtask"
|
8
|
+
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << "test"
|
11
|
+
t.pattern = "test/**/*_test.rb"
|
12
|
+
t.verbose = false
|
13
|
+
end
|
14
|
+
|
15
|
+
task default: :test
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "association_scope/version"
|
4
|
+
require "association_scope/scope"
|
5
|
+
require "association_scope/scope/has_many_reflection"
|
6
|
+
require "association_scope/scope/has_one_reflection"
|
7
|
+
require "association_scope/scope/belongs_to_reflection"
|
8
|
+
require "association_scope/scope/has_and_belongs_to_many_reflection"
|
9
|
+
require "association_scope/scope/through_reflection"
|
10
|
+
|
11
|
+
require "association_scope/errors/association_missing_error"
|
12
|
+
|
13
|
+
module AssociationScope
|
14
|
+
end
|
15
|
+
|
16
|
+
module ActiveRecord
|
17
|
+
class Base
|
18
|
+
def self.acts_as_association_scope(only: reflections.keys, except: [])
|
19
|
+
# Apply given filters.
|
20
|
+
# Don't be picky about singular or plural.
|
21
|
+
raise ArgumentError, "Don't use :only and :except together!" unless only == reflections.keys || except == []
|
22
|
+
|
23
|
+
only = only.map { |o| o.to_s }
|
24
|
+
except = except.map { |e| e.to_s }
|
25
|
+
|
26
|
+
::AssociationScope::Scope.inject_scopes(self, only - except)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AssociationScope
|
4
|
+
class AssociationMissingError < StandardError
|
5
|
+
attr_accessor :missing_in,
|
6
|
+
:association
|
7
|
+
|
8
|
+
def initialize(missing_in: "Model", association: nil)
|
9
|
+
@missing_in = missing_in
|
10
|
+
@association = association
|
11
|
+
end
|
12
|
+
|
13
|
+
def message
|
14
|
+
"Association #{association} missing in #{missing_in}!"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AssociationScope
|
4
|
+
class Scope
|
5
|
+
attr_reader :model, :association
|
6
|
+
|
7
|
+
def initialize(model, association)
|
8
|
+
@model = model
|
9
|
+
@association = association
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.inject_scopes(model, reflections)
|
13
|
+
model.reflections.slice(*reflections).each do |association, details|
|
14
|
+
scope_type = details.class.to_s.split("::").last
|
15
|
+
|
16
|
+
"AssociationScope::Scope::#{scope_type}".constantize.new(model, association).apply
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AssociationScope
|
4
|
+
class Scope
|
5
|
+
class BelongsToReflection < Scope
|
6
|
+
def apply
|
7
|
+
association = @association
|
8
|
+
details = model.reflections[association]
|
9
|
+
class_name = details.options[:class_name]&.constantize || association.camelize.constantize
|
10
|
+
association_name = association.to_s.underscore.to_sym
|
11
|
+
foreign_key = details.options[:foreign_key]
|
12
|
+
own_table_name = class_name.to_s.pluralize.underscore
|
13
|
+
|
14
|
+
inverse_reflection = class_name.reflections[model.to_s.underscore.singularize] || class_name.reflections[model.to_s.underscore.pluralize]
|
15
|
+
case inverse_reflection&.source_reflection&.class&.to_s&.split("::")&.last
|
16
|
+
when "HasOneReflection"
|
17
|
+
table_name = model.to_s.underscore.to_sym
|
18
|
+
when "HasManyReflection"
|
19
|
+
table_name = model.to_s.underscore.pluralize.to_sym
|
20
|
+
else
|
21
|
+
raise AssociationMissingError.new missing_in: class_name, association: model.to_s.underscore.pluralize
|
22
|
+
end
|
23
|
+
|
24
|
+
model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
25
|
+
scope association.pluralize, -> do
|
26
|
+
if foreign_key.present?
|
27
|
+
class_name
|
28
|
+
.joins("JOIN #{table_name} ON #{table_name}.#{foreign_key} = #{own_table_name}.id")
|
29
|
+
else
|
30
|
+
class_name
|
31
|
+
.joins(table_name)
|
32
|
+
end
|
33
|
+
.where(table_name => { association_name =>
|
34
|
+
select("#{association_name}_id".to_sym) })
|
35
|
+
.distinct
|
36
|
+
end
|
37
|
+
RUBY
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AssociationScope
|
4
|
+
class Scope
|
5
|
+
class HasAndBelongsToManyReflection < Scope
|
6
|
+
def apply
|
7
|
+
association = @association.pluralize
|
8
|
+
details = model.reflections[association]
|
9
|
+
class_name = details.options[:class_name]&.constantize || association.singularize.camelize.constantize
|
10
|
+
table_name = model.to_s.underscore.pluralize.to_sym
|
11
|
+
|
12
|
+
model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
13
|
+
raise AssociationMissingError.new(missing_in: class_name, association: table_name) unless class_name.reflections.has_key?(table_name.to_s)
|
14
|
+
|
15
|
+
scope association, -> do
|
16
|
+
class_name
|
17
|
+
.joins(table_name)
|
18
|
+
.where(table_name => self)
|
19
|
+
.distinct
|
20
|
+
end
|
21
|
+
RUBY
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AssociationScope
|
4
|
+
class Scope
|
5
|
+
class HasManyReflection < Scope
|
6
|
+
def apply
|
7
|
+
details = model.reflections[@association]
|
8
|
+
class_name = details.options[:class_name]&.constantize || association.singularize.camelize.constantize
|
9
|
+
association = @association.pluralize
|
10
|
+
column_name = model.to_s.underscore
|
11
|
+
|
12
|
+
model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
13
|
+
raise AssociationMissingError.new(missing_in: class_name, association: column_name) unless class_name.reflections.has_key?(column_name)
|
14
|
+
|
15
|
+
scope association, -> do
|
16
|
+
class_name
|
17
|
+
.where(column_name => self)
|
18
|
+
.distinct
|
19
|
+
end
|
20
|
+
RUBY
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AssociationScope
|
4
|
+
class Scope
|
5
|
+
class ThroughReflection < Scope
|
6
|
+
def apply
|
7
|
+
details = model.reflections[@association]
|
8
|
+
association = @association
|
9
|
+
class_name = details.options[:class_name]&.constantize || association.singularize.camelize.constantize
|
10
|
+
|
11
|
+
inverse = details.options[:inverse_of]&.to_s || model.to_s.underscore
|
12
|
+
inverse_reflection = class_name.reflections[inverse.singularize] || class_name.reflections[inverse.pluralize]
|
13
|
+
|
14
|
+
first_join = inverse_reflection&.options&.fetch(:through, nil) || inverse_reflection&.options&.fetch(:source, nil)
|
15
|
+
|
16
|
+
reflection_type = inverse_reflection&.source_reflection&.class&.to_s&.split("::")&.last
|
17
|
+
second_join = if %w[HasOneReflection BelongsToReflection].include?(reflection_type)
|
18
|
+
model.to_s.underscore.to_sym
|
19
|
+
else
|
20
|
+
model.to_s.underscore.pluralize.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
24
|
+
raise AssociationMissingError.new missing_in: class_name, association: inverse unless inverse_reflection
|
25
|
+
scope association.pluralize, -> do
|
26
|
+
class_name
|
27
|
+
.joins(first_join => second_join)
|
28
|
+
.distinct
|
29
|
+
end
|
30
|
+
RUBY
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: association_scope
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- datae
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-08-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 6.1.4
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 6.1.4
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: standard
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.1.6
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.1.6
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: yard
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.9.26
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.9.26
|
55
|
+
description: AssociationScope adds useful scopes targeting Associations in ActiveRecord.
|
56
|
+
email:
|
57
|
+
- accounts@datae.de
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- MIT-LICENSE
|
63
|
+
- README.md
|
64
|
+
- Rakefile
|
65
|
+
- lib/association_scope.rb
|
66
|
+
- lib/association_scope/errors/association_missing_error.rb
|
67
|
+
- lib/association_scope/scope.rb
|
68
|
+
- lib/association_scope/scope/belongs_to_reflection.rb
|
69
|
+
- lib/association_scope/scope/has_and_belongs_to_many_reflection.rb
|
70
|
+
- lib/association_scope/scope/has_many_reflection.rb
|
71
|
+
- lib/association_scope/scope/has_one_reflection.rb
|
72
|
+
- lib/association_scope/scope/through_reflection.rb
|
73
|
+
- lib/association_scope/version.rb
|
74
|
+
homepage: https://github.com/datae95/association_scope
|
75
|
+
licenses:
|
76
|
+
- MIT
|
77
|
+
metadata:
|
78
|
+
homepage_uri: https://github.com/datae95/association_scope
|
79
|
+
source_code_uri: https://github.com/datae95/association_scope
|
80
|
+
changelog_uri: https://github.com/datae95/association_scope/changelog
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubygems_version: 3.1.4
|
97
|
+
signing_key:
|
98
|
+
specification_version: 4
|
99
|
+
summary: AssociationScope adds useful scopes targeting Associations in ActiveRecord.
|
100
|
+
test_files: []
|