auto_preload 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/.editorconfig +12 -0
- data/.github/workflows/codeql-analysis.yml +72 -0
- data/.github/workflows/gem-push.yml +44 -0
- data/.github/workflows/test.yml +18 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.rubocop.yml +16 -0
- data/Gemfile +17 -0
- data/LICENSE +21 -0
- data/README.md +116 -0
- data/Rakefile +12 -0
- data/auto_preload.gemspec +38 -0
- data/lib/auto_preload/active_record.rb +33 -0
- data/lib/auto_preload/adapters/active_record.rb +16 -0
- data/lib/auto_preload/adapters/serializer.rb +36 -0
- data/lib/auto_preload/adapters.rb +10 -0
- data/lib/auto_preload/config.rb +14 -0
- data/lib/auto_preload/resolver.rb +116 -0
- data/lib/auto_preload/version.rb +5 -0
- data/lib/auto_preload.rb +23 -0
- metadata +106 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 27b495a7c7242a5cf7b8adbd58d4bd5b94b517f6c4328d0cd25ce9dd33108438
|
4
|
+
data.tar.gz: dfa50abfe824c5a79ebfff08549e56e3fd9e84269a6f2bf4d7ac55885457ac99
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1e499cdcc1ec269098a702eb824aa3f3a36d730b64867f74b359f5595d80e57f617942ad0decabf717baf1de01df2649b1a644ee4b521ecf22f9b4664a069692
|
7
|
+
data.tar.gz: b13655e3b6423d98a2b6f26d6f91e7096be16c890a4a6449fda57e986959733d11d1f1a8cd5119dd4fcd9bff93f46d387a7bc16e4dd4b603754ab5162dbe2d8c
|
data/.editorconfig
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# For most projects, this workflow file will not need changing; you simply need
|
2
|
+
# to commit it to your repository.
|
3
|
+
#
|
4
|
+
# You may wish to alter this file to override the set of languages analyzed,
|
5
|
+
# or to provide custom queries or build logic.
|
6
|
+
#
|
7
|
+
# ******** NOTE ********
|
8
|
+
# We have attempted to detect the languages in your repository. Please check
|
9
|
+
# the `language` matrix defined below to confirm you have the correct set of
|
10
|
+
# supported CodeQL languages.
|
11
|
+
#
|
12
|
+
name: "CodeQL"
|
13
|
+
|
14
|
+
on:
|
15
|
+
push:
|
16
|
+
branches: [ master ]
|
17
|
+
pull_request:
|
18
|
+
# The branches below must be a subset of the branches above
|
19
|
+
branches: [ master ]
|
20
|
+
schedule:
|
21
|
+
- cron: '19 13 * * 1'
|
22
|
+
|
23
|
+
jobs:
|
24
|
+
analyze:
|
25
|
+
name: Analyze
|
26
|
+
runs-on: ubuntu-latest
|
27
|
+
permissions:
|
28
|
+
actions: read
|
29
|
+
contents: read
|
30
|
+
security-events: write
|
31
|
+
|
32
|
+
strategy:
|
33
|
+
fail-fast: false
|
34
|
+
matrix:
|
35
|
+
language: [ 'ruby' ]
|
36
|
+
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
37
|
+
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
38
|
+
|
39
|
+
steps:
|
40
|
+
- name: Checkout repository
|
41
|
+
uses: actions/checkout@v3
|
42
|
+
|
43
|
+
# Initializes the CodeQL tools for scanning.
|
44
|
+
- name: Initialize CodeQL
|
45
|
+
uses: github/codeql-action/init@v2
|
46
|
+
with:
|
47
|
+
languages: ${{ matrix.language }}
|
48
|
+
# If you wish to specify custom queries, you can do so here or in a config file.
|
49
|
+
# By default, queries listed here will override any specified in a config file.
|
50
|
+
# Prefix the list here with "+" to use these queries and those in the config file.
|
51
|
+
|
52
|
+
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
53
|
+
# queries: security-extended,security-and-quality
|
54
|
+
|
55
|
+
|
56
|
+
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
57
|
+
# If this step fails, then you should remove it and run the build manually (see below)
|
58
|
+
- name: Autobuild
|
59
|
+
uses: github/codeql-action/autobuild@v2
|
60
|
+
|
61
|
+
# ℹ️ Command-line programs to run using the OS shell.
|
62
|
+
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
63
|
+
|
64
|
+
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
65
|
+
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
66
|
+
|
67
|
+
# - run: |
|
68
|
+
# echo "Run, Build Application using script"
|
69
|
+
# ./location_of_script_within_repo/buildscript.sh
|
70
|
+
|
71
|
+
- name: Perform CodeQL Analysis
|
72
|
+
uses: github/codeql-action/analyze@v2
|
@@ -0,0 +1,44 @@
|
|
1
|
+
name: Ruby Gem
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
tags: [ '*' ]
|
6
|
+
|
7
|
+
jobs:
|
8
|
+
build:
|
9
|
+
name: Build + Publish
|
10
|
+
runs-on: ubuntu-latest
|
11
|
+
environment: production
|
12
|
+
permissions:
|
13
|
+
contents: read
|
14
|
+
packages: write
|
15
|
+
|
16
|
+
steps:
|
17
|
+
- uses: actions/checkout@v3
|
18
|
+
- name: Set up Ruby 3.0
|
19
|
+
uses: actions/setup-ruby@v1
|
20
|
+
with:
|
21
|
+
ruby-version: 3.0.x
|
22
|
+
|
23
|
+
- name: Publish to GPR
|
24
|
+
run: |
|
25
|
+
mkdir -p $HOME/.gem
|
26
|
+
touch $HOME/.gem/credentials
|
27
|
+
chmod 0600 $HOME/.gem/credentials
|
28
|
+
printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
29
|
+
gem build *.gemspec
|
30
|
+
gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
|
31
|
+
env:
|
32
|
+
GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}"
|
33
|
+
OWNER: ${{ github.repository_owner }}
|
34
|
+
|
35
|
+
- name: Publish to RubyGems
|
36
|
+
run: |
|
37
|
+
mkdir -p $HOME/.gem
|
38
|
+
touch $HOME/.gem/credentials
|
39
|
+
chmod 0600 $HOME/.gem/credentials
|
40
|
+
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
41
|
+
gem build *.gemspec
|
42
|
+
gem push *.gem
|
43
|
+
env:
|
44
|
+
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
|
@@ -0,0 +1,18 @@
|
|
1
|
+
name: Tests
|
2
|
+
on: [push, pull_request]
|
3
|
+
jobs:
|
4
|
+
test:
|
5
|
+
strategy:
|
6
|
+
fail-fast: false
|
7
|
+
matrix:
|
8
|
+
os: [ubuntu-latest, macos-latest]
|
9
|
+
# Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
|
10
|
+
ruby: [2.7, '3.0', 3.1, head, truffleruby]
|
11
|
+
runs-on: ${{ matrix.os }}
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@v2
|
14
|
+
- uses: ruby/setup-ruby@v1
|
15
|
+
with:
|
16
|
+
ruby-version: ${{ matrix.ruby }}
|
17
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
18
|
+
- run: bundle exec rspec
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.7
|
3
|
+
|
4
|
+
Style/StringLiterals:
|
5
|
+
Enabled: true
|
6
|
+
EnforcedStyle: double_quotes
|
7
|
+
|
8
|
+
Style/StringLiteralsInInterpolation:
|
9
|
+
Enabled: true
|
10
|
+
EnforcedStyle: double_quotes
|
11
|
+
|
12
|
+
Layout/LineLength:
|
13
|
+
Max: 120
|
14
|
+
|
15
|
+
Metrics/BlockLength:
|
16
|
+
Enabled: false
|
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in rspec_match_structure.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem "rake", "~> 13.0"
|
9
|
+
gem "sqlite3"
|
10
|
+
|
11
|
+
gem "rspec", "~> 3.0"
|
12
|
+
|
13
|
+
gem "rubocop", "~> 1.7"
|
14
|
+
|
15
|
+
gem "active_model_serializers", "~> 0.10"
|
16
|
+
gem "simplecov"
|
17
|
+
gem "yard"
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2022 Mònade
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+

