tapioca 0.11.7 → 0.11.9

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +116 -49
  3. data/lib/tapioca/bundler_ext/auto_require_hook.rb +1 -1
  4. data/lib/tapioca/cli.rb +76 -67
  5. data/lib/tapioca/commands/{dsl.rb → abstract_dsl.rb} +32 -78
  6. data/lib/tapioca/commands/{gem.rb → abstract_gem.rb} +26 -93
  7. data/lib/tapioca/commands/annotations.rb +15 -13
  8. data/lib/tapioca/commands/check_shims.rb +2 -0
  9. data/lib/tapioca/commands/command.rb +9 -2
  10. data/lib/tapioca/commands/configure.rb +2 -2
  11. data/lib/tapioca/commands/dsl_compiler_list.rb +31 -0
  12. data/lib/tapioca/commands/dsl_generate.rb +40 -0
  13. data/lib/tapioca/commands/dsl_verify.rb +25 -0
  14. data/lib/tapioca/commands/gem_generate.rb +51 -0
  15. data/lib/tapioca/commands/gem_sync.rb +37 -0
  16. data/lib/tapioca/commands/gem_verify.rb +36 -0
  17. data/lib/tapioca/commands/require.rb +2 -0
  18. data/lib/tapioca/commands/todo.rb +21 -2
  19. data/lib/tapioca/commands.rb +8 -2
  20. data/lib/tapioca/dsl/compiler.rb +8 -4
  21. data/lib/tapioca/dsl/compilers/action_controller_helpers.rb +3 -1
  22. data/lib/tapioca/dsl/compilers/active_model_validations_confirmation.rb +94 -0
  23. data/lib/tapioca/dsl/compilers/active_record_columns.rb +19 -9
  24. data/lib/tapioca/dsl/compilers/active_record_typed_store.rb +3 -2
  25. data/lib/tapioca/dsl/compilers/active_support_concern.rb +1 -1
  26. data/lib/tapioca/dsl/compilers/graphql_input_object.rb +1 -1
  27. data/lib/tapioca/dsl/compilers/graphql_mutation.rb +9 -2
  28. data/lib/tapioca/dsl/compilers/json_api_client_resource.rb +208 -0
  29. data/lib/tapioca/dsl/compilers/protobuf.rb +2 -8
  30. data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +20 -4
  31. data/lib/tapioca/dsl/helpers/graphql_type_helper.rb +20 -3
  32. data/lib/tapioca/dsl/pipeline.rb +6 -4
  33. data/lib/tapioca/gem/pipeline.rb +103 -36
  34. data/lib/tapioca/gemfile.rb +13 -7
  35. data/lib/tapioca/helpers/cli_helper.rb +2 -2
  36. data/lib/tapioca/helpers/git_attributes.rb +34 -0
  37. data/lib/tapioca/helpers/test/isolation.rb +7 -2
  38. data/lib/tapioca/helpers/test/template.rb +4 -4
  39. data/lib/tapioca/internal.rb +1 -0
  40. data/lib/tapioca/loaders/dsl.rb +11 -1
  41. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +0 -27
  42. data/lib/tapioca/static/symbol_loader.rb +9 -8
  43. data/lib/tapioca/version.rb +1 -1
  44. metadata +19 -10
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Commands
6
+ class GemVerify < AbstractGem
7
+ private
8
+
9
+ sig { override.void }
10
+ def execute
11
+ say("Checking for out-of-date RBIs...")
12
+ say("")
13
+ perform_sync_verification
14
+ end
15
+
16
+ sig { void }
17
+ def perform_sync_verification
18
+ diff = {}
19
+
20
+ removed_rbis.each do |gem_name|
21
+ next if @exclude.include?(gem_name)
22
+
23
+ filename = existing_rbi(gem_name)
24
+ diff[filename] = :removed
25
+ end
26
+
27
+ added_rbis.each do |gem_name|
28
+ filename = expected_rbi(gem_name)
29
+ diff[filename] = gem_rbi_exists?(gem_name) ? :changed : :added
30
+ end
31
+
32
+ report_diff_and_exit_if_out_of_date(diff, :gem)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -17,6 +17,8 @@ module Tapioca
17
17
  super()
18
18
  end
19
19
 
20
+ private
21
+
20
22
  sig { override.void }
21
23
  def execute
