tapioca 0.14.3 → 0.15.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 +4 -4
- data/README.md +22 -0
- data/lib/tapioca/cli.rb +5 -0
- data/lib/tapioca/commands/abstract_dsl.rb +5 -1
- data/lib/tapioca/commands/annotations.rb +26 -14
- data/lib/tapioca/dsl/compiler.rb +13 -2
- data/lib/tapioca/dsl/compilers/active_record_columns.rb +43 -2
- data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +10 -5
- data/lib/tapioca/dsl/compilers/active_record_relations.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_typed_store.rb +18 -5
- data/lib/tapioca/dsl/compilers/active_support_concern.rb +1 -1
- data/lib/tapioca/dsl/compilers/identity_cache.rb +5 -2
- data/lib/tapioca/dsl/compilers/protobuf.rb +4 -1
- data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +98 -33
- data/lib/tapioca/dsl/pipeline.rb +5 -2
- data/lib/tapioca/gem_info.rb +18 -0
- data/lib/tapioca/helpers/config_helper.rb +3 -1
- data/lib/tapioca/helpers/test/dsl_compiler.rb +16 -6
- data/lib/tapioca/internal.rb +1 -0
- data/lib/tapioca/static/symbol_loader.rb +1 -1
- data/lib/tapioca/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f93b130a7096e0712ccd91b8406d92ce537287afafc8aad7586e7f670d88a131
|
4
|
+
data.tar.gz: b2cc1e6d2658002600c1edd9c5c88b22704e3f4170ff19aff93bb9fc21df688e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa10ff7ffd4768cea802b63dc68273fa0abf7f69b61a83b1325669e97d706cb8afcc704093c93596a830662fe04b01ad2ded3c166abd69a5c69097bd28f586c9
|
7
|
+
data.tar.gz: 63499428de9374990372beb46ece3f98d0f446ba28ef02bfa181179113e37e587ff9867bbe0d729d67c03ddb2813943f6f04d14f2065589a603a766a06ce8096
|
data/README.md
CHANGED
@@ -47,6 +47,7 @@ Tapioca makes it easy to work with [Sorbet](https://sorbet.org) in your codebase
|
|
47
47
|
* [Changing the typed strictness of annotations files](#changing-the-typed-strictness-of-annotations-files)
|
48
48
|
* [Generating RBI files for Rails and other DSLs](#generating-rbi-files-for-rails-and-other-dsls)
|
49
49
|
* [Keeping RBI files for DSLs up-to-date](#keeping-rbi-files-for-dsls-up-to-date)
|
50
|
+
* [Using DSL compiler options](#using-dsl-compiler-options)
|
50
51
|
* [Writing custom DSL compilers](#writing-custom-dsl-compilers)
|
51
52
|
* [Writing custom DSL extensions](#writing-custom-dsl-extensions)
|
52
53
|
* [RBI files for missing constants and methods](#rbi-files-for-missing-constants-and-methods)
|
@@ -501,6 +502,7 @@ Options:
|
|
501
502
|
[--halt-upon-load-error], [--no-halt-upon-load-error], [--skip-halt-upon-load-error] # Halt upon a load error while loading the Rails application
|
502
503
|
# Default: true
|
503
504
|
[--skip-constant=constant [constant ...]] # Do not generate RBI definitions for the given application constant(s)
|
505
|
+
[--compiler-options=key:value] # Options to pass to the DSL compilers
|
504
506
|
-c, [--config=<config file path>] # Path to the Tapioca configuration file
|
505
507
|
# Default: sorbet/tapioca/config.yml
|
506
508
|
-V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes
|
@@ -545,6 +547,25 @@ if Rails.env.development?
|
|
545
547
|
end
|
546
548
|
```
|
547
549
|
|
550
|
+
#### Using DSL compiler options
|
551
|
+
|
552
|
+
Some DSL compilers are able to change their behaviour based on the options passed to them. For example, the
|
553
|
+
`ActiveRecordColumns` compiler can be configured to change how it generates types for method related to Active Record
|
554
|
+
column attributes. To pass options during DSL RBI generation, use the `--compiler-options` flag:
|
555
|
+
```shell
|
556
|
+
$ bin/tapioca dsl --compiler-options=ActiveRecordColumnTypes:untyped
|
557
|
+
```
|
558
|
+
which will make the `ActiveRecordColumns` compiler generate untyped signatures for column attribute methods.
|
559
|
+
|
560
|
+
Compiler options can be passed through the configuration file, as like any other option, and we expect most users to
|
561
|
+
configure them this way. For example, to configure the `ActiveRecordColumns` compiler to generate untyped signatures,
|
562
|
+
you need to add the following to your `sorbet/tapioca/config.yml` file:
|
563
|
+
```yaml
|
564
|
+
dsl:
|
565
|
+
compiler_options:
|
566
|
+
ActiveRecordColumnTypes: untyped
|
567
|
+
```
|
568
|
+
|
548
569
|
#### Writing custom DSL compilers
|
549
570
|
|
550
571
|
It is possible to create your own compilers for DSLs not supported by Tapioca out of the box.
|
@@ -935,6 +956,7 @@ dsl:
|
|
935
956
|
app_root: "."
|
936
957
|
halt_upon_load_error: true
|
937
958
|
skip_constant: []
|
959
|
+
compiler_options: {}
|
938
960
|
gem:
|
939
961
|
outdir: sorbet/rbi/gems
|
940
962
|
file_header: true
|
data/lib/tapioca/cli.rb
CHANGED
@@ -140,6 +140,10 @@ module Tapioca
|
|
140
140
|
banner: "constant [constant ...]",
|
141
141
|
desc: "Do not generate RBI definitions for the given application constant(s)",
|
142
142
|
default: []
|
143
|
+
option :compiler_options,
|
144
|
+
type: :hash,
|
145
|
+
desc: "Options to pass to the DSL compilers",
|
146
|
+
default: {}
|
143
147
|
def dsl(*constant_or_paths)
|
144
148
|
set_environment(options)
|
145
149
|
|
@@ -161,6 +165,7 @@ module Tapioca
|
|
161
165
|
rbi_formatter: rbi_formatter(options),
|
162
166
|
app_root: options[:app_root],
|
163
167
|
halt_upon_load_error: options[:halt_upon_load_error],
|
168
|
+
compiler_options: options[:compiler_options],
|
164
169
|
}
|
165
170
|
|
166
171
|
command = if options[:verify]
|
@@ -27,6 +27,7 @@ module Tapioca
|
|
27
27
|
rbi_formatter: RBIFormatter,
|
28
28
|
app_root: String,
|
29
29
|
halt_upon_load_error: T::Boolean,
|
30
|
+
compiler_options: T::Hash[String, T.untyped],
|
30
31
|
).void
|
31
32
|
end
|
32
33
|
def initialize(
|
@@ -45,7 +46,8 @@ module Tapioca
|
|
45
46
|
gem_dir: DEFAULT_GEM_DIR,
|
46
47
|
rbi_formatter: DEFAULT_RBI_FORMATTER,
|
47
48
|
app_root: ".",
|
48
|
-
halt_upon_load_error: true
|
49
|
+
halt_upon_load_error: true,
|
50
|
+
compiler_options: {}
|
49
51
|
)
|
50
52
|
@requested_constants = requested_constants
|
51
53
|
@requested_paths = requested_paths
|
@@ -63,6 +65,7 @@ module Tapioca
|
|
63
65
|
@app_root = app_root
|
64
66
|
@halt_upon_load_error = halt_upon_load_error
|
65
67
|
@skip_constant = skip_constant
|
68
|
+
@compiler_options = compiler_options
|
66
69
|
|
67
70
|
super()
|
68
71
|
end
|
@@ -129,6 +132,7 @@ module Tapioca
|
|
129
132
|
},
|
130
133
|
skipped_constants: constantize(@skip_constant, ignore_missing: true),
|
131
134
|
number_of_workers: @number_of_workers,
|
135
|
+
compiler_options: @compiler_options,
|
132
136
|
)
|
133
137
|
end
|
134
138
|
|
@@ -46,22 +46,22 @@ module Tapioca
|
|
46
46
|
GitAttributes.create_vendored_attribute_file(@outpath)
|
47
47
|
end
|
48
48
|
|
49
|
-
sig { returns(T::Array[
|
49
|
+
sig { returns(T::Array[GemInfo]) }
|
50
50
|
def list_gemfile_gems
|
51
51
|
say("Listing gems from Gemfile.lock... ", [:blue, :bold])
|
52
52
|
gemfile = Bundler.read_file("Gemfile.lock")
|
53
53
|
parser = Bundler::LockfileParser.new(gemfile)
|
54
|
-
|
54
|
+
gem_info = parser.specs.map { |spec| GemInfo.from_spec(spec) }
|
55
55
|
say("Done", :green)
|
56
|
-
|
56
|
+
gem_info
|
57
57
|
end
|
58
58
|
|
59
|
-
sig { params(project_gems: T::Array[
|
59
|
+
sig { params(project_gems: T::Array[GemInfo]).void }
|
60
60
|
def remove_expired_annotations(project_gems)
|
61
61
|
say("Removing annotations for gems that have been removed... ", [:blue, :bold])
|
62
62
|
|
63
63
|
annotations = Pathname.glob(@outpath.join("*.rbi")).map { |f| f.basename(".*").to_s }
|
64
|
-
expired = annotations - project_gems
|
64
|
+
expired = annotations - project_gems.map(&:name)
|
65
65
|
|
66
66
|
if expired.empty?
|
67
67
|
say(" Nothing to do")
|
@@ -109,14 +109,14 @@ module Tapioca
|
|
109
109
|
index
|
110
110
|
end
|
111
111
|
|
112
|
-
sig { params(
|
113
|
-
def fetch_annotations(
|
112
|
+
sig { params(project_gems: T::Array[GemInfo]).returns(T::Array[String]) }
|
113
|
+
def fetch_annotations(project_gems)
|
114
114
|
say("Fetching gem annotations from central repository... ", [:blue, :bold])
|
115
|
-
fetchable_gems = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[
|
115
|
+
fetchable_gems = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[GemInfo, T::Array[String]])
|
116
116
|
|
117
|
-
|
117
|
+
project_gems.each_with_object(fetchable_gems) do |gem_info, hash|
|
118
118
|
@indexes.each do |uri, index|
|
119
|
-
T.must(hash[
|
119
|
+
T.must(hash[gem_info]) << uri if index.has_gem?(gem_info.name)
|
120
120
|
end
|
121
121
|
end
|
122
122
|
|
@@ -127,13 +127,16 @@ module Tapioca
|
|
127
127
|
end
|
128
128
|
|
129
129
|
say("\n")
|
130
|
-
fetched_gems = fetchable_gems.select { |
|
130
|
+
fetched_gems = fetchable_gems.select { |gem_info, repo_uris| fetch_annotation(repo_uris, gem_info) }
|
131
131
|
say("\nDone", :green)
|
132
|
-
fetched_gems.keys.sort
|
132
|
+
fetched_gems.keys.map(&:name).sort
|
133
133
|
end
|
134
134
|
|
135
|
-
sig { params(repo_uris: T::Array[String],
|
136
|
-
def fetch_annotation(repo_uris,
|
135
|
+
sig { params(repo_uris: T::Array[String], gem_info: GemInfo).void }
|
136
|
+
def fetch_annotation(repo_uris, gem_info)
|
137
|
+
gem_name = gem_info.name
|
138
|
+
gem_version = gem_info.version
|
139
|
+
|
137
140
|
contents = repo_uris.map do |repo_uri|
|
138
141
|
fetch_file(repo_uri, "#{CENTRAL_REPO_ANNOTATIONS_DIR}/#{gem_name}.rbi")
|
139
142
|
end
|
@@ -142,6 +145,7 @@ module Tapioca
|
|
142
145
|
return unless content
|
143
146
|
|
144
147
|
content = apply_typed_override(gem_name, content)
|
148
|
+
content = filter_versions(gem_version, content)
|
145
149
|
content = add_header(gem_name, content)
|
146
150
|
|
147
151
|
say("\n Fetched #{set_color(gem_name, :yellow, :bold)}", :green)
|
@@ -221,6 +225,14 @@ module Tapioca
|
|
221
225
|
Spoom::Sorbet::Sigils.update_sigil(content, strictness)
|
222
226
|
end
|
223
227
|
|
228
|
+
sig { params(gem_version: ::Gem::Version, content: String).returns(String) }
|
229
|
+
def filter_versions(gem_version, content)
|
230
|
+
rbi = RBI::Parser.parse_string(content)
|
231
|
+
rbi.filter_versions!(gem_version)
|
232
|
+
|
233
|
+
rbi.string
|
234
|
+
end
|
235
|
+
|
224
236
|
sig { params(gem_name: String, contents: T::Array[String]).returns(T.nilable(String)) }
|
225
237
|
def merge_files(gem_name, contents)
|
226
238
|
return if contents.empty?
|
data/lib/tapioca/dsl/compiler.rb
CHANGED
@@ -22,6 +22,9 @@ module Tapioca
|
|
22
22
|
sig { returns(RBI::Tree) }
|
23
23
|
attr_reader :root
|
24
24
|
|
25
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
26
|
+
attr_reader :options
|
27
|
+
|
25
28
|
class << self
|
26
29
|
extend T::Sig
|
27
30
|
|
@@ -60,11 +63,19 @@ module Tapioca
|
|
60
63
|
end
|
61
64
|
end
|
62
65
|
|
63
|
-
sig
|
64
|
-
|
66
|
+
sig do
|
67
|
+
params(
|
68
|
+
pipeline: Tapioca::Dsl::Pipeline,
|
69
|
+
root: RBI::Tree,
|
70
|
+
constant: ConstantType,
|
71
|
+
options: T::Hash[String, T.untyped],
|
72
|
+
).void
|
73
|
+
end
|
74
|
+
def initialize(pipeline, root, constant, options = {})
|
65
75
|
@pipeline = pipeline
|
66
76
|
@root = root
|
67
77
|
@constant = constant
|
78
|
+
@options = options
|
68
79
|
@errors = T.let([], T::Array[String])
|
69
80
|
end
|
70
81
|
|
@@ -15,6 +15,21 @@ module Tapioca
|
|
15
15
|
# created for columns and virtual attributes that are defined in the Active Record
|
16
16
|
# model.
|
17
17
|
#
|
18
|
+
# This compiler accepts a `ActiveRecordColumnTypes` option that can be used to specify
|
19
|
+
# how the types of the column related methods should be generated. The option can be one of the following:
|
20
|
+
# - `persisted` (_default_): The methods will be generated with the type that matches the actual database
|
21
|
+
# column type as the return type. This means that if the column is a string, the method return type
|
22
|
+
# will be `String`, but if the column is also nullable, then the return type will be `T.nilable(String)`. This
|
23
|
+
# mode basically treats each model as if it was a valid and persisted model. Note that this makes typing
|
24
|
+
# Active Record models easier, but does not match the behaviour of non-persisted or invalid models, which can
|
25
|
+
# have all kinds of non-sensical values in their column attributes.
|
26
|
+
# - `nilable`: All column methods will be generated with `T.nilable` return types. This is strictly the most
|
27
|
+
# correct way to type the methods, but it can make working with the models more cumbersome, as you will have to
|
28
|
+
# handle the `nil` cases explicitly using `T.must` or the safe navigation operator `&.`, even for valid
|
29
|
+
# persisted models.
|
30
|
+
# - `untyped`: The methods will be generated with `T.untyped` return types. This mode is practical if you are not
|
31
|
+
# ready to start typing your models strictly yet, but still want to generate RBI files for them.
|
32
|
+
#
|
18
33
|
# For example, with the following model class:
|
19
34
|
# ~~~rb
|
20
35
|
# class Post < ActiveRecord::Base
|
@@ -33,7 +48,7 @@ module Tapioca
|
|
33
48
|
# end
|
34
49
|
# ~~~
|
35
50
|
#
|
36
|
-
# this compiler will produce the following methods in the RBI file
|
51
|
+
# this compiler will, by default, produce the following methods in the RBI file
|
37
52
|
# `post.rbi`:
|
38
53
|
#
|
39
54
|
# ~~~rbi
|
@@ -94,6 +109,17 @@ module Tapioca
|
|
94
109
|
# end
|
95
110
|
# end
|
96
111
|
# ~~~
|
112
|
+
#
|
113
|
+
# However, if `ActiveRecordColumnTypes` is set to `nilable`, the `title` method will be generated as:
|
114
|
+
# ~~~rbi
|
115
|
+
# sig { returns(T.nilable(::String)) }
|
116
|
+
# def title; end
|
117
|
+
# ~~~
|
118
|
+
# and if the option is set to `untyped`, the `title` method will be generated as:
|
119
|
+
# ~~~rbi
|
120
|
+
# sig { returns(T.untyped) }
|
121
|
+
# def title; end
|
122
|
+
# ~~~
|
97
123
|
class ActiveRecordColumns < Compiler
|
98
124
|
extend T::Sig
|
99
125
|
include Helpers::ActiveRecordConstantsHelper
|
@@ -147,6 +173,21 @@ module Tapioca
|
|
147
173
|
|
148
174
|
private
|
149
175
|
|
176
|
+
ColumnTypeOption = Helpers::ActiveRecordColumnTypeHelper::ColumnTypeOption
|
177
|
+
|
178
|
+
sig { returns(ColumnTypeOption) }
|
179
|
+
def column_type_option
|
180
|
+
@column_type_option ||= T.let(
|
181
|
+
ColumnTypeOption.from_options(options) do |value, default_column_type_option|
|
182
|
+
add_error(<<~MSG.strip)
|
183
|
+
Unknown value for compiler option `ActiveRecordColumnTypes` given: `#{value}`.
|
184
|
+
Proceeding with the default value: `#{default_column_type_option.serialize}`.
|
185
|
+
MSG
|
186
|
+
end,
|
187
|
+
T.nilable(ColumnTypeOption),
|
188
|
+
)
|
189
|
+
end
|
190
|
+
|
150
191
|
sig do
|
151
192
|
params(
|
152
193
|
klass: RBI::Scope,
|
@@ -174,7 +215,7 @@ module Tapioca
|
|
174
215
|
end
|
175
216
|
def add_methods_for_attribute(klass, attribute_name, column_name = attribute_name, methods_to_add = nil)
|
176
217
|
getter_type, setter_type = Helpers::ActiveRecordColumnTypeHelper
|
177
|
-
.new(constant)
|
218
|
+
.new(constant, column_type_option: column_type_option)
|
178
219
|
.type_for(attribute_name, column_name)
|
179
220
|
|
180
221
|
# Added by ActiveRecord::AttributeMethods::Read
|
@@ -37,6 +37,7 @@ module Tapioca
|
|
37
37
|
extend T::Sig
|
38
38
|
|
39
39
|
ConstantType = type_member { { fixed: T.class_of(ActiveSupport::TestCase) } }
|
40
|
+
MISSING = Object.new
|
40
41
|
|
41
42
|
sig { override.void }
|
42
43
|
def decorate
|
@@ -46,6 +47,7 @@ module Tapioca
|
|
46
47
|
method_names_from_eager_fixture_loader
|
47
48
|
end
|
48
49
|
|
50
|
+
method_names.select! { |name| fixture_class_mapping_from_fixture_files[name] != MISSING }
|
49
51
|
return if method_names.empty?
|
50
52
|
|
51
53
|
root.create_path(constant) do |mod|
|
@@ -96,15 +98,14 @@ module Tapioca
|
|
96
98
|
T.unsafe(fixture_loader).fixture_sets.keys
|
97
99
|
end
|
98
100
|
|
99
|
-
sig { returns(T::Array[
|
101
|
+
sig { returns(T::Array[String]) }
|
100
102
|
def method_names_from_eager_fixture_loader
|
101
103
|
fixture_loader.ancestors # get all ancestors from class that includes AR fixtures
|
102
104
|
.drop(1) # drop the anonymous class itself from the array
|
103
105
|
.reject(&:name) # only collect anonymous ancestors because fixture methods are always on an anonymous module
|
104
|
-
.
|
105
|
-
|
106
|
+
.flat_map do |mod|
|
107
|
+
mod.private_instance_methods(false).map(&:to_s) + mod.instance_methods(false).map(&:to_s)
|
106
108
|
end
|
107
|
-
.flatten # merge methods into a single list
|
108
109
|
end
|
109
110
|
|
110
111
|
sig { params(mod: RBI::Scope, name: String).void }
|
@@ -190,10 +191,14 @@ module Tapioca
|
|
190
191
|
next unless ::File.file?(file)
|
191
192
|
|
192
193
|
ActiveRecord::FixtureSet::File.open(file) do |fh|
|
194
|
+
fixture_name = file.delete_prefix(path.to_s).delete_prefix("/").delete_suffix(".yml")
|
193
195
|
next unless fh.model_class
|
194
196
|
|
195
|
-
fixture_name = file.delete_prefix(path.to_s).delete_prefix("/").delete_suffix(".yml")
|
196
197
|
mapping[fixture_name] = fh.model_class
|
198
|
+
rescue ActiveRecord::Fixture::FormatError
|
199
|
+
# For fixtures that are not associated to any models and just contain raw data or fixtures that
|
200
|
+
# contain invalid formatting, we want to skip them and avoid crashing
|
201
|
+
mapping[fixture_name] = MISSING
|
197
202
|
end
|
198
203
|
end
|
199
204
|
end
|
@@ -190,7 +190,7 @@ module Tapioca
|
|
190
190
|
# Grab all Spawn methods
|
191
191
|
query_methods |= ActiveRecord::SpawnMethods.instance_methods(false)
|
192
192
|
# Remove the ones we know are private API
|
193
|
-
query_methods -= [:arel, :build_subquery, :construct_join_dependency, :extensions, :spawn]
|
193
|
+
query_methods -= [:all, :arel, :build_subquery, :construct_join_dependency, :extensions, :spawn]
|
194
194
|
# Remove "group" which needs a custom return type for GroupChains
|
195
195
|
query_methods -= [:group]
|
196
196
|
# Remove "where" which needs a custom return type for WhereChains
|
@@ -98,8 +98,7 @@ module Tapioca
|
|
98
98
|
stores.values.each do |store_data|
|
99
99
|
store_data.accessors.each do |accessor, name|
|
100
100
|
field = store_data.fields.fetch(accessor)
|
101
|
-
type = type_for(field
|
102
|
-
type = as_nilable_type(type) if field.null
|
101
|
+
type = type_for(field)
|
103
102
|
name ||= field.name # support < 1.5.0
|
104
103
|
|
105
104
|
generate_methods(store_accessors_module, name.to_s, type)
|
@@ -136,9 +135,23 @@ module Tapioca
|
|
136
135
|
T::Hash[Symbol, String],
|
137
136
|
)
|
138
137
|
|
139
|
-
sig { params(
|
140
|
-
def type_for(
|
141
|
-
TYPES.fetch(
|
138
|
+
sig { params(field: ActiveRecord::TypedStore::Field).returns(String) }
|
139
|
+
def type_for(field)
|
140
|
+
type = TYPES.fetch(field.type_sym, "T.untyped")
|
141
|
+
|
142
|
+
type = if field.array
|
143
|
+
# `null: false` applies to the array itself, not the elements, which are always nilable.
|
144
|
+
# https://github.com/byroot/activerecord-typedstore/blob/2f3fb98/spec/support/models.rb#L46C34-L46C45
|
145
|
+
# https://github.com/byroot/activerecord-typedstore/blob/2f3fb98/spec/active_record/typed_store_spec.rb#L854-L857
|
146
|
+
nilable_element_type = as_nilable_type(type)
|
147
|
+
"T::Array[#{nilable_element_type}]"
|
148
|
+
else
|
149
|
+
type
|
150
|
+
end
|
151
|
+
|
152
|
+
type = as_nilable_type(type) if field.null
|
153
|
+
|
154
|
+
type
|
142
155
|
end
|
143
156
|
|
144
157
|
sig do
|
@@ -49,7 +49,7 @@ module Tapioca
|
|
49
49
|
mixed_in_class_methods = dependencies
|
50
50
|
.uniq # Deduplicate
|
51
51
|
.filter_map do |concern| # Map to class methods module name, if exists
|
52
|
-
"#{qualified_name_of(concern)}::ClassMethods" if concern.const_defined?(:ClassMethods)
|
52
|
+
"#{qualified_name_of(concern)}::ClassMethods" if concern.const_defined?(:ClassMethods, false)
|
53
53
|
end
|
54
54
|
|
55
55
|
return if mixed_in_class_methods.empty?
|
@@ -216,7 +216,10 @@ module Tapioca
|
|
216
216
|
).void
|
217
217
|
end
|
218
218
|
def create_aliased_fetch_by_methods(field, klass)
|
219
|
-
type, _ = Helpers::ActiveRecordColumnTypeHelper.new(
|
219
|
+
type, _ = Helpers::ActiveRecordColumnTypeHelper.new(
|
220
|
+
constant,
|
221
|
+
column_type_option: Helpers::ActiveRecordColumnTypeHelper::ColumnTypeOption::Nilable,
|
222
|
+
).type_for(field.alias_name.to_s)
|
220
223
|
multi_type = type.delete_prefix("T.nilable(").delete_suffix(")").delete_prefix("::")
|
221
224
|
suffix = field.send(:fetch_method_suffix)
|
222
225
|
|
@@ -228,7 +231,7 @@ module Tapioca
|
|
228
231
|
"fetch_#{suffix}",
|
229
232
|
class_method: true,
|
230
233
|
parameters: parameters,
|
231
|
-
return_type: type,
|
234
|
+
return_type: field.unique ? type : COLLECTION_TYPE.call(type),
|
232
235
|
)
|
233
236
|
|
234
237
|
klass.create_method(
|
@@ -29,7 +29,7 @@ module Tapioca
|
|
29
29
|
# ~~~rbi
|
30
30
|
# # cart.rbi
|
31
31
|
# # typed: strong
|
32
|
-
# class Cart
|
32
|
+
# class Cart < Google::Protobuf::AbstractMessage
|
33
33
|
# sig { returns(Integer) }
|
34
34
|
# def customer_id; end
|
35
35
|
#
|
@@ -123,6 +123,9 @@ module Tapioca
|
|
123
123
|
class_method: true,
|
124
124
|
)
|
125
125
|
when Google::Protobuf::Descriptor
|
126
|
+
raise "#{klass} is not a RBI::Class" unless klass.is_a?(RBI::Class)
|
127
|
+
|
128
|
+
klass.superclass_name = "Google::Protobuf::AbstractMessage"
|
126
129
|
descriptor.each_oneof { |oneof| create_oneof_method(klass, oneof) }
|
127
130
|
fields = descriptor.map { |desc| create_descriptor_method(klass, desc) }
|
128
131
|
fields.sort_by!(&:name)
|
@@ -10,12 +10,73 @@ module Tapioca
|
|
10
10
|
extend T::Sig
|
11
11
|
include RBIHelper
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
class ColumnTypeOption < T::Enum
|
14
|
+
extend T::Sig
|
15
|
+
|
16
|
+
enums do
|
17
|
+
Untyped = new("untyped")
|
18
|
+
Nilable = new("nilable")
|
19
|
+
Persisted = new("persisted")
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
extend T::Sig
|
24
|
+
|
25
|
+
sig do
|
26
|
+
params(
|
27
|
+
options: T::Hash[String, T.untyped],
|
28
|
+
block: T.proc.params(value: String, default_column_type_option: ColumnTypeOption).void,
|
29
|
+
).returns(ColumnTypeOption)
|
30
|
+
end
|
31
|
+
def from_options(options, &block)
|
32
|
+
column_type_option = Persisted
|
33
|
+
value = options["ActiveRecordColumnTypes"]
|
34
|
+
|
35
|
+
if value
|
36
|
+
if has_serialized?(value)
|
37
|
+
column_type_option = from_serialized(value)
|
38
|
+
else
|
39
|
+
block.call(value, column_type_option)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
column_type_option
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { returns(T::Boolean) }
|
48
|
+
def persisted?
|
49
|
+
self == ColumnTypeOption::Persisted
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { returns(T::Boolean) }
|
53
|
+
def nilable?
|
54
|
+
self == ColumnTypeOption::Nilable
|
55
|
+
end
|
56
|
+
|
57
|
+
sig { returns(T::Boolean) }
|
58
|
+
def untyped?
|
59
|
+
self == ColumnTypeOption::Untyped
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
sig do
|
64
|
+
params(
|
65
|
+
constant: T.class_of(ActiveRecord::Base),
|
66
|
+
column_type_option: ColumnTypeOption,
|
67
|
+
).void
|
68
|
+
end
|
69
|
+
def initialize(constant, column_type_option: ColumnTypeOption::Persisted)
|
15
70
|
@constant = constant
|
71
|
+
@column_type_option = column_type_option
|
16
72
|
end
|
17
73
|
|
18
|
-
sig
|
74
|
+
sig do
|
75
|
+
params(
|
76
|
+
attribute_name: String,
|
77
|
+
column_name: String,
|
78
|
+
).returns([String, String])
|
79
|
+
end
|
19
80
|
def type_for(attribute_name, column_name = attribute_name)
|
20
81
|
return id_type if attribute_name == "id"
|
21
82
|
|
@@ -27,7 +88,11 @@ module Tapioca
|
|
27
88
|
sig { returns([String, String]) }
|
28
89
|
def id_type
|
29
90
|
if @constant.respond_to?(:composite_primary_key?) && T.unsafe(@constant).composite_primary_key?
|
30
|
-
@constant.primary_key.map
|
91
|
+
@constant.primary_key.map do |column|
|
92
|
+
column_type_for(column)
|
93
|
+
end.map do |tuple|
|
94
|
+
"[#{tuple.join(", ")}]"
|
95
|
+
end
|
31
96
|
else
|
32
97
|
column_type_for(@constant.primary_key)
|
33
98
|
end
|
@@ -35,7 +100,7 @@ module Tapioca
|
|
35
100
|
|
36
101
|
sig { params(column_name: String).returns([String, String]) }
|
37
102
|
def column_type_for(column_name)
|
38
|
-
return ["T.untyped", "T.untyped"] if
|
103
|
+
return ["T.untyped", "T.untyped"] if @column_type_option.untyped?
|
39
104
|
|
40
105
|
column = @constant.columns_hash[column_name]
|
41
106
|
column_type = @constant.attribute_types[column_name]
|
@@ -48,28 +113,24 @@ module Tapioca
|
|
48
113
|
getter_type
|
49
114
|
end
|
50
115
|
|
51
|
-
if column&.null
|
116
|
+
if @column_type_option.persisted? && !column&.null
|
117
|
+
[getter_type, setter_type]
|
118
|
+
else
|
52
119
|
getter_type = as_nilable_type(getter_type) unless not_nilable_serialized_column?(column_type)
|
53
|
-
|
54
|
-
end
|
55
|
-
|
56
|
-
if Array(@constant.primary_key).include?(column_name) ||
|
57
|
-
column_name == "created_at" ||
|
58
|
-
column_name == "updated_at"
|
59
|
-
getter_type = as_nilable_type(getter_type)
|
120
|
+
[getter_type, as_nilable_type(setter_type)]
|
60
121
|
end
|
61
|
-
|
62
|
-
[getter_type, setter_type]
|
63
122
|
end
|
64
123
|
|
65
124
|
sig { params(column_type: T.untyped).returns(String) }
|
66
125
|
def type_for_activerecord_value(column_type)
|
67
126
|
case column_type
|
68
|
-
when defined?(MoneyColumn) && MoneyColumn::ActiveRecordType
|
127
|
+
when ->(type) { defined?(MoneyColumn) && MoneyColumn::ActiveRecordType === type }
|
69
128
|
"::Money"
|
70
129
|
when ActiveRecord::Type::Integer
|
71
130
|
"::Integer"
|
72
|
-
when
|
131
|
+
when ->(type) {
|
132
|
+
defined?(ActiveRecord::Encryption) && ActiveRecord::Encryption::EncryptedAttributeType === type
|
133
|
+
}
|
73
134
|
# Reflect to see if `ActiveModel::Type::Value` is being used first.
|
74
135
|
getter_type = Tapioca::Dsl::Helpers::ActiveModelTypeHelper.type_for(column_type)
|
75
136
|
return getter_type unless getter_type == "T.untyped"
|
@@ -95,32 +156,36 @@ module Tapioca
|
|
95
156
|
"::String"
|
96
157
|
when ActiveRecord::Type::Serialized
|
97
158
|
serialized_column_type(column_type)
|
98
|
-
when
|
99
|
-
|
159
|
+
when ->(type) {
|
160
|
+
defined?(ActiveRecord::Normalization::NormalizedValueType) &&
|
161
|
+
ActiveRecord::Normalization::NormalizedValueType === type
|
162
|
+
}
|
100
163
|
type_for_activerecord_value(column_type.cast_type)
|
101
|
-
when
|
102
|
-
|
164
|
+
when ->(type) {
|
165
|
+
defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid) &&
|
166
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid === type
|
167
|
+
}
|
103
168
|
"::String"
|
104
|
-
when
|
105
|
-
|
169
|
+
when ->(type) {
|
170
|
+
defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Cidr) &&
|
171
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Cidr === type
|
172
|
+
}
|
106
173
|
"::IPAddr"
|
107
|
-
when
|
108
|
-
|
174
|
+
when ->(type) {
|
175
|
+
defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore) &&
|
176
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore === type
|
177
|
+
}
|
109
178
|
"T::Hash[::String, ::String]"
|
110
|
-
when
|
111
|
-
|
179
|
+
when ->(type) {
|
180
|
+
defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array) &&
|
181
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array === type
|
182
|
+
}
|
112
183
|
"T::Array[#{type_for_activerecord_value(column_type.subtype)}]"
|
113
184
|
else
|
114
185
|
ActiveModelTypeHelper.type_for(column_type)
|
115
186
|
end
|
116
187
|
end
|
117
188
|
|
118
|
-
sig { params(constant: Module).returns(T::Boolean) }
|
119
|
-
def do_not_generate_strong_types?(constant)
|
120
|
-
Object.const_defined?(:StrongTypeGeneration) &&
|
121
|
-
!(constant.singleton_class < Object.const_get(:StrongTypeGeneration))
|
122
|
-
end
|
123
|
-
|
124
189
|
sig { params(column_type: ActiveRecord::Enum::EnumType).returns(String) }
|
125
190
|
def enum_setter_type(column_type)
|
126
191
|
# In Rails < 7 this method is private. When support for that is dropped we can call the method directly
|
data/lib/tapioca/dsl/pipeline.rb
CHANGED
@@ -33,6 +33,7 @@ module Tapioca
|
|
33
33
|
error_handler: T.proc.params(error: String).void,
|
34
34
|
skipped_constants: T::Array[Module],
|
35
35
|
number_of_workers: T.nilable(Integer),
|
36
|
+
compiler_options: T::Hash[String, T.untyped],
|
36
37
|
).void
|
37
38
|
end
|
38
39
|
def initialize(
|
@@ -42,7 +43,8 @@ module Tapioca
|
|
42
43
|
excluded_compilers: [],
|
43
44
|
error_handler: $stderr.method(:puts).to_proc,
|
44
45
|
skipped_constants: [],
|
45
|
-
number_of_workers: nil
|
46
|
+
number_of_workers: nil,
|
47
|
+
compiler_options: {}
|
46
48
|
)
|
47
49
|
@active_compilers = T.let(
|
48
50
|
gather_active_compilers(requested_compilers, excluded_compilers),
|
@@ -53,6 +55,7 @@ module Tapioca
|
|
53
55
|
@error_handler = error_handler
|
54
56
|
@skipped_constants = skipped_constants
|
55
57
|
@number_of_workers = number_of_workers
|
58
|
+
@compiler_options = compiler_options
|
56
59
|
@errors = T.let([], T::Array[String])
|
57
60
|
end
|
58
61
|
|
@@ -197,7 +200,7 @@ module Tapioca
|
|
197
200
|
active_compilers.each do |compiler_class|
|
198
201
|
next unless compiler_class.handles?(constant)
|
199
202
|
|
200
|
-
compiler = compiler_class.new(self, file.root, constant)
|
203
|
+
compiler = compiler_class.new(self, file.root, constant, @compiler_options)
|
201
204
|
compiler.decorate
|
202
205
|
rescue
|
203
206
|
$stderr.puts("Error: `#{compiler_class.name}` failed to generate RBI for `#{constant}`")
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Tapioca
|
5
|
+
class GemInfo < T::Struct
|
6
|
+
const :name, String
|
7
|
+
const :version, ::Gem::Version
|
8
|
+
|
9
|
+
class << self
|
10
|
+
extend(T::Sig)
|
11
|
+
|
12
|
+
sig { params(spec: Bundler::LazySpecification).returns(GemInfo) }
|
13
|
+
def from_spec(spec)
|
14
|
+
new(name: spec.name, version: spec.version)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -132,7 +132,9 @@ module Tapioca
|
|
132
132
|
when :hash
|
133
133
|
error_msg = "invalid value for option `#{config_option_key}` for key `#{config_key}` - expected " \
|
134
134
|
"`Hash[String, String]` but found `#{config_option_value}`"
|
135
|
-
|
135
|
+
values_to_validate = config_option_value.keys
|
136
|
+
values_to_validate += config_option_value.values
|
137
|
+
all_strings = values_to_validate.all? { |v| v.is_a?(String) }
|
136
138
|
next build_error(error_msg) unless all_strings
|
137
139
|
end
|
138
140
|
end
|
@@ -29,9 +29,14 @@ module Tapioca
|
|
29
29
|
context.activate_other_dsl_compilers(compiler_classes)
|
30
30
|
end
|
31
31
|
|
32
|
-
sig
|
33
|
-
|
34
|
-
|
32
|
+
sig do
|
33
|
+
params(
|
34
|
+
constant_name: T.any(Symbol, String),
|
35
|
+
compiler_options: T::Hash[Symbol, T.untyped],
|
36
|
+
).returns(String)
|
37
|
+
end
|
38
|
+
def rbi_for(constant_name, compiler_options: {})
|
39
|
+
context.rbi_for(constant_name, compiler_options: compiler_options)
|
35
40
|
end
|
36
41
|
|
37
42
|
sig { returns(T::Array[String]) }
|
@@ -85,8 +90,13 @@ module Tapioca
|
|
85
90
|
compiler_class.processable_constants.filter_map(&:name).sort
|
86
91
|
end
|
87
92
|
|
88
|
-
sig
|
89
|
-
|
93
|
+
sig do
|
94
|
+
params(
|
95
|
+
constant_name: T.any(Symbol, String),
|
96
|
+
compiler_options: T::Hash[Symbol, T.untyped],
|
97
|
+
).returns(String)
|
98
|
+
end
|
99
|
+
def rbi_for(constant_name, compiler_options: {})
|
90
100
|
# Make sure this is a constant that we can handle.
|
91
101
|
unless gathered_constants.include?(constant_name.to_s)
|
92
102
|
raise "`#{constant_name}` is not processable by the `#{compiler_class}` compiler."
|
@@ -95,7 +105,7 @@ module Tapioca
|
|
95
105
|
file = RBI::File.new(strictness: "strong")
|
96
106
|
constant = Object.const_get(constant_name)
|
97
107
|
|
98
|
-
compiler = compiler_class.new(pipeline, file.root, constant)
|
108
|
+
compiler = compiler_class.new(pipeline, file.root, constant, compiler_options.transform_keys(&:to_s))
|
99
109
|
compiler.decorate
|
100
110
|
|
101
111
|
rbi = Tapioca::DEFAULT_RBI_FORMATTER.print_file(file)
|
data/lib/tapioca/internal.rb
CHANGED
@@ -22,7 +22,7 @@ module Tapioca
|
|
22
22
|
sig { params(gem: Gemfile::GemSpec).returns(T::Set[String]) }
|
23
23
|
def engine_symbols(gem)
|
24
24
|
gem_engine = engines.find do |engine|
|
25
|
-
gem.
|
25
|
+
gem.full_gem_path == engine.config.root.to_s
|
26
26
|
end
|
27
27
|
|
28
28
|
return Set.new unless gem_engine
|
data/lib/tapioca/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tapioca
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ufuk Kayserilioglu
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: exe
|
13
13
|
cert_chain: []
|
14
|
-
date: 2024-
|
14
|
+
date: 2024-06-27 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: bundler
|
@@ -228,6 +228,7 @@ files:
|
|
228
228
|
- lib/tapioca/gem/listeners/subconstants.rb
|
229
229
|
- lib/tapioca/gem/listeners/yard_doc.rb
|
230
230
|
- lib/tapioca/gem/pipeline.rb
|
231
|
+
- lib/tapioca/gem_info.rb
|
231
232
|
- lib/tapioca/gemfile.rb
|
232
233
|
- lib/tapioca/helpers/cli_helper.rb
|
233
234
|
- lib/tapioca/helpers/config_helper.rb
|
@@ -288,7 +289,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
288
289
|
- !ruby/object:Gem::Version
|
289
290
|
version: '0'
|
290
291
|
requirements: []
|
291
|
-
rubygems_version: 3.5.
|
292
|
+
rubygems_version: 3.5.13
|
292
293
|
signing_key:
|
293
294
|
specification_version: 4
|
294
295
|
summary: A Ruby Interface file generator for gems, core types and the Ruby standard
|