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 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
+ [![RSpec](https://github.com/datae95/association_scope/actions/workflows/rspec.yml/badge.svg)](https://github.com/datae95/association_scope/actions/workflows/rspec.yml)
2
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AssociationScope
4
+ class Scope
5
+ class HasOneReflection < HasManyReflection
6
+ end
7
+ end
8
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AssociationScope
4
+ VERSION = "0.1.0"
5
+ 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: []