22
24
  compiler = Static::RequiresCompiler.new(@sorbet_config_path)
@@ -6,6 +6,17 @@ module Tapioca
6
6
  class Todo < CommandWithoutTracker
7
7
  include SorbetHelper
8
8
 
9
+ DEPRECATION_MESSAGE = T.let(<<~DEPRECATION, String)
10
+ The `todo` command is deprecated and will be removed in a future release.
11
+
12
+ If your project is still missing type definitions for constants, try the following:
13
+ 1. Regenerate gem RBIs by running `bin/tapioca gem --all` and `bin/tapioca annotations`
14
+ 2. Generate RBIs for DSLs by running `bin/tapioca dsl`
15
+ 3. If the missing constants are defined in files that a gem does not load by default,
16
+ manually require those files in `sorbet/tapioca/require.rb` and regenerate gem RBIs
17
+ 4. Manually create an RBI shim defining the missing constants
18
+ DEPRECATION
19
+
9
20
  sig do
10
21
  params(
11
22
  todo_file: String,
@@ -19,6 +30,16 @@ module Tapioca
19
30
  super()
20
31
  end
21
32
 
33
+ sig { void }
34
+ def run_with_deprecation
35
+ say(DEPRECATION_MESSAGE, :red)
36
+ say("")
37
+
38
+ run
39
+ end
40
+
41
+ private
42
+
22
43
  sig { override.void }
23
44
  def execute
24
45
  say("Finding all unresolved constants, this may take a few seconds... ")
@@ -43,8 +64,6 @@ module Tapioca
43
64
  say("Please review changes and commit them.", [:green, :bold])
44
65
  end
45
66
 
46
- private
47
-
48
67
  sig { params(constants: T::Array[String], command: String).returns(RBI::File) }
49
68
  def rbi(constants, command:)
50
69
  file = RBI::File.new
@@ -7,9 +7,15 @@ module Tapioca
7
7
  autoload :CommandWithoutTracker, "tapioca/commands/command_without_tracker"
8
8
  autoload :Annotations, "tapioca/commands/annotations"
9
9
  autoload :CheckShims, "tapioca/commands/check_shims"
10
- autoload :Dsl, "tapioca/commands/dsl"
10
+ autoload :AbstractDsl, "tapioca/commands/abstract_dsl"
11
+ autoload :DslCompilerList, "tapioca/commands/dsl_compiler_list"
12
+ autoload :DslGenerate, "tapioca/commands/dsl_generate"
13
+ autoload :DslVerify, "tapioca/commands/dsl_verify"
11
14
  autoload :Configure, "tapioca/commands/configure"
12
- autoload :Gem, "tapioca/commands/gem"
15
+ autoload :AbstractGem, "tapioca/commands/abstract_gem"
16
+ autoload :GemGenerate, "tapioca/commands/gem_generate"
17
+ autoload :GemSync, "tapioca/commands/gem_sync"
18
+ autoload :GemVerify, "tapioca/commands/gem_verify"
13
19
  autoload :Require, "tapioca/commands/require"
14
20
  autoload :Todo, "tapioca/commands/todo"
15
21
  end
@@ -45,14 +45,18 @@ module Tapioca
45
45
 
46
46
  sig { returns(T::Enumerable[T::Class[T.anything]]) }
47
47
  def all_classes
48
- @all_classes = T.let(@all_classes, T.nilable(T::Enumerable[T::Class[T.anything]]))
49
- @all_classes ||= T.cast(ObjectSpace.each_object(Class), T::Enumerable[T::Class[T.anything]]).each
48
+ @all_classes ||= T.let(
49
+ T.cast(ObjectSpace.each_object(Class), T::Enumerable[T::Class[T.anything]]).each,
50
+ T.nilable(T::Enumerable[T::Class[T.anything]]),
51
+ )
50
52
  end
51
53
 
52
54
  sig { returns(T::Enumerable[Module]) }
53
55
  def all_modules
54
- @all_modules = T.let(@all_modules, T.nilable(T::Enumerable[Module]))
55
- @all_modules ||= T.cast(ObjectSpace.each_object(Module), T::Enumerable[Module]).each
56
+ @all_modules ||= T.let(
57
+ T.cast(ObjectSpace.each_object(Module), T::Enumerable[Module]).each,
58
+ T.nilable(T::Enumerable[Module]),
59
+ )
56
60
  end
57
61
  end
58
62
 
@@ -126,7 +126,9 @@ module Tapioca
126
126
 
127
127
  sig { override.returns(T::Enumerable[Module]) }
128
128
  def gather_constants
129
- descendants_of(::ActionController::Base).reject(&:abstract?).select(&:name)
129
+ descendants_of(::ActionController::Base).select(&:name).select do |klass|
130
+ klass.const_defined?(:HelperMethods, false)
131
+ end
130
132
  end
131
133
  end
132
134
 
@@ -0,0 +1,94 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "active_model"
6
+ rescue LoadError
7
+ return
8
+ end
9
+
10
+ module Tapioca
11
+ module Dsl
12
+ module Compilers
13
+ # `Tapioca::Dsl::Compilers::ActiveModelValidationsConfirmation` decorates RBI files for all
14
+ # classes that use [`ActiveModel::Validates::Confirmation`](https://api.rubyonrails.org/classes/ActiveModel/Validations/HelperMethods.html#method-i-validates_confirmation_of).
15
+ #
16
+ # For example, with the following class:
17
+ #
18
+ # ~~~rb
19
+ # class User
20
+ # include ActiveModel::Validations
21
+ #
22
+ # validates_confirmation_of :password
23
+ #
24
+ # validates :email, confirmation: true
25
+ # end
26
+ # ~~~
27
+ #
28
+ # this compiler will produce an RBI file with the following content:
29
+ # ~~~rbi
30
+ # # typed: true
31
+ #
32
+ # class User
33
+ #
34
+ # sig { returns(T.untyped) }
35
+ # def email_confirmation; end
36
+ #
37
+ # sig { params(email_confirmation=: T.untyped).returns(T.untyped) }
38
+ # def email_confirmation=(email_confirmation); end
39
+ #
40
+ # sig { returns(T.untyped) }
41
+ # def password_confirmation; end
42
+ #
43
+ # sig { params(password_confirmation=: T.untyped).returns(T.untyped) }
44
+ # def password_confirmation=(password_confirmation); end
45
+ # end
46
+ # ~~~
47
+ class ActiveModelValidationsConfirmation < Compiler
48
+ extend T::Sig
49
+
50
+ ConstantType = type_member do
51
+ {
52
+ fixed: T.all(
53
+ T::Class[ActiveModel::Validations],
54
+ ActiveModel::Validations::HelperMethods,
55
+ ActiveModel::Validations::ClassMethods,
56
+ ),
57
+ }
58
+ end
59
+
60
+ class << self
61
+ sig { override.returns(T::Enumerable[Module]) }
62
+ def gather_constants
63
+ # Collect all the classes that include ActiveModel::Validations
64
+ all_classes.select do |c|
65
+ c < ActiveModel::Validations
66
+ end
67
+ end
68
+ end
69
+
70
+ sig { override.void }
71
+ def decorate
72
+ confirmation_validators = constant.validators.grep(ActiveModel::Validations::ConfirmationValidator)
73
+
74
+ return if confirmation_validators.empty?
75
+
76
+ # Create a RBI definition for each class that includes Active::Model::Validations
77
+ root.create_path(constant) do |klass|
78
+ # Create RBI definitions for all the attributes that use confirmation validation
79
+ confirmation_validators.each do |validator|
80
+ validator.attributes.each do |attr_name|
81
+ klass.create_method("#{attr_name}_confirmation", return_type: "T.untyped")
82
+ klass.create_method(
83
+ "#{attr_name}_confirmation=",
84
+ parameters: [create_param("#{attr_name}_confirmation", type: "T.untyped")],
85
+ return_type: "T.untyped",
86
+ )
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -110,8 +110,8 @@ module Tapioca
110
110
 
111
111
  root.create_path(constant) do |model|
112
112
  model.create_module(AttributeMethodsModuleName) do |mod|
113
- constant.attribute_names.each do |column_name|
114
- add_methods_for_attribute(mod, column_name)
113
+ (constant.attribute_names + ["id"]).uniq.each do |attribute_name|
114
+ add_methods_for_attribute(mod, attribute_name)
115
115
  end
116
116
 
117
117
  constant.attribute_aliases.each do |attribute_name, column_name|
@@ -127,7 +127,7 @@ module Tapioca
127
127
  old_method_names = patterns.map { |m| m.method_name(column_name) }
128
128
  methods_to_add = new_method_names - old_method_names
129
129
 
130
- add_methods_for_attribute(mod, column_name, attribute_name, methods_to_add)
130
+ add_methods_for_attribute(mod, attribute_name, column_name, methods_to_add)
131
131
  end
132
132
  end
133
133
 
@@ -152,13 +152,13 @@ module Tapioca
152
152
  name: String,
153
153
  methods_to_add: T.nilable(T::Array[String]),
154
154
  return_type: String,
155
- parameters: T::Array[[String, String]],
155
+ parameters: T::Array[RBI::TypedParam],
156
156
  ).void