|
2
|
+
[](https://badge.fury.io/rb/auto_preload)
|
3
|
+
|
4
|
+
# Auto Preload
|
5
|
+
|
6
|
+
A gem to parse and run `preload`/`includes`/`eager_load` on your model from a JSON::API include string.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add the gem to your Gemfile
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'auto_preload'
|
14
|
+
```
|
15
|
+
|
16
|
+
and run the `bundle install` command.
|
17
|
+
|
18
|
+
## The problem
|
19
|
+
JSON::API allows API consumers to pass a query parameter, called `include`, to manually select which model associations should be resolved and returned in the output JSON.
|
20
|
+
|
21
|
+
This means that in your controller, you may have a dilemma:
|
22
|
+
* If the consumer requests an association that is not preloaded, Rails will run [N+1 queries](https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations), slowing down the response
|
23
|
+
* You can't know, beforehand, which association may be requested by the consumer, since it's parametric
|
24
|
+
* You can just preload every possible association, but you'll end up making a lot of extra (redundant) queries in most cases.
|
25
|
+
|
26
|
+
This gem tries to fix this by parsing the `include` parameter and transforming it to a `preload`, `includes` or `eager_load` call in the model.
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
This gem adds to ActiveRecord classes a couple of utility methods that will help to preload associations.
|
30
|
+
|
31
|
+
To start using it, simply pass a [JSON::API include string](https://jsonapi.org/format/#fetching-includes) to the `auto_preload` class method of a model, and it will resolve it.
|
32
|
+
|
33
|
+
Here's an example:
|
34
|
+
```ruby
|
35
|
+
# Models declaration
|
36
|
+
class User < ApplicationRecord
|
37
|
+
has_many :articles
|
38
|
+
has_many :comments
|
39
|
+
end
|
40
|
+
|
41
|
+
class Comment < ApplicationRecord
|
42
|
+
belongs_to :user
|
43
|
+
end
|
44
|
+
|
45
|
+
class Article < ApplicationRecord
|
46
|
+
belongs_to :user
|
47
|
+
has_many :comments
|
48
|
+
end
|
49
|
+
|
50
|
+
# Now calling auto_preload on User
|
51
|
+
User.auto_preload('*') # Equivalent to preload(:articles, :comments)
|
52
|
+
User.auto_preload('articles.*') # Equivalent to preload(articles: [:user, :comments])
|
53
|
+
```
|
54
|
+
|
55
|
+
The same works also with `eager_load` and `includes`:
|
56
|
+
```ruby
|
57
|
+
User.auto_eager_load('*') # Equivalent to eager_load(:articles, :comments)
|
58
|
+
User.auto_includes('*') # Equivalent to includes(:articles, :comments)
|
59
|
+
```
|
60
|
+
|
61
|
+
### Caveats: the `**` resolver
|
62
|
+
You can also use the keyword `**`, however it may take you to a loop.
|
63
|
+
|
64
|
+
For instance in this case, it would raise an error:
|
65
|
+
```ruby
|
66
|
+
User.auto_preload('**') # Raises "Too many iterations reached (101 of 100)"
|
67
|
+
```
|
68
|
+
Since `User` resolves `:articles`, but `Article` declares `belongs_to :user`.
|
69
|
+
|
70
|
+
To solve this you can whitelist the associations you want to preload:
|
71
|
+
```ruby
|
72
|
+
class Article < ApplicationRecord
|
73
|
+
self.auto_preloadable = [:comments]
|
74
|
+
belongs_to :user
|
75
|
+
has_many :comments
|
76
|
+
end
|
77
|
+
|
78
|
+
class Comment < ApplicationRecord
|
79
|
+
self.auto_preloadable = []
|
80
|
+
belongs_to :user
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
Now you can safely use auto_preload:
|
85
|
+
```ruby
|
86
|
+
User.auto_preload('**') # Equivalent to preload(:comments, articles: :comments)
|
87
|
+
```
|
88
|
+
|
89
|
+
### Adapters
|
90
|
+
By default, the resolution of the expressions passed to `auto_preload` methods is resolved by the [ActiveRecord Adapter](https://github.com/monade/auto_preload/blob/master/lib/auto_preload/adapters/active_record.rb).
|
91
|
+
|
92
|
+
An Adapter is simply a class that, given a model, returns the list of the associations that can be preloaded.
|
93
|
+
|
94
|
+
The ActiveRecord Adapter uses `reflect_on_all_associations` to get this list.
|
95
|
+
|
96
|
+
In many circumstances, you don't want this. For instance, if you use `ActiveModelSerializers` gem, you want to resolve only associations that are declared in the serializer.
|
97
|
+
|
98
|
+
To do so, just change the default adapter using an initializer, in `config/initializers/auto_preload.rb`:
|
99
|
+
```ruby
|
100
|
+
AutoPreload.config.adapter = AutoPreload::Adapters::Serializer.new
|
101
|
+
```
|
102
|
+
|
103
|
+
Of course, you can also declare your custom Adapters, simply creating a class that implements the method `resolve_preloadables(model, options = {})` and returns a list of associations.
|
104
|
+
|
105
|
+
## License
|
106
|
+
|
107
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
108
|
+
|
109
|
+
About Monade
|
110
|
+
----------------
|
111
|
+
|
112
|
+

|
113
|
+
|
114
|
+
auto_preload is maintained by [mònade srl](https://monade.io/en/home-en/).
|
115
|
+
|
116
|
+
We <3 open source software. [Contact us](https://monade.io/en/contact-us/) for your next project!
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path("lib", __dir__)
|
4
|
+
|
5
|
+
require_relative "lib/auto_preload/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "auto_preload"
|
9
|
+
spec.version = AutoPreload::VERSION
|
10
|
+
spec.authors = ["Mònade"]
|
11
|
+
spec.email = ["team@monade.io"]
|
12
|
+
|
13
|
+
spec.summary = "A gem to run nested preloads/includes from string."
|
14
|
+
spec.description = "A gem to run nested preloads/includes from string."
|
15
|
+
spec.homepage = "https://github.com/monade/auto_preload"
|
16
|
+
spec.license = "MIT"
|
17
|
+
spec.required_ruby_version = ">= 2.7"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/monade/auto_preload/CHANGELOG.md"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
26
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
27
|
+
end
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
# Uncomment to register a new dependency of your gem
|
33
|
+
spec.add_dependency "activerecord", [">= 5", "< 8"]
|
34
|
+
spec.add_dependency "activesupport", [">= 5", "< 8"]
|
35
|
+
|
36
|
+
# For more information and examples about making a new gem, checkout our
|
37
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
38
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AutoPreload
|
4
|
+
module ActiveRecord
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
scope :auto_includes, lambda { |inclusions, options = {}|
|
9
|
+
if inclusions.present?
|
10
|
+
includes(*Resolver.new(options).resolve(self, inclusions))
|
11
|
+
else
|
12
|
+
self
|
13
|
+
end
|
14
|
+
}
|
15
|
+
scope :auto_preload, lambda { |inclusions, options = {}|
|
16
|
+
if inclusions.present?
|
17
|
+
preload(*Resolver.new(options).resolve(self, inclusions))
|
18
|
+
else
|
19
|
+
self
|
20
|
+
end
|
21
|
+
}
|
22
|
+
scope :auto_eager_load, lambda { |inclusions, options = {}|
|
23
|
+
if inclusions.present?
|
24
|
+
eager_load(*Resolver.new(options).resolve(self, inclusions))
|
25
|
+
else
|
26
|
+
self
|
27
|
+
end
|
28
|
+
}
|
29
|
+
|
30
|
+
class_attribute :auto_preloadable
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AutoPreload
|
4
|
+
module Adapters
|
5
|
+
# This class takes a model and finds all the preloadable associations.
|
6
|
+
class ActiveRecord
|
7
|
+
def resolve_preloadables(model, _options = {})
|
8
|
+
if model.auto_preloadable
|
9
|
+
model.auto_preloadable.map { |w| model.reflect_on_association(w) }.compact
|
10
|
+
else
|
11
|
+
model.reflect_on_all_associations
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_model_serializers"
|
4
|
+
|
5
|
+
module AutoPreload
|
6
|
+
module Adapters
|
7
|
+
# This class takes a model and finds all the preloadable associations.
|
8
|
+
class Serializer
|
9
|
+
def initialize
|
10
|
+
@fallback = ActiveRecord.new
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param model [ActiveRecord::Base] The model to find preloadable associations for.
|
14
|
+
# @return [Array<ActiveRecord::Reflection>] The preloadable associations.
|
15
|
+
def resolve_preloadables(model, options = {})
|
16
|
+
serializer = resolve_serializer(model, options)
|
17
|
+
preloadables = @fallback.resolve_preloadables(model)
|
18
|
+
return preloadables unless serializer
|
19
|
+
|
20
|
+
preloadables_map = preloadables.index_by(&:name)
|
21
|
+
|
22
|
+
serializer._reflections.map do |key, _|
|
23
|
+
preloadables_map[key]
|
24
|
+
end.compact
|
25
|
+
end
|
26
|
+
|
27
|
+
def resolve_serializer(model, options = {})
|
28
|
+
if options[:root]
|
29
|
+
options[:serializer] || ActiveModel::Serializer.serializer_for(model)
|
30
|
+
else
|
31
|
+
ActiveModel::Serializer.serializer_for(model)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AutoPreload
|
4
|
+
# This class handles the gem configurations.
|
5
|
+
class Config
|
6
|
+
# @attr_writr [AutoPreload::Adapters::ActiveRecord, AutoPreload::Adapters::Serializer] adapter The adapter to use.
|
7
|
+
attr_writer :adapter
|
8
|
+
|
9
|
+
# @return [AutoPreload::Adapters::ActiveRecord, AutoPreload::Adapters::Serializer] The adapter to use.
|
10
|
+
def adapter
|
11
|
+
@adapter ||= AutoPreload::Adapters::ActiveRecord.new
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AutoPreload
|
4
|
+
# This class parses a string in the format "articles,comments" and returns an array of symbols.
|
5
|
+
class Resolver
|
6
|
+
MAX_ITERATIONS = 100
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
@iterations = 0
|
10
|
+
@options = options
|
11
|
+
@max_iterations = options[:max_iterations] || MAX_ITERATIONS
|
12
|
+
@adapter = AutoPreload.config.adapter
|
13
|
+
end
|
14
|
+
|
15
|
+
# Resolves a string as an array of symbols or hashes.
|
16
|
+
#
|
17
|
+
# @param query [ActiveRecord::Base, ActiveRecord::Relation]
|
18
|
+
# @param inclusions [String, Array<String>]
|
19
|
+
# @return [Array<Symbol, Hash>]
|
20
|
+
def resolve(query, inclusions)
|
21
|
+
model = query.respond_to?(:klass) ? query.klass : query
|
22
|
+
|
23
|
+
format_output(run_resolve(model, inclusions, root: true))
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
# @param model [ActiveRecord::Base]
|
29
|
+
# @param inclusions [String, Array<String>]
|
30
|
+
# @return [Array<Symbol, Hash>]
|
31
|
+
def run_resolve(model, inclusions, root: false)
|
32
|
+
inclusions = inclusions.split(",") if inclusions.is_a? String
|
33
|
+
inclusions.flat_map { |item| parse_association(model, item, root: root) }
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param list [Array<Symbol, Hash>]
|
37
|
+
# @return [Array<Symbol, Hash>]
|
38
|
+
def format_output(list)
|
39
|
+
list = list.compact.uniq # .sort { |a, _b| a.is_a?(Hash) ? 1 : -1 }
|
40
|
+
symbols = list.select { |item| item.is_a? Symbol }
|
41
|
+
objects = merge(list.select { |item| item.is_a? Hash })
|
42
|
+
|
43
|
+
objects.present? ? (symbols << objects) : symbols
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param list [Array<Hash>]
|
47
|
+
# @return [Hash]
|
48
|
+
def merge(objects)
|
49
|
+
objects.reduce({}) do |result, object|
|
50
|
+
result.merge(object) do |_, old_value, new_value|
|
51
|
+
(old_value + new_value).uniq
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param model [ActiveRecord::Base]
|
57
|
+
# @param item [String, Symbol]
|
58
|
+
# @return [Symbol, Hash, Array<Hash>]
|
59
|
+
def parse_association(model, item, root: false)
|
60
|
+
if item == "*"
|
61
|
+
resolve_preloadables(model, root: root).map(&:name)
|
62
|
+
elsif item == "**"
|
63
|
+
recurse_associations(model, root: root)
|
64
|
+
elsif item.include?(".")
|
65
|
+
split_inclusions(model, item, root: root)
|
66
|
+
else
|
67
|
+
item = item.strip.underscore.to_sym
|
68
|
+
find_association(model, item, root: root) ? item : nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# @param model [ActiveRecord::Base]
|
73
|
+
# @return [Array<Hash>]
|
74
|
+
def recurse_associations(model, root: false)
|
75
|
+
increase_iterations_count!
|
76
|
+
|
77
|
+
associations = resolve_preloadables(model, root: root)
|
78
|
+
|
79
|
+
associations.map do |association|
|
80
|
+
resolved = resolve(association.klass, "**")
|
81
|
+
resolved.present? ? { association.name.to_sym => resolved } : association.name.to_sym
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param model [ActiveRecord::Base]
|
86
|
+
# @param inclusions [String]
|
87
|
+
# @return [Array<Hash>]
|
88
|
+
def split_inclusions(model, inclusions, root: false)
|
89
|
+
increase_iterations_count!
|
90
|
+
|
91
|
+
head, *tail = inclusions.split(".", 2)
|
92
|
+
head = head.strip.underscore.to_sym
|
93
|
+
child_model = find_association(model, head, root: root).klass
|
94
|
+
[{ head => resolve(child_model, tail[0]) }]
|
95
|
+
end
|
96
|
+
|
97
|
+
# @param model [ActiveRecord::Base]
|
98
|
+
# @return [Array<ActiveRecord::Reflection::AssociationReflection>]
|
99
|
+
def resolve_preloadables(model, root: false)
|
100
|
+
@adapter.resolve_preloadables(model, @options.merge(root: root))
|
101
|
+
end
|
102
|
+
|
103
|
+
# @param model [ActiveRecord::Base]
|
104
|
+
# @param name [Symbol]
|
105
|
+
# @return [nil, ActiveRecord::Reflection::AssociationReflection]
|
106
|
+
def find_association(model, name, root: false)
|
107
|
+
resolve_preloadables(model, root: root).find { |association| association.name == name.to_sym }
|
108
|
+
end
|
109
|
+
|
110
|
+
# @raise [RuntimeError]
|
111
|
+
def increase_iterations_count!
|
112
|
+
@iterations += 1
|
113
|
+
raise "Iterations limit reached (#{@iterations} of #{@max_iterations})" if @iterations > @max_iterations
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/auto_preload.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_record"
|
5
|
+
|
6
|
+
module AutoPreload
|
7
|
+
extend ActiveSupport::Autoload
|
8
|
+
|
9
|
+
autoload :ActiveRecord
|
10
|
+
autoload :Adapters
|
11
|
+
autoload :Resolver
|
12
|
+
autoload :Config
|
13
|
+
|
14
|
+
def self.configure
|
15
|
+
yield(config)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.config
|
19
|
+
@config ||= Config.new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
ActiveRecord::Base.include AutoPreload::ActiveRecord
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: auto_preload
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mònade
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-11-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '8'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '5'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '8'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: activesupport
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '5'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '8'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '5'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '8'
|
53
|
+
description: A gem to run nested preloads/includes from string.
|
54
|
+
email:
|
55
|
+
- team@monade.io
|
56
|
+
executables: []
|
57
|
+
extensions: []
|
58
|
+
extra_rdoc_files: []
|
59
|
+
files:
|
60
|
+
- ".editorconfig"
|
61
|
+
- ".github/workflows/codeql-analysis.yml"
|
62
|
+
- ".github/workflows/gem-push.yml"
|
63
|
+
- ".github/workflows/test.yml"
|
64
|
+
- ".gitignore"
|
65
|
+
- ".rspec"
|
66
|
+
- ".rubocop.yml"
|
67
|
+
- Gemfile
|
68
|
+
- LICENSE
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- auto_preload.gemspec
|
72
|
+
- lib/auto_preload.rb
|
73
|
+
- lib/auto_preload/active_record.rb
|
74
|
+
- lib/auto_preload/adapters.rb
|
75
|
+
- lib/auto_preload/adapters/active_record.rb
|
76
|
+
- lib/auto_preload/adapters/serializer.rb
|
77
|
+
- lib/auto_preload/config.rb
|
78
|
+
- lib/auto_preload/resolver.rb
|
79
|
+
- lib/auto_preload/version.rb
|
80
|
+
homepage: https://github.com/monade/auto_preload
|
81
|
+
licenses:
|
82
|
+
- MIT
|
83
|
+
metadata:
|
84
|
+
homepage_uri: https://github.com/monade/auto_preload
|
85
|
+
source_code_uri: https://github.com/monade/auto_preload
|
86
|
+
changelog_uri: https://github.com/monade/auto_preload/CHANGELOG.md
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '2.7'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
requirements: []
|
102
|
+
rubygems_version: 3.2.33
|
103
|
+
signing_key:
|
104
|
+
specification_version: 4
|
105
|
+
summary: A gem to run nested preloads/includes from string.
|
106
|
+
test_files: []
|