tapioca 0.4.0 → 0.4.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +26 -1
- data/README.md +16 -0
- data/Rakefile +16 -4
- data/lib/tapioca.rb +6 -2
- data/lib/tapioca/cli.rb +25 -3
- data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +130 -0
- data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
- data/lib/tapioca/compilers/dsl/active_record_associations.rb +267 -0
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +404 -0
- data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
- data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +212 -0
- data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
- data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +168 -0
- data/lib/tapioca/compilers/dsl/active_resource.rb +140 -0
- data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +126 -0
- data/lib/tapioca/compilers/dsl/base.rb +165 -0
- data/lib/tapioca/compilers/dsl/frozen_record.rb +96 -0
- data/lib/tapioca/compilers/dsl/protobuf.rb +144 -0
- data/lib/tapioca/compilers/dsl/smart_properties.rb +173 -0
- data/lib/tapioca/compilers/dsl/state_machines.rb +378 -0
- data/lib/tapioca/compilers/dsl/url_helpers.rb +160 -0
- data/lib/tapioca/compilers/dsl_compiler.rb +121 -0
- data/lib/tapioca/compilers/requires_compiler.rb +67 -0
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +195 -32
- data/lib/tapioca/config.rb +11 -6
- data/lib/tapioca/config_builder.rb +19 -9
- data/lib/tapioca/constant_locator.rb +1 -0
- data/lib/tapioca/core_ext/class.rb +23 -0
- data/lib/tapioca/gemfile.rb +32 -9
- data/lib/tapioca/generator.rb +200 -24
- data/lib/tapioca/loader.rb +30 -9
- data/lib/tapioca/version.rb +1 -1
- metadata +31 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a6ca30be86eac3958354ca4204904993088bfeeef36c72d63b428ccc5ff3eff3
|
4
|
+
data.tar.gz: e218a8ed57c6eb6c46b00f423771fa1a4874a8f0a945d8f5683e849fe5ca9e91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39b2b67d35896d998f0f11dcdeda386e6e8312e124b12d8e5e252650417ba3c8a452aedfe014059064ca69e465d97089bfad55f1801d6667612902a4edc24903
|
7
|
+
data.tar.gz: 0fa4fe00a45d1f0e870f6d2cf985291ee1ed3f78d339c1f27be5653cff99490de31c6907abc462f1c1c7c197d7eb3eba63c2a90764c56a89fd607fd950c4a493
|
data/Gemfile
CHANGED
@@ -7,5 +7,30 @@ gemspec
|
|
7
7
|
gem 'rubocop-shopify', require: false
|
8
8
|
|
9
9
|
group(:deployment, :development) do
|
10
|
-
gem("rake"
|
10
|
+
gem("rake")
|
11
|
+
end
|
12
|
+
|
13
|
+
gem("bundler", "~> 1.17")
|
14
|
+
gem("yard", "~> 0.9.25")
|
15
|
+
gem("pry-byebug")
|
16
|
+
gem("minitest")
|
17
|
+
gem("minitest-hooks")
|
18
|
+
gem("minitest-fork_executor")
|
19
|
+
gem("minitest-reporters")
|
20
|
+
gem("sorbet")
|
21
|
+
|
22
|
+
group(:development, :test) do
|
23
|
+
gem("smart_properties", ">= 1.15.0", require: false)
|
24
|
+
gem("frozen_record", ">= 0.17", require: false)
|
25
|
+
gem("sprockets", "~> 3.7", require: false)
|
26
|
+
gem("rails", "~> 5.2", require: false)
|
27
|
+
gem("state_machines", "~> 0.5.0", require: false)
|
28
|
+
gem("activerecord-typedstore", "~> 1.3", require: false)
|
29
|
+
gem("sqlite3")
|
30
|
+
gem("identity_cache", "~> 1.0", require: false)
|
31
|
+
gem('cityhash', git: 'https://github.com/csfrancis/cityhash.git',
|
32
|
+
ref: '3cfc7d01f333c01811d5e834f1495eaa29f87c36', require: false)
|
33
|
+
gem("activemodel-serializers-xml", "~> 1.0", require: false)
|
34
|
+
gem("activeresource", "~> 5.1", require: false)
|
35
|
+
gem("google-protobuf", "~>3.12.0", require: false)
|
11
36
|
end
|
data/README.md
CHANGED
@@ -8,6 +8,8 @@ As yet, no gem exports type information in a consumable format and it would be a
|
|
8
8
|
|
9
9
|
When you run `tapioca sync` in a project, `tapioca` loads all the gems that are in your dependency list from the Gemfile into memory. It then performs runtime introspection on the loaded types to understand their structure and generates an appropriate RBI file for each gem with a versioned filename.
|
10
10
|
|
11
|
+
## Manual gem requires
|
12
|
+
|
11
13
|
For gems that have a normal default `require` and load all of their constants through such a require, everything works seamlessly. However, for gems that are marked as `require: false` in the Gemfile, or for gems that export optionally loaded types via different requires, where a single require does not load the whole gem code into memory, `tapioca` will not be able to load some of the types into memory and, thus, won't be able to generate complete RBIs for them. For this reason, we need to keep a small external file named `sorbet/tapioca/require.rb` that is executed after all the gems in the Gemfile have been required and before generation of gem RBIs have started. This file is responsible for adding the requires for additional files from gems, which are not covered by the default require.
|
12
14
|
|
13
15
|
For example, suppose you are using the class `BetterHtml::Parser` exported from the `better_html` gem. Just doing a `require "better_html"` (which is the default require) does not load that type:
|
@@ -31,6 +33,13 @@ In order to make sure that `tapioca` can reflect on that type, we need to add th
|
|
31
33
|
|
32
34
|
If you ever run into a case, where you add a gem or update the version of a gem and run `tapioca sync` but don't have some types you expect in the generated gem RBI files, you will need to make sure you have added the necessary requires to the `sorbet/tapioca/require.rb` file.
|
33
35
|
|
36
|
+
You can use the command `tapioca require` to auto-populate the `sorbet/tapioca/require.rb` file with all the requires found
|
37
|
+
in your application. Once the file generated, you should review it, remove all unnecessary requires and commit it.
|
38
|
+
|
39
|
+
## How does tapioca compare to "srb rbi gems" ?
|
40
|
+
|
41
|
+
[Please see the detailed answer on our wiki](https://github.com/Shopify/tapioca/wiki/How-does-tapioca-compare-to-%22srb-rbi-gems%22-%3F)
|
42
|
+
|
34
43
|
## Installation
|
35
44
|
|
36
45
|
Add this line to your application's `Gemfile`:
|
@@ -49,6 +58,7 @@ Commands:
|
|
49
58
|
tapioca generate [gem...] # generate RBIs from gems
|
50
59
|
tapioca help [COMMAND] # Describe available commands or one specific command
|
51
60
|
tapioca init # initializes folder structure
|
61
|
+
tapioca require # generate the list of files to be required by tapioca
|
52
62
|
tapioca sync # sync RBIs to Gemfile
|
53
63
|
tapioca todo # generate the list of unresolved constants
|
54
64
|
|
@@ -87,6 +97,12 @@ Command: `tapioca todo`
|
|
87
97
|
|
88
98
|
This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved constants as empty modules.
|
89
99
|
|
100
|
+
### Generate DSL RBI files
|
101
|
+
|
102
|
+
Command: `tapioca dsl [constant...]`
|
103
|
+
|
104
|
+
This will generate DSL RBIs for specified constants (or for all handled constants, if a constant name is not supplied). You can read about DSL RBI generators supplied by `tapioca` in [the manual](manual/generators.md).
|
105
|
+
|
90
106
|
### Flags
|
91
107
|
|
92
108
|
- `--prerequire [file]`: A file to be required before `Bundler.require` is called.
|
data/Rakefile
CHANGED
@@ -2,11 +2,23 @@
|
|
2
2
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "rake/testtask"
|
5
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
Rake.application.options.trace = false
|
8
|
+
|
9
|
+
Rake::TestTask.new do |t|
|
10
|
+
t.libs << "lib"
|
11
|
+
t.libs << "spec"
|
12
|
+
t.warning = false
|
13
|
+
t.test_files = FileList['spec/**/*_spec.rb']
|
14
|
+
end
|
15
|
+
|
16
|
+
task(:spec) do
|
17
|
+
begin
|
18
|
+
Rake::Task[:test].execute
|
19
|
+
rescue RuntimeError
|
20
|
+
exit(1)
|
21
|
+
end
|
10
22
|
end
|
11
23
|
|
12
24
|
task(default: :spec)
|
data/lib/tapioca.rb
CHANGED
@@ -4,10 +4,12 @@
|
|
4
4
|
require "sorbet-runtime"
|
5
5
|
|
6
6
|
module Tapioca
|
7
|
-
def self.silence_warnings
|
7
|
+
def self.silence_warnings(&blk)
|
8
8
|
original_verbosity = $VERBOSE
|
9
9
|
$VERBOSE = nil
|
10
|
-
|
10
|
+
Gem::DefaultUserInteraction.use_ui(Gem::SilentUI.new) do
|
11
|
+
blk.call
|
12
|
+
end
|
11
13
|
ensure
|
12
14
|
$VERBOSE = original_verbosity
|
13
15
|
end
|
@@ -36,8 +38,10 @@ require "tapioca/generator"
|
|
36
38
|
require "tapioca/cli"
|
37
39
|
require "tapioca/gemfile"
|
38
40
|
require "tapioca/compilers/sorbet"
|
41
|
+
require "tapioca/compilers/requires_compiler"
|
39
42
|
require "tapioca/compilers/symbol_table_compiler"
|
40
43
|
require "tapioca/compilers/symbol_table/symbol_generator"
|
41
44
|
require "tapioca/compilers/symbol_table/symbol_loader"
|
42
45
|
require "tapioca/compilers/todos_compiler"
|
46
|
+
require "tapioca/compilers/dsl_compiler"
|
43
47
|
require "tapioca/version"
|
data/lib/tapioca/cli.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
# typed:
|
2
|
+
# typed: true
|
3
3
|
|
4
4
|
require 'thor'
|
5
5
|
|
@@ -44,14 +44,21 @@ module Tapioca
|
|
44
44
|
end
|
45
45
|
create_file(Config::DEFAULT_POSTREQUIRE, skip: true) do
|
46
46
|
<<~CONTENT
|
47
|
-
# frozen_string_literal: true
|
48
47
|
# typed: false
|
48
|
+
# frozen_string_literal: true
|
49
49
|
|
50
50
|
# Add your extra requires here
|
51
51
|
CONTENT
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
+
desc "require", "generate the list of files to be required by tapioca"
|
56
|
+
def require
|
57
|
+
Tapioca.silence_warnings do
|
58
|
+
generator.build_requires
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
55
62
|
desc "todo", "generate the list of unresolved constants"
|
56
63
|
def todo
|
57
64
|
Tapioca.silence_warnings do
|
@@ -59,6 +66,18 @@ module Tapioca
|
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
69
|
+
desc "dsl [constant...]", "generate RBIs for dynamic methods"
|
70
|
+
option :generators,
|
71
|
+
type: :array,
|
72
|
+
aliases: ["--gen", "-g"],
|
73
|
+
banner: "generator [generator ...]",
|
74
|
+
desc: "Only run supplied DSL generators"
|
75
|
+
def dsl(*constants)
|
76
|
+
Tapioca.silence_warnings do
|
77
|
+
generator.build_dsl(constants)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
62
81
|
desc "generate [gem...]", "generate RBIs from gems"
|
63
82
|
def generate(*gems)
|
64
83
|
Tapioca.silence_warnings do
|
@@ -79,7 +98,10 @@ module Tapioca
|
|
79
98
|
end
|
80
99
|
|
81
100
|
def generator
|
82
|
-
|
101
|
+
current_command = T.must(current_command_chain.first)
|
102
|
+
@generator ||= Generator.new(
|
103
|
+
ConfigBuilder.from_options(current_command, options)
|
104
|
+
)
|
83
105
|
end
|
84
106
|
end
|
85
107
|
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "parlour"
|
5
|
+
|
6
|
+
begin
|
7
|
+
require "action_controller"
|
8
|
+
rescue LoadError
|
9
|
+
return
|
10
|
+
end
|
11
|
+
|
12
|
+
module Tapioca
|
13
|
+
module Compilers
|
14
|
+
module Dsl
|
15
|
+
# `Tapioca::Compilers::Dsl::ActionControllerHelpers` decorates RBI files for all
|
16
|
+
# subclasses of `::ActionController::Base`
|
17
|
+
# to add helper methods (see https://api.rubyonrails.org/classes/ActionController/Helpers.html).
|
18
|
+
#
|
19
|
+
# For example, with the following `MyHelper` module:
|
20
|
+
#
|
21
|
+
# ~~~rb
|
22
|
+
# module MyHelper
|
23
|
+
# def greet(user)
|
24
|
+
# # ...
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# def localized_time
|
28
|
+
# # ...
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
# ~~~
|
32
|
+
#
|
33
|
+
# and the following controller:
|
34
|
+
#
|
35
|
+
# ~~~rb
|
36
|
+
# class UserController < ActionController::Base
|
37
|
+
# helper MyHelper
|
38
|
+
# helper { def age(user) "99" end }
|
39
|
+
# helper_method :current_user_name
|
40
|
+
#
|
41
|
+
# def current_user_name
|
42
|
+
# # ...
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
# ~~~
|
46
|
+
#
|
47
|
+
# this generator will produce an RBI file `user_controller.rbi` with the following content:
|
48
|
+
#
|
49
|
+
# ~~~rbi
|
50
|
+
# # user_controller.rbi
|
51
|
+
# # typed: strong
|
52
|
+
# class UserController
|
53
|
+
# sig { returns(UserController::HelperProxy) }
|
54
|
+
# def helpers; end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# module UserController::HelperMethods
|
58
|
+
# include MyHelper
|
59
|
+
#
|
60
|
+
# sig { params(user: T.untyped).returns(T.untyped) }
|
61
|
+
# def age(user); end
|
62
|
+
#
|
63
|
+
# sig { returns(T.untyped) }
|
64
|
+
# def current_user_name; end
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# class UserController::HelperProxy < ::ActionView::Base
|
68
|
+
# include UserController::HelperMethods
|
69
|
+
# end
|
70
|
+
# ~~~
|
71
|
+
class ActionControllerHelpers < Base
|
72
|
+
extend T::Sig
|
73
|
+
|
74
|
+
sig do
|
75
|
+
override
|
76
|
+
.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::ActionController::Base))
|
77
|
+
.void
|
78
|
+
end
|
79
|
+
def decorate(root, constant)
|
80
|
+
helper_proxy_name = "#{constant}::HelperProxy"
|
81
|
+
helper_methods_name = "#{constant}::HelperMethods"
|
82
|
+
proxied_helper_methods = constant._helper_methods.map(&:to_s).map(&:to_sym)
|
83
|
+
|
84
|
+
# Create helper method module
|
85
|
+
root.create_module(helper_methods_name) do |helper_methods|
|
86
|
+
helpers_module = constant._helpers
|
87
|
+
|
88
|
+
gather_includes(helpers_module).each do |ancestor|
|
89
|
+
helper_methods.create_include(ancestor)
|
90
|
+
end
|
91
|
+
|
92
|
+
helpers_module.instance_methods(false).each do |method_name|
|
93
|
+
method = if proxied_helper_methods.include?(method_name)
|
94
|
+
constant.instance_method(method_name)
|
95
|
+
else
|
96
|
+
helpers_module.instance_method(method_name)
|
97
|
+
end
|
98
|
+
create_method_from_def(helper_methods, method)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Create helper proxy class
|
103
|
+
root.create_class(helper_proxy_name, superclass: "::ActionView::Base") do |proxy|
|
104
|
+
proxy.create_include(helper_methods_name)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Define the helpers method
|
108
|
+
root.path(constant) do |controller|
|
109
|
+
create_method(controller, 'helpers', return_type: helper_proxy_name)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
sig { override.returns(T::Enumerable[Module]) }
|
114
|
+
def gather_constants
|
115
|
+
::ActionController::Base.descendants.reject(&:abstract?)
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
sig { params(mod: Module).returns(T::Array[String]) }
|
121
|
+
def gather_includes(mod)
|
122
|
+
mod.ancestors
|
123
|
+
.reject { |ancestor| ancestor.is_a?(Class) || ancestor == mod || ancestor.name.nil? }
|
124
|
+
.map { |ancestor| T.must(ancestor.name) }
|
125
|
+
.reverse
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "parlour"
|
5
|
+
|
6
|
+
begin
|
7
|
+
require "action_mailer"
|
8
|
+
rescue LoadError
|
9
|
+
return
|
10
|
+
end
|
11
|
+
|
12
|
+
module Tapioca
|
13
|
+
module Compilers
|
14
|
+
module Dsl
|
15
|
+
# `Tapioca::Compilers::Dsl::ActionMailer` generates RBI files for subclasses of `ActionMailer::Base`
|
16
|
+
# (see https://api.rubyonrails.org/classes/ActionMailer/Base.html).
|
17
|
+
#
|
18
|
+
# For example, with the following `ActionMailer` subclass:
|
19
|
+
#
|
20
|
+
# ~~~rb
|
21
|
+
# class NotifierMailer < ActionMailer::Base
|
22
|
+
# def notify_customer(customer_id)
|
23
|
+
# # ...
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# ~~~
|
27
|
+
#
|
28
|
+
# this generator will produce the RBI file `notifier_mailer.rbi` with the following content:
|
29
|
+
#
|
30
|
+
# ~~~rbi
|
31
|
+
# # notifier_mailer.rbi
|
32
|
+
# # typed: true
|
33
|
+
# class NotifierMailer
|
34
|
+
# sig { params(customer_id: T.untyped).returns(::ActionMailer::MessageDelivery) }
|
35
|
+
# def self.notify_customer(customer_id); end
|
36
|
+
# end
|
37
|
+
# ~~~
|
38
|
+
class ActionMailer < Base
|
39
|
+
extend T::Sig
|
40
|
+
|
41
|
+
sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::ActionMailer::Base)).void }
|
42
|
+
def decorate(root, constant)
|
43
|
+
root.path(constant) do |k|
|
44
|
+
constant.action_methods.to_a.each do |mailer_method|
|
45
|
+
method_def = constant.instance_method(mailer_method)
|
46
|
+
parameters = compile_method_parameters_to_parlour(method_def)
|
47
|
+
create_method(
|
48
|
+
k,
|
49
|
+
mailer_method,
|
50
|
+
parameters: parameters,
|
51
|
+
return_type: '::ActionMailer::MessageDelivery',
|
52
|
+
class_method: true
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
sig { override.returns(T::Enumerable[Module]) }
|
59
|
+
def gather_constants
|
60
|
+
::ActionMailer::Base.descendants.reject(&:abstract?)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,267 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "parlour"
|
5
|
+
|
6
|
+
begin
|
7
|
+
require "active_record"
|
8
|
+
rescue LoadError
|
9
|
+
return
|
10
|
+
end
|
11
|
+
|
12
|
+
module Tapioca
|
13
|
+
module Compilers
|
14
|
+
module Dsl
|
15
|
+
# `Tapioca::Compilers::Dsl::ActiveRecordAssociations` refines RBI files for subclasses of `ActiveRecord::Base`
|
16
|
+
# (see https://api.rubyonrails.org/classes/ActiveRecord/Base.html). This generator is only
|
17
|
+
# responsible for defining the methods that would be created for the association that
|
18
|
+
# are defined in the Active Record model.
|
19
|
+
#
|
20
|
+
# For example, with the following model class:
|
21
|
+
#
|
22
|
+
# ~~~rb
|
23
|
+
# class Post < ActiveRecord::Base
|
24
|
+
# belongs_to :category
|
25
|
+
# has_many :comments
|
26
|
+
# has_one :author, class_name: "User"
|
27
|
+
# end
|
28
|
+
# ~~~
|
29
|
+
#
|
30
|
+
# this generator will produce the following methods in the RBI file
|
31
|
+
# `post.rbi`:
|
32
|
+
#
|
33
|
+
# ~~~rbi
|
34
|
+
# # post.rbi
|
35
|
+
# # typed: true
|
36
|
+
#
|
37
|
+
# class Post
|
38
|
+
# include Post::GeneratedAssociationMethods
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# module Post::GeneratedAssociationMethods
|
42
|
+
# sig { returns(T.nilable(::User)) }
|
43
|
+
# def author; end
|
44
|
+
#
|
45
|
+
# sig { params(value: T.nilable(::User)).void }
|
46
|
+
# def author=(value); end
|
47
|
+
#
|
48
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::User)) }
|
49
|
+
# def build_author(*args, &blk); end
|
50
|
+
#
|
51
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::Category)) }
|
52
|
+
# def build_category(*args, &blk); end
|
53
|
+
#
|
54
|
+
# sig { returns(T.nilable(::Category)) }
|
55
|
+
# def category; end
|
56
|
+
#
|
57
|
+
# sig { params(value: T.nilable(::Category)).void }
|
58
|
+
# def category=(value); end
|
59
|
+
#
|
60
|
+
# sig { returns(T::Array[T.untyped]) }
|
61
|
+
# def comment_ids; end
|
62
|
+
#
|
63
|
+
# sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
|
64
|
+
# def comment_ids=(ids); end
|
65
|
+
#
|
66
|
+
# sig { returns(::ActiveRecord::Associations::CollectionProxy[Comment]) }
|
67
|
+
# def comments; end
|
68
|
+
#
|
69
|
+
# sig { params(value: T::Enumerable[::Comment]).void }
|
70
|
+
# def comments=(value); end
|
71
|
+
#
|
72
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::User)) }
|
73
|
+
# def create_author(*args, &blk); end
|
74
|
+
#
|
75
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::User)) }
|
76
|
+
# def create_author!(*args, &blk); end
|
77
|
+
#
|
78
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::Category)) }
|
79
|
+
# def create_category(*args, &blk); end
|
80
|
+
#
|
81
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::Category)) }
|
82
|
+
# def create_category!(*args, &blk); end
|
83
|
+
#
|
84
|
+
# sig { returns(T.nilable(::User)) }
|
85
|
+
# def reload_author; end
|
86
|
+
#
|
87
|
+
# sig { returns(T.nilable(::Category)) }
|
88
|
+
# def reload_category; end
|
89
|
+
# end
|
90
|
+
# ~~~
|
91
|
+
class ActiveRecordAssociations < Base
|
92
|
+
extend T::Sig
|
93
|
+
|
94
|
+
ReflectionType = T.type_alias do
|
95
|
+
T.any(::ActiveRecord::Reflection::ThroughReflection, ::ActiveRecord::Reflection::AssociationReflection)
|
96
|
+
end
|
97
|
+
|
98
|
+
sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(ActiveRecord::Base)).void }
|
99
|
+
def decorate(root, constant)
|
100
|
+
return if constant.reflections.empty?
|
101
|
+
|
102
|
+
module_name = "#{constant}::GeneratedAssociationMethods"
|
103
|
+
root.create_module(module_name) do |mod|
|
104
|
+
constant.reflections.each do |association_name, reflection|
|
105
|
+
if reflection.collection?
|
106
|
+
populate_collection_assoc_getter_setter(mod, constant, association_name, reflection)
|
107
|
+
else
|
108
|
+
populate_single_assoc_getter_setter(mod, constant, association_name, reflection)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
root.path(constant) do |klass|
|
114
|
+
klass.create_include(module_name)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
sig { override.returns(T::Enumerable[Module]) }
|
119
|
+
def gather_constants
|
120
|
+
ActiveRecord::Base.descendants.reject(&:abstract_class?)
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
sig do
|
126
|
+
params(
|
127
|
+
klass: Parlour::RbiGenerator::Namespace,
|
128
|
+
constant: T.class_of(ActiveRecord::Base),
|
129
|
+
association_name: T.any(String, Symbol),
|
130
|
+
reflection: ReflectionType
|
131
|
+
).void
|
132
|
+
end
|
133
|
+
def populate_single_assoc_getter_setter(klass, constant, association_name, reflection)
|
134
|
+
association_class = type_for(constant, reflection)
|
135
|
+
association_type = "T.nilable(#{association_class})"
|
136
|
+
|
137
|
+
create_method(
|
138
|
+
klass,
|
139
|
+
association_name.to_s,
|
140
|
+
return_type: association_type,
|
141
|
+
)
|
142
|
+
create_method(
|
143
|
+
klass,
|
144
|
+
"#{association_name}=",
|
145
|
+
parameters: [
|
146
|
+
Parlour::RbiGenerator::Parameter.new("value", type: association_type),
|
147
|
+
],
|
148
|
+
return_type: nil
|
149
|
+
)
|
150
|
+
create_method(
|
151
|
+
klass,
|
152
|
+
"reload_#{association_name}",
|
153
|
+
return_type: association_type,
|
154
|
+
)
|
155
|
+
if reflection.constructable?
|
156
|
+
create_method(
|
157
|
+
klass,
|
158
|
+
"build_#{association_name}",
|
159
|
+
parameters: [
|
160
|
+
Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
|
161
|
+
Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
|
162
|
+
],
|
163
|
+
return_type: association_type
|
164
|
+
)
|
165
|
+
create_method(
|
166
|
+
klass,
|
167
|
+
"create_#{association_name}",
|
168
|
+
parameters: [
|
169
|
+
Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
|
170
|
+
Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
|
171
|
+
],
|
172
|
+
return_type: association_type
|
173
|
+
)
|
174
|
+
create_method(
|
175
|
+
klass,
|
176
|
+
"create_#{association_name}!",
|
177
|
+
parameters: [
|
178
|
+
Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
|
179
|
+
Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
|
180
|
+
],
|
181
|
+
return_type: association_type
|
182
|
+
)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
sig do
|
187
|
+
params(
|
188
|
+
klass: Parlour::RbiGenerator::Namespace,
|
189
|
+
constant: T.class_of(ActiveRecord::Base),
|
190
|
+
association_name: T.any(String, Symbol),
|
191
|
+
reflection: ReflectionType
|
192
|
+
).void
|
193
|
+
end
|
194
|
+
def populate_collection_assoc_getter_setter(klass, constant, association_name, reflection)
|
195
|
+
association_class = type_for(constant, reflection)
|
196
|
+
relation_class = relation_type_for(constant, reflection)
|
197
|
+
|
198
|
+
create_method(
|
199
|
+
klass,
|
200
|
+
association_name.to_s,
|
201
|
+
return_type: relation_class,
|
202
|
+
)
|
203
|
+
create_method(
|
204
|
+
klass,
|
205
|
+
"#{association_name}=",
|
206
|
+
parameters: [
|
207
|
+
Parlour::RbiGenerator::Parameter.new("value", type: "T::Enumerable[#{association_class}]"),
|
208
|
+
],
|
209
|
+
return_type: nil,
|
210
|
+
)
|
211
|
+
create_method(
|
212
|
+
klass,
|
213
|
+
"#{association_name.to_s.singularize}_ids",
|
214
|
+
return_type: "T::Array[T.untyped]"
|
215
|
+
)
|
216
|
+
create_method(
|
217
|
+
klass,
|
218
|
+
"#{association_name.to_s.singularize}_ids=",
|
219
|
+
parameters: [
|
220
|
+
Parlour::RbiGenerator::Parameter.new("ids", type: "T::Array[T.untyped]"),
|
221
|
+
],
|
222
|
+
return_type: "T::Array[T.untyped]"
|
223
|
+
)
|
224
|
+
end
|
225
|
+
|
226
|
+
sig do
|
227
|
+
params(
|
228
|
+
constant: T.class_of(ActiveRecord::Base),
|
229
|
+
reflection: ReflectionType
|
230
|
+
).returns(String)
|
231
|
+
end
|
232
|
+
def type_for(constant, reflection)
|
233
|
+
return "T.untyped" if !constant.table_exists? || polymorphic_association?(reflection)
|
234
|
+
|
235
|
+
"::#{reflection.klass.name}"
|
236
|
+
end
|
237
|
+
|
238
|
+
sig do
|
239
|
+
params(
|
240
|
+
constant: T.class_of(ActiveRecord::Base),
|
241
|
+
reflection: ReflectionType
|
242
|
+
).returns(String)
|
243
|
+
end
|
244
|
+
def relation_type_for(constant, reflection)
|
245
|
+
"ActiveRecord::Associations::CollectionProxy" if !constant.table_exists? ||
|
246
|
+
polymorphic_association?(reflection)
|
247
|
+
|
248
|
+
# Change to: "::#{reflection.klass.name}::ActiveRecord_Associations_CollectionProxy"
|
249
|
+
"::ActiveRecord::Associations::CollectionProxy[#{reflection.klass.name}]"
|
250
|
+
end
|
251
|
+
|
252
|
+
sig do
|
253
|
+
params(
|
254
|
+
reflection: ReflectionType
|
255
|
+
).returns(T::Boolean)
|
256
|
+
end
|
257
|
+
def polymorphic_association?(reflection)
|
258
|
+
if reflection.through_reflection?
|
259
|
+
polymorphic_association?(reflection.source_reflection)
|
260
|
+
else
|
261
|
+
!!reflection.polymorphic?
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|