157
157
  end
158
158
  def add_method(klass, name, methods_to_add, return_type: "void", parameters: [])
159
159
  klass.create_method(
160
160
  name,
161
- parameters: parameters.map { |param, type| create_param(param, type: type) },
161
+ parameters: parameters,
162
162
  return_type: return_type,
163
163
  ) if methods_to_add.nil? || methods_to_add.include?(name)
164
164
  end
@@ -166,13 +166,15 @@ module Tapioca
166
166
  sig do
167
167
  params(
168
168
  klass: RBI::Scope,
169
- column_name: String,
170
169
  attribute_name: String,
170
+ column_name: String,
171
171
  methods_to_add: T.nilable(T::Array[String]),
172
172
  ).void
173
173
  end
174
- def add_methods_for_attribute(klass, column_name, attribute_name = column_name, methods_to_add = nil)
175
- getter_type, setter_type = Helpers::ActiveRecordColumnTypeHelper.new(constant).type_for(column_name)
174
+ def add_methods_for_attribute(klass, attribute_name, column_name = attribute_name, methods_to_add = nil)
175
+ getter_type, setter_type = Helpers::ActiveRecordColumnTypeHelper
176
+ .new(constant)
177
+ .type_for(attribute_name, column_name)
176
178
 
177
179
  # Added by ActiveRecord::AttributeMethods::Read
178
180
  #
@@ -189,7 +191,7 @@ module Tapioca
189
191
  klass,
190
192
  "#{attribute_name}=",
191
193
  methods_to_add,
192
- parameters: [["value", setter_type]],
194
+ parameters: [create_param("value", type: setter_type)],
193
195
  return_type: setter_type,
194
196
  )
195
197
 
@@ -254,6 +256,10 @@ module Tapioca
254
256
  "#{attribute_name}_changed?",
255
257
  methods_to_add,
256
258
  return_type: "T::Boolean",
259
+ parameters: [
260
+ create_kw_opt_param("from", type: setter_type, default: "T.unsafe(nil)"),
261
+ create_kw_opt_param("to", type: setter_type, default: "T.unsafe(nil)"),
262
+ ],
257
263
  )
258
264
  add_method(
259
265
  klass,
@@ -277,6 +283,10 @@ module Tapioca
277
283
  "#{attribute_name}_previously_changed?",
278
284
  methods_to_add,
279
285
  return_type: "T::Boolean",
286
+ parameters: [
287
+ create_kw_opt_param("from", type: setter_type, default: "T.unsafe(nil)"),
288
+ create_kw_opt_param("to", type: setter_type, default: "T.unsafe(nil)"),
289
+ ],
280
290
  )
281
291
  add_method(
282
292
  klass,
@@ -98,6 +98,9 @@ module Tapioca
98
98
  return if stores.values.all? { |store| store.accessors.empty? }
99
99
 
100
100
  root.create_path(constant) do |model|
101
+ store_accessors_module = model.create_module("StoreAccessors")
102
+ model.create_include("StoreAccessors")
103
+
101
104
  stores.values.each do |store_data|
102
105
  store_data.accessors.each do |accessor, name|
103
106
  field = store_data.fields.fetch(accessor)
@@ -105,9 +108,7 @@ module Tapioca
105
108
  type = as_nilable_type(type) if field.null
106
109
  name ||= field.name # support < 1.5.0
107
110
 
108
- store_accessors_module = model.create_module("StoreAccessors")
109
111
  generate_methods(store_accessors_module, name.to_s, type)
110
- model.create_include("StoreAccessors")
111
112
  end
112
113
  end
113
114
  end
@@ -85,7 +85,7 @@ module Tapioca
85
85
 
86
86
  sig { params(concern: Module).returns(T::Array[Module]) }
87
87
  def dependencies_of(concern)
88
- concern.instance_variable_get(:@_dependencies)
88
+ concern.instance_variable_get(:@_dependencies) || []
89
89
  end
90
90
  end
91
91
 
@@ -55,7 +55,7 @@ module Tapioca
55
55
  root.create_path(constant) do |input_object|
56
56
  arguments.each do |argument|
57
57
  name = argument.keyword.to_s
58
- input_object.create_method(name, return_type: Helpers::GraphqlTypeHelper.type_for(argument.type))
58
+ input_object.create_method(name, return_type: Helpers::GraphqlTypeHelper.type_for(argument))
59
59
  end
60
60
  end
61
61
  end
@@ -42,7 +42,7 @@ module Tapioca
42
42
  class GraphqlMutation < Compiler
43
43
  extend T::Sig
44
44
 
45
- ConstantType = type_member { { fixed: T.class_of(GraphQL::Schema::InputObject) } }
45
+ ConstantType = type_member { { fixed: T.class_of(GraphQL::Schema::Mutation) } }
46
46
 
47
47
  sig { override.void }
48
48
  def decorate
@@ -59,7 +59,7 @@ module Tapioca
59
59
  params = compile_method_parameters_to_rbi(method_def).map do |param|
60
60
  name = param.param.name
61
61
  argument = arguments_by_name.fetch(name, nil)
62
- create_typed_param(param.param, argument ? Helpers::GraphqlTypeHelper.type_for(argument.type) : "T.untyped")
62
+ create_typed_param(param.param, argument_type(argument))
63
63
  end
64
64
 
65
65
  root.create_path(constant) do |mutation|
@@ -67,6 +67,13 @@ module Tapioca
67
67
  end
68
68
  end
69
69
 
70
+ sig { params(argument: T.nilable(GraphQL::Schema::Argument)).returns(String) }
71
+ def argument_type(argument)
72
+ return "T.untyped" unless argument
73
+
74
+ Helpers::GraphqlTypeHelper.type_for(argument)
75
+ end
76
+
70
77
  class << self
71
78
  extend T::Sig
72
79
 
@@ -0,0 +1,208 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "json_api_client"
6
+ rescue LoadError
7
+ # means JsonApiClient is not installed,
8
+ # so let's not even define the compiler.
9
+ return
10
+ end
11
+
12
+ module Tapioca
13
+ module Dsl
14
+ module Compilers
15
+ # `Tapioca::Dsl::Compilers::JsonApiClientResource` generates RBI files for classes that inherit
16
+ # [`JsonApiClient::Resource`](https://github.com/JsonApiClient/json_api_client).
17
+ #
18
+ # For example, with the following classes that inherits `JsonApiClient::Resource`:
19
+ #
20
+ # ~~~rb
21
+ # # user.rb
22
+ # class User < JsonApiClient::Resource
23
+ # has_many :posts
24
+ #
25
+ # property :name, type: :string
26
+ # property :is_admin, type: :boolean, default: false
27
+ # end
28
+ #
29
+ # # post.rb
30
+ # class Post < JsonApiClient::Resource
31
+ # belongs_to :user
32
+ #
33
+ # property :title, type: :string
34
+ # end
35
+ # ~~~
36
+ #
37
+ # this compiler will produce RBI files with the following content:
38
+ #
39
+ # ~~~rbi
40
+ # # user.rbi
41
+ # # typed: strong
42
+ #
43
+ # class User
44
+ # include JsonApiClientResourceGeneratedMethods
45
+ #
46
+ # module JsonApiClientResourceGeneratedMethods
47
+ # sig { returns(T::Boolean) }
48
+ # def is_admin; end
49
+ #
50
+ # sig { params(is_admin: T::Boolean).returns(T::Boolean) }
51
+ # def is_admin=(is_admin); end
52
+ #
53
+ # sig { returns(T.nilable(::String)) }
54
+ # def name; end
55
+ #
56
+ # sig { params(name: T.nilable(::String)).returns(T.nilable(::String)) }
57
+ # def name=(name); end
58
+ #
59
+ # sig { returns(T.nilable(T::Array[Post])) }
60
+ # def posts; end
61
+ #
62
+ # sig { params(posts: T.nilable(T::Array[Post])).returns(T.nilable(T::Array[Post])) }
63
+ # def posts=(posts); end
64
+ # end
65
+ # end
66
+ #
67
+ # # post.rbi
68
+ # # typed: strong
69
+ #
70
+ # class Post
71
+ # include JsonApiClientResourceGeneratedMethods
72
+ #
73
+ # module JsonApiClientResourceGeneratedMethods
74
+ # sig { returns(T.nilable(::String)) }
75
+ # def title; end
76
+ #
77
+ # sig { params(title: T.nilable(::String)).returns(T.nilable(::String)) }
78
+ # def title=(title); end
79
+ #
80
+ # sig { returns(T.nilable(::String)) }
81
+ # def user_id; end
82
+ #
83
+ # sig { params(user_id: T.nilable(::String)).returns(T.nilable(::String)) }
84
+ # def user_id=(user_id); end
85
+ # end
86
+ # end
87
+ # ~~~
88
+ class JsonApiClientResource < Compiler
89
+ extend T::Sig
90
+
91
+ ConstantType = type_member { { fixed: T.class_of(::JsonApiClient::Resource) } }
92
+
93
+ sig { override.void }
94
+ def decorate
95
+ schema = resource_schema
96
+ return if schema.nil? && constant.associations.empty?
97
+
98
+ root.create_path(constant) do |k|
99
+ module_name = "JsonApiClientResourceGeneratedMethods"
100
+ k.create_module(module_name) do |mod|
101
+ schema&.each_property do |property|
102
+ generate_methods_for_property(mod, property)
103
+ end
104
+
105
+ constant.associations.each do |association|
106
+ generate_methods_for_association(mod, association)
107
+ end
108
+ end
109
+
110
+ k.create_include(module_name)
111
+ end
112
+ end
113
+
114
+ class << self
115
+ extend T::Sig
116
+
117
+ sig { override.returns(T::Enumerable[Module]) }
118
+ def gather_constants
119
+ all_modules.select do |c|
120
+ name_of(c) && c < ::JsonApiClient::Resource
121
+ end
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ sig { returns(T.nilable(::JsonApiClient::Schema)) }
128
+ def resource_schema
129
+ schema = constant.schema
130
+
131
+ # empty? does not exist on JsonApiClient::Schema
132
+ schema if schema.size > 0 # rubocop:disable Style/ZeroLengthPredicate
133
+ end
134
+
135
+ sig do
136
+ params(
137
+ mod: RBI::Scope,
138
+ property: ::JsonApiClient::Schema::Property,
139
+ ).void
140
+ end
141
+ def generate_methods_for_property(mod, property)
142
+ type = type_for(property)
143
+
144
+ name = property.name.to_s
145
+
146
+ mod.create_method(name, return_type: type)
147
+ mod.create_method("#{name}=", parameters: [create_param(name, type: type)], return_type: type)
148
+ end
149
+
150
+ sig { params(property: ::JsonApiClient::Schema::Property).returns(String) }
151
+ def type_for(property)
152
+ type = ::JsonApiClient::Schema::TypeFactory.type_for(property.type)
153
+ return "T.untyped" if type.nil?
154
+
155
+ sorbet_type = if type.respond_to?(:sorbet_type)
156
+ type.sorbet_type
157
+ elsif type == ::JsonApiClient::Schema::Types::Integer
158
+ "::Integer"
159
+ elsif type == ::JsonApiClient::Schema::Types::String
160
+ "::String"
161
+ elsif type == ::JsonApiClient::Schema::Types::Float
162
+ "::Float"
163
+ elsif type == ::JsonApiClient::Schema::Types::Time
164
+ "::Time"
165
+ elsif type == ::JsonApiClient::Schema::Types::Decimal
166
+ "::BigDecimal"
167
+ elsif type == ::JsonApiClient::Schema::Types::Boolean
168
+ "T::Boolean"
169
+ else
170
+ "T.untyped"
171
+ end
172
+
173
+ if property.default.nil?
174
+ as_nilable_type(sorbet_type)
175
+ else
176
+ sorbet_type
177
+ end
178
+ end
179
+
180
+ sig do
181
+ params(
182
+ mod: RBI::Scope,
183
+ association: JsonApiClient::Associations::BaseAssociation,
184
+ ).void
185
+ end
186
+ def generate_methods_for_association(mod, association)
187
+ # If the association is broken, it will raise a NameError when trying to access the association_class
188
+ klass = association.association_class
189
+
190
+ name, type = case association
191
+ when ::JsonApiClient::Associations::BelongsTo::Association
192
+ # id must be a string: # https://jsonapi.org/format/#document-resource-object-identification
193
+ [association.param.to_s, "T.nilable(::String)"]
194
+ when ::JsonApiClient::Associations::HasOne::Association
195
+ [association.attr_name.to_s, "T.nilable(#{klass})"]
196
+ when ::JsonApiClient::Associations::HasMany::Association
197
+ [association.attr_name.to_s, "T.nilable(T::Array[#{klass}])"]
198
+ else
199
+ return # Unsupported association type
200
+ end
201
+
202
+ mod.create_method(name, return_type: type)
203
+ mod.create_method("#{name}=", parameters: [create_param(name, type: type)], return_type: type)
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -250,27 +250,21 @@ module Tapioca
250
250
  value_type = type_of(value)
251
251
  type = "Google::Protobuf::Map[#{key_type}, #{value_type}]"
252
252
 
253
- default_args = [key.type.inspect, value.type.inspect]
254
- default_args << value_type if [:enum, :message].include?(value.type)
255
-
256
253
  Field.new(
257
254
  name: descriptor.name,
258
255
  type: type,
259
256
  init_type: "T.nilable(T.any(#{type}, T::Hash[#{key_type}, #{value_type}]))",
260
- default: "Google::Protobuf::Map.new(#{default_args.join(", ")})",
257
+ default: "T.unsafe(nil)",
261
258
  )
262
259
  else
263
260
  elem_type = type_of(descriptor)
264
261
  type = "Google::Protobuf::RepeatedField[#{elem_type}]"
265
262
 
266
- default_args = [descriptor.type.inspect]
267
- default_args << elem_type if [:enum, :message].include?(descriptor.type)
268
-
269
263
  Field.new(
270
264
  name: descriptor.name,
271
265
  type: type,
272
266
  init_type: "T.nilable(T.any(#{type}, T::Array[#{elem_type}]))",
273
- default: "Google::Protobuf::RepeatedField.new(#{default_args.join(", ")})",
267
+ default: "T.unsafe(nil)",
274
268
  )
275
269
  end
276
270
  else
@@ -13,8 +13,26 @@ module Tapioca
13
13
  @constant = constant
14
14
  end
15
15
 
16
+ sig { params(attribute_name: String, column_name: String).returns([String, String]) }
17
+ def type_for(attribute_name, column_name = attribute_name)
18
+ return id_type if attribute_name == "id"
19
+
20
+ column_type_for(column_name)
21
+ end
22
+
23
+ private
24
+
25
+ sig { returns([String, String]) }
26
+ def id_type
27
+ if @constant.respond_to?(:composite_primary_key?) && T.unsafe(@constant).composite_primary_key?
28
+ @constant.primary_key.map(&method(:column_type_for)).map { |tuple| "[#{tuple.join(", ")}]" }
29
+ else
30
+ column_type_for(@constant.primary_key)
31
+ end
32
+ end
33
+
16
34
  sig { params(column_name: String).returns([String, String]) }
17
- def type_for(column_name)
35
+ def column_type_for(column_name)
18
36
  return ["T.untyped", "T.untyped"] if do_not_generate_strong_types?(@constant)
19
37
 
20
38
  column = @constant.columns_hash[column_name]
@@ -33,7 +51,7 @@ module Tapioca
33
51
  return [getter_type, as_nilable_type(setter_type)]
34
52
  end
35
53
 
36
- if column_name == @constant.primary_key ||
54
+ if Array(@constant.primary_key).include?(column_name) ||
37
55
  column_name == "created_at" ||
38
56
  column_name == "updated_at"
39
57
  getter_type = as_nilable_type(getter_type)
@@ -42,8 +60,6 @@ module Tapioca
42
60
  [getter_type, setter_type]
43
61
  end
44
62
 
45
- private
46
-
47
63
  sig { params(column_type: T.untyped).returns(String) }
48
64
  def type_for_activerecord_value(column_type)
49
65
  case column_type