sorbet-rails 0.6.5.1 → 0.7.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (199) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +1 -2
  3. data/.gitignore +2 -1
  4. data/.travis.yml +1 -1
  5. data/README.md +66 -13
  6. data/Rakefile +3 -3
  7. data/lib/bundled_rbi/customizabel_rbi_formatter.rbi +29 -0
  8. data/lib/bundled_rbi/pluck_to_tstruct.rbi +2 -1
  9. data/lib/bundled_rbi/typed_enum.rbi +7 -0
  10. data/lib/sorbet-rails.rb +1 -3
  11. data/lib/sorbet-rails/active_record_rbi_formatter.rb +6 -6
  12. data/lib/sorbet-rails/config.rb +12 -0
  13. data/lib/sorbet-rails/dependent_gem_rbis/activerecord.rbi +3 -0
  14. data/lib/sorbet-rails/dependent_gem_rbis/parlour.rbi +1896 -0
  15. data/lib/sorbet-rails/deprecation.rb +1 -0
  16. data/lib/sorbet-rails/gem_plugins/attr_json_plugin.rb +137 -0
  17. data/lib/sorbet-rails/gem_plugins/flag_shih_tzu_plugin.rb +201 -0
  18. data/lib/sorbet-rails/gem_plugins/kaminari_plugin.rb +28 -0
  19. data/lib/sorbet-rails/gem_plugins/shrine_plugin.rb +1 -1
  20. data/lib/sorbet-rails/helper_rbi_formatter.rb +1 -1
  21. data/lib/sorbet-rails/job_rbi_formatter.rb +75 -62
  22. data/lib/sorbet-rails/mailer_rbi_formatter.rb +40 -27
  23. data/lib/sorbet-rails/model_column_utils.rb +129 -0
  24. data/lib/sorbet-rails/model_plugins/active_record_assoc.rb +40 -1
  25. data/lib/sorbet-rails/model_plugins/active_record_attribute.rb +12 -112
  26. data/lib/sorbet-rails/model_plugins/active_record_serialized_attribute.rb +68 -0
  27. data/lib/sorbet-rails/model_plugins/base.rb +6 -8
  28. data/lib/sorbet-rails/model_plugins/plugins.rb +9 -0
  29. data/lib/sorbet-rails/model_rbi_formatter.rb +3 -3
  30. data/lib/sorbet-rails/model_utils.rb +5 -2
  31. data/lib/sorbet-rails/rails_mixins/generated_url_helpers.rb +2 -3
  32. data/lib/sorbet-rails/rails_mixins/pluck_to_tstruct.rb +54 -7
  33. data/lib/sorbet-rails/railtie.rb +0 -2
  34. data/lib/sorbet-rails/routes_rbi_formatter.rb +6 -2
  35. data/lib/sorbet-rails/sorbet_utils.rb +152 -150
  36. data/lib/sorbet-rails/tasks/rails_rbi.rake +9 -8
  37. data/sorbet-rails.gemspec +2 -2
  38. data/spec/bin/run_spec.sh +1 -1
  39. data/spec/generators/rails-template.rb +16 -0
  40. data/spec/generators/sorbet_test_cases.rb +28 -56
  41. data/spec/job_rbi_formatter_spec.rb +1 -1
  42. data/spec/pluck_to_tstruct_spec.rb +115 -16
  43. data/spec/rails_helper.rb +3 -7
  44. data/spec/rake_rails_rbi_jobs_spec.rb +20 -0
  45. data/spec/rake_rails_rbi_mailers_spec.rb +21 -0
  46. data/spec/sorbet_spec.rb +5 -5
  47. data/spec/support/v5.0/Gemfile +1 -1
  48. data/spec/support/v5.0/Gemfile.lock +23 -19
  49. data/spec/support/v5.0/app/controllers/application_controller.rb +1 -1
  50. data/spec/support/v5.0/app/models/headmaster.rb +1 -1
  51. data/spec/support/v5.0/app/models/potion.rb +1 -1
  52. data/spec/support/v5.0/app/models/robe.rb +1 -1
  53. data/spec/support/v5.0/app/models/school.rb +1 -1
  54. data/spec/support/v5.0/app/models/spell.rb +1 -1
  55. data/spec/support/v5.0/app/models/subject.rb +1 -1
  56. data/spec/support/v5.0/app/models/wizard.rb +5 -0
  57. data/spec/support/v5.0/config/environments/development.rb +1 -1
  58. data/spec/support/v5.0/config/environments/production.rb +1 -1
  59. data/spec/support/v5.0/config/environments/test.rb +1 -1
  60. data/spec/support/v5.0/config/routes.rb +1 -1
  61. data/spec/support/v5.0/db/migrate/20190620000015_add_serialized_to_wizards.rb +9 -0
  62. data/spec/support/v5.0/db/schema.rb +8 -4
  63. data/spec/support/v5.0/sorbet_test_cases.rb +28 -56
  64. data/spec/support/v5.1/Gemfile.lock +21 -17
  65. data/spec/support/v5.1/app/controllers/application_controller.rb +1 -1
  66. data/spec/support/v5.1/app/models/headmaster.rb +1 -1
  67. data/spec/support/v5.1/app/models/school.rb +1 -1
  68. data/spec/support/v5.1/app/models/wizard.rb +5 -0
  69. data/spec/support/v5.1/config/environments/production.rb +1 -1
  70. data/spec/support/v5.1/config/routes.rb +1 -1
  71. data/spec/support/v5.1/db/migrate/20190620000015_add_serialized_to_wizards.rb +9 -0
  72. data/spec/support/v5.1/db/schema.rb +5 -1
  73. data/spec/support/v5.1/sorbet_test_cases.rb +28 -56
  74. data/spec/support/v5.2/Gemfile +1 -1
  75. data/spec/support/v5.2/Gemfile.lock +23 -19
  76. data/spec/support/v5.2/app/models/headmaster.rb +1 -1
  77. data/spec/support/v5.2/app/models/school.rb +1 -1
  78. data/spec/support/v5.2/app/models/wizard.rb +5 -0
  79. data/spec/support/v5.2/config/environments/development.rb +1 -1
  80. data/spec/support/v5.2/config/environments/production.rb +1 -1
  81. data/spec/support/v5.2/config/environments/test.rb +1 -1
  82. data/spec/support/v5.2/config/routes.rb +1 -1
  83. data/spec/support/v5.2/db/migrate/20190620000015_add_serialized_to_wizards.rb +9 -0
  84. data/spec/support/v5.2/db/schema.rb +5 -1
  85. data/spec/support/v5.2/sorbet_test_cases.rb +28 -56
  86. data/spec/support/v6.0/.gitignore +6 -0
  87. data/spec/support/v6.0/Gemfile +3 -3
  88. data/spec/support/v6.0/Gemfile.lock +89 -84
  89. data/spec/support/v6.0/app/models/wizard.rb +5 -0
  90. data/spec/support/v6.0/bin/bundle +22 -13
  91. data/spec/support/v6.0/config/environments/development.rb +1 -1
  92. data/spec/support/v6.0/config/environments/production.rb +1 -1
  93. data/spec/support/v6.0/config/environments/test.rb +2 -2
  94. data/spec/support/v6.0/config/routes.rb +1 -1
  95. data/spec/support/v6.0/db/migrate/20190620000015_add_serialized_to_wizards.rb +9 -0
  96. data/spec/support/v6.0/db/schema.rb +5 -1
  97. data/spec/support/v6.0/sorbet_test_cases.rb +28 -56
  98. data/spec/support/v6.0/tmp/pids/.keep +0 -0
  99. data/spec/test_data/v5.0/expected_active_record_relation.rbi +0 -1
  100. data/spec/test_data/v5.0/expected_application_job.rbi +2 -2
  101. data/spec/test_data/v5.0/expected_award_house_point_hourglasses.rbi +2 -2
  102. data/spec/test_data/v5.0/expected_custom_application_job.rbi +21 -0
  103. data/spec/test_data/v5.0/expected_custom_application_mailer.rbi +6 -0
  104. data/spec/test_data/v5.0/expected_custom_award_house_point_hourglasses.rbi +21 -0
  105. data/spec/test_data/v5.0/expected_custom_daily_prophet_mailer.rbi +8 -0
  106. data/spec/test_data/v5.0/expected_custom_hogwarts_acceptance_mailer.rbi +21 -0
  107. data/spec/test_data/v5.0/expected_headmaster.rbi +24 -0
  108. data/spec/test_data/v5.0/expected_potion.rbi +12 -0
  109. data/spec/test_data/v5.0/expected_robe.rbi +12 -0
  110. data/spec/test_data/v5.0/expected_routes.rbi +4 -0
  111. data/spec/test_data/v5.0/expected_school.rbi +12 -0
  112. data/spec/test_data/v5.0/expected_spell/habtm_spell_books.rbi +24 -0
  113. data/spec/test_data/v5.0/expected_spell_book.rbi +21 -9
  114. data/spec/test_data/v5.0/expected_spell_book/habtm_spells.rbi +24 -0
  115. data/spec/test_data/v5.0/expected_squib.rbi +63 -0
  116. data/spec/test_data/v5.0/expected_subject/habtm_wizards.rbi +24 -0
  117. data/spec/test_data/v5.0/expected_wand.rbi +22 -10
  118. data/spec/test_data/v5.0/expected_wizard.rbi +121 -58
  119. data/spec/test_data/v5.0/expected_wizard/habtm_subjects.rbi +24 -0
  120. data/spec/test_data/v5.0/expected_wizard_wo_spellbook.rbi +121 -58
  121. data/spec/test_data/v5.1/expected_active_record_relation.rbi +0 -1
  122. data/spec/test_data/v5.1/expected_application_job.rbi +2 -2
  123. data/spec/test_data/v5.1/expected_award_house_point_hourglasses.rbi +2 -2
  124. data/spec/test_data/v5.1/expected_custom_application_job.rbi +21 -0
  125. data/spec/test_data/v5.1/expected_custom_application_mailer.rbi +6 -0
  126. data/spec/test_data/v5.1/expected_custom_award_house_point_hourglasses.rbi +21 -0
  127. data/spec/test_data/v5.1/expected_custom_daily_prophet_mailer.rbi +8 -0
  128. data/spec/test_data/v5.1/expected_custom_hogwarts_acceptance_mailer.rbi +21 -0
  129. data/spec/test_data/v5.1/expected_headmaster.rbi +24 -0
  130. data/spec/test_data/v5.1/expected_potion.rbi +12 -0
  131. data/spec/test_data/v5.1/expected_robe.rbi +12 -0
  132. data/spec/test_data/v5.1/expected_routes.rbi +4 -0
  133. data/spec/test_data/v5.1/expected_school.rbi +12 -0
  134. data/spec/test_data/v5.1/expected_spell/habtm_spell_books.rbi +24 -0
  135. data/spec/test_data/v5.1/expected_spell_book.rbi +21 -9
  136. data/spec/test_data/v5.1/expected_spell_book/habtm_spells.rbi +24 -0
  137. data/spec/test_data/v5.1/expected_squib.rbi +63 -0
  138. data/spec/test_data/v5.1/expected_subject/habtm_wizards.rbi +24 -0
  139. data/spec/test_data/v5.1/expected_wand.rbi +22 -10
  140. data/spec/test_data/v5.1/expected_wizard.rbi +121 -58
  141. data/spec/test_data/v5.1/expected_wizard/habtm_subjects.rbi +24 -0
  142. data/spec/test_data/v5.1/expected_wizard_wo_spellbook.rbi +121 -58
  143. data/spec/test_data/v5.2/expected_active_record_relation.rbi +0 -1
  144. data/spec/test_data/v5.2/expected_application_job.rbi +2 -2
  145. data/spec/test_data/v5.2/expected_attachment.rbi +24 -0
  146. data/spec/test_data/v5.2/expected_award_house_point_hourglasses.rbi +2 -2
  147. data/spec/test_data/v5.2/expected_blob.rbi +25 -1
  148. data/spec/test_data/v5.2/expected_custom_application_job.rbi +21 -0
  149. data/spec/test_data/v5.2/expected_custom_application_mailer.rbi +6 -0
  150. data/spec/test_data/v5.2/expected_custom_award_house_point_hourglasses.rbi +21 -0
  151. data/spec/test_data/v5.2/expected_custom_daily_prophet_mailer.rbi +8 -0
  152. data/spec/test_data/v5.2/expected_custom_hogwarts_acceptance_mailer.rbi +21 -0
  153. data/spec/test_data/v5.2/expected_headmaster.rbi +24 -0
  154. data/spec/test_data/v5.2/expected_potion.rbi +12 -0
  155. data/spec/test_data/v5.2/expected_robe.rbi +12 -0
  156. data/spec/test_data/v5.2/expected_routes.rbi +4 -0
  157. data/spec/test_data/v5.2/expected_school.rbi +12 -0
  158. data/spec/test_data/v5.2/expected_spell/habtm_spell_books.rbi +24 -0
  159. data/spec/test_data/v5.2/expected_spell_book.rbi +21 -9
  160. data/spec/test_data/v5.2/expected_spell_book/habtm_spells.rbi +24 -0
  161. data/spec/test_data/v5.2/expected_squib.rbi +89 -2
  162. data/spec/test_data/v5.2/expected_subject/habtm_wizards.rbi +24 -0
  163. data/spec/test_data/v5.2/expected_wand.rbi +22 -10
  164. data/spec/test_data/v5.2/expected_wizard.rbi +147 -60
  165. data/spec/test_data/v5.2/expected_wizard/habtm_subjects.rbi +24 -0
  166. data/spec/test_data/v5.2/expected_wizard_wo_spellbook.rbi +147 -60
  167. data/spec/test_data/v6.0/expected_active_record_relation.rbi +0 -1
  168. data/spec/test_data/v6.0/expected_application_job.rbi +2 -2
  169. data/spec/test_data/v6.0/expected_attachment.rbi +24 -0
  170. data/spec/test_data/v6.0/expected_award_house_point_hourglasses.rbi +2 -2
  171. data/spec/test_data/v6.0/expected_blob.rbi +25 -1
  172. data/spec/test_data/v6.0/expected_custom_application_job.rbi +21 -0
  173. data/spec/test_data/v6.0/expected_custom_application_mailer.rbi +6 -0
  174. data/spec/test_data/v6.0/expected_custom_award_house_point_hourglasses.rbi +21 -0
  175. data/spec/test_data/v6.0/expected_custom_daily_prophet_mailer.rbi +8 -0
  176. data/spec/test_data/v6.0/expected_custom_hogwarts_acceptance_mailer.rbi +21 -0
  177. data/spec/test_data/v6.0/expected_headmaster.rbi +24 -0
  178. data/spec/test_data/v6.0/expected_potion.rbi +12 -0
  179. data/spec/test_data/v6.0/expected_robe.rbi +12 -0
  180. data/spec/test_data/v6.0/expected_routes.rbi +4 -0
  181. data/spec/test_data/v6.0/expected_school.rbi +12 -0
  182. data/spec/test_data/v6.0/expected_spell/habtm_spell_books.rbi +24 -0
  183. data/spec/test_data/v6.0/expected_spell_book.rbi +21 -9
  184. data/spec/test_data/v6.0/expected_spell_book/habtm_spells.rbi +24 -0
  185. data/spec/test_data/v6.0/expected_squib.rbi +89 -2
  186. data/spec/test_data/v6.0/expected_subject/habtm_wizards.rbi +24 -0
  187. data/spec/test_data/v6.0/expected_wand.rbi +22 -10
  188. data/spec/test_data/v6.0/expected_wizard.rbi +147 -60
  189. data/spec/test_data/v6.0/expected_wizard/habtm_subjects.rbi +24 -0
  190. data/spec/test_data/v6.0/expected_wizard_wo_spellbook.rbi +147 -60
  191. data/spec/tstruct_comparable.rb +13 -0
  192. metadata +64 -15
  193. data/lib/bundled_rbi/parameters.rbi +0 -28
  194. data/lib/sorbet-rails/custom_types/boolean_string.rb +0 -42
  195. data/lib/sorbet-rails/custom_types/integer_string.rb +0 -45
  196. data/lib/sorbet-rails/rails_mixins/custom_params_methods.rb +0 -57
  197. data/spec/boolean_string_spec.rb +0 -59
  198. data/spec/custom_params_methods_spec.rb +0 -138
  199. data/spec/integer_string_spec.rb +0 -46
@@ -2,37 +2,50 @@
2
2
  require('parlour')
3
3
  require('sorbet-rails/sorbet_utils.rb')
4
4
 
5
- class SorbetRails::MailerRbiFormatter
6
- extend T::Sig
5
+ module SorbetRails
6
+ class MailerRbiFormatter
7
+ extend T::Sig
7
8
 
8
- sig { params(mailer_class: T.class_of(ActionMailer::Base)).void }
9
- def initialize(mailer_class)
10
- @mailer_class = T.let(mailer_class, T.class_of(ActionMailer::Base))
11
- @parlour = T.let(Parlour::RbiGenerator.new, Parlour::RbiGenerator)
12
- end
9
+ Parameter = ::Parlour::RbiGenerator::Parameter
10
+
11
+ sig { returns(Parlour::RbiGenerator) }
12
+ attr_reader :rbi_generator
13
13
 
14
- sig {returns(String)}
15
- def generate_rbi
16
- puts "-- Generate sigs for mailer #{@mailer_class.name} --"
17
-
18
- @parlour.root.add_comments([
19
- "This is an autogenerated file for Rails' mailers.",
20
- 'Please rerun bundle exec rake rails_rbi:mailers to regenerate.'
21
- ])
22
-
23
- @parlour.root.create_class(@mailer_class.name) do |mailer_rbi|
24
- @mailer_class.action_methods.to_a.sort.each do |mailer_method|
25
- method_def = @mailer_class.instance_method(mailer_method)
26
- parameters = SorbetRails::SorbetUtils.parameters_from_method_def(method_def)
27
- mailer_rbi.create_method(
28
- mailer_method,
29
- parameters: parameters,
30
- return_type: 'ActionMailer::MessageDelivery',
31
- class_method: true,
32
- )
14
+ sig { returns(T.class_of(ActionMailer::Base)) }
15
+ attr_reader :mailer_class
16
+
17
+ sig { params(mailer_class: T.class_of(ActionMailer::Base)).void }
18
+ def initialize(mailer_class)
19
+ @mailer_class = T.let(mailer_class, T.class_of(ActionMailer::Base))
20
+ @rbi_generator = T.let(Parlour::RbiGenerator.new, Parlour::RbiGenerator)
21
+ end
22
+
23
+ sig { void }
24
+ def populate_rbi
25
+ @rbi_generator.root.add_comment([
26
+ "This is an autogenerated file for Rails' mailers.",
27
+ 'Please rerun bundle exec rake rails_rbi:mailers to regenerate.'
28
+ ])
29
+
30
+ @rbi_generator.root.create_class(T.must(@mailer_class.name)) do |mailer_rbi|
31
+ @mailer_class.action_methods.to_a.sort.each do |mailer_method|
32
+ method_def = @mailer_class.instance_method(mailer_method)
33
+ parameters = SorbetRails::SorbetUtils.parameters_from_method_def(method_def)
34
+ mailer_rbi.create_method(
35
+ mailer_method.to_s,
36
+ parameters: parameters,
37
+ return_type: 'ActionMailer::MessageDelivery',
38
+ class_method: true,
39
+ )
40
+ end
33
41
  end
34
42
  end
35
43
 
36
- @parlour.rbi
44
+ sig { returns(String) }
45
+ def generate_rbi
46
+ puts "-- Generate sigs for mailer #{@mailer_class.name} --"
47
+ populate_rbi
48
+ @rbi_generator.rbi
49
+ end
37
50
  end
38
51
  end
@@ -0,0 +1,129 @@
1
+ # typed: strict
2
+ module SorbetRails::ModelColumnUtils
3
+ extend T::Sig
4
+ extend T::Helpers
5
+
6
+ abstract!
7
+
8
+ class ColumnType < T::Struct
9
+ extend T::Sig
10
+
11
+ const :base_type, T.any(Class, String)
12
+ const :nilable, T.nilable(T::Boolean)
13
+ const :array_type, T.nilable(T::Boolean)
14
+
15
+ sig { returns(String) }
16
+ def to_s
17
+ type = base_type.to_s
18
+ # A nullable array column should be T.nilable(T::Array[column_type]) not T::Array[T.nilable(column_type)]
19
+ type = "T::Array[#{type}]" if array_type
20
+ type = "T.nilable(#{type})" if nilable
21
+ type
22
+ end
23
+ end
24
+
25
+ # if we're a HABTM class then model_class is an anonymous class (see the rails link below) and
26
+ # i'm not sure how to explain that to sorbet other than T.class_of(Class).
27
+ sig { abstract.returns(T.any(T.class_of(ActiveRecord::Base), T.class_of(Class))) }
28
+ def model_class; end
29
+
30
+ sig { params(column_def: T.untyped).returns(ColumnType) }
31
+ def type_for_column_def(column_def)
32
+ cast_type = ActiveRecord::Base.connection.respond_to?(:lookup_cast_type_from_column) ?
33
+ ActiveRecord::Base.connection.lookup_cast_type_from_column(column_def) :
34
+ column_def.cast_type
35
+
36
+ array_type = false
37
+ if column_def.try(:array?)
38
+ cast_type = cast_type.subtype if cast_type.try(:subtype)
39
+ array_type = true
40
+ end
41
+ strict_type =
42
+ active_record_type_to_sorbet_type(
43
+ cast_type,
44
+ time_zone_aware: time_zone_aware_column?(column_def, cast_type),
45
+ )
46
+
47
+ ColumnType.new(
48
+ base_type: strict_type,
49
+ nilable: nilable_column?(column_def),
50
+ array_type: array_type,
51
+ )
52
+ end
53
+
54
+ # True if this column is "time zone aware", which means it'll be converted on
55
+ # access from its original class (e.g. `DateTime`) to something with better
56
+ # support for time zones (usually `ActiveSupport::TimeWithZone`)
57
+ sig do
58
+ params(column_def: T.untyped, cast_type: T.untyped).returns(T::Boolean)
59
+ end
60
+ def time_zone_aware_column?(column_def, cast_type)
61
+ # this private class method returns true if the attribute should be "time
62
+ # zone aware"; it takes into account various global and model-specific
63
+ # configuration options as described here:
64
+ # https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html
65
+ #
66
+ # although it's private, it's better this than trying to reimplement the "is
67
+ # this attribute tz aware?" logic ourselves
68
+ model_class.send(
69
+ :create_time_zone_conversion_attribute?,
70
+ column_def.name,
71
+ cast_type
72
+ )
73
+ end
74
+
75
+ sig { params(column_def: T.untyped).returns(T::Boolean) }
76
+ def nilable_column?(column_def)
77
+ !!(column_def.null && !attribute_has_unconditional_presence_validation?(column_def.name))
78
+ end
79
+
80
+ sig do
81
+ params(
82
+ klass: Object,
83
+ time_zone_aware: T::Boolean,
84
+ ).returns(T.any(String, Class))
85
+ end
86
+ def active_record_type_to_sorbet_type(klass, time_zone_aware: false)
87
+ case klass
88
+ when ActiveRecord::Type::Boolean
89
+ "T::Boolean"
90
+ when ActiveRecord::Type::DateTime, ActiveRecord::Type::Time
91
+ time_zone_aware ? ActiveSupport::TimeWithZone : Time
92
+ when ActiveRecord::Type::Date
93
+ Date
94
+ when ActiveRecord::Type::Decimal
95
+ BigDecimal
96
+ when ActiveRecord::Type::Float
97
+ Float
98
+ when ActiveRecord::Type::BigInteger, ActiveRecord::Type::Integer, ActiveRecord::Type::DecimalWithoutScale, ActiveRecord::Type::UnsignedInteger
99
+ Integer
100
+ when ActiveRecord::Type::Binary, ActiveRecord::Type::String, ActiveRecord::Type::Text
101
+ String
102
+ else
103
+ # Json type is only supported in Rails 5.2 and above
104
+ case
105
+ when Object.const_defined?('ActiveRecord::Type::Json') && klass.is_a?(ActiveRecord::Type::Json)
106
+ "T.any(T::Array[T.untyped], T::Boolean, Float, T::Hash[T.untyped, T.untyped], Integer, String)"
107
+ when Object.const_defined?('ActiveRecord::Enum::EnumType') && klass.is_a?(ActiveRecord::Enum::EnumType)
108
+ String
109
+ # For Postgres UUIDs, they're represented as Strings
110
+ when Object.const_defined?('ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid') && klass.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid)
111
+ String
112
+ else
113
+ "T.untyped"
114
+ end
115
+ end
116
+ end
117
+
118
+ sig { params(attribute: T.any(String, Symbol)).returns(T::Boolean) }
119
+ def attribute_has_unconditional_presence_validation?(attribute)
120
+ model_class_srb = model_class
121
+ model_class_srb < ActiveRecord::Base &&
122
+ model_class_srb.validators_on(attribute).any? do |validator|
123
+ validator.is_a?(ActiveModel::Validations::PresenceValidator) &&
124
+ !validator.options.key?(:if) &&
125
+ !validator.options.key?(:unless) &&
126
+ !validator.options.key?(:on)
127
+ end
128
+ end
129
+ end
@@ -36,10 +36,30 @@ class SorbetRails::ModelPlugins::ActiveRecordAssoc < SorbetRails::ModelPlugins::
36
36
  assoc_class = assoc_should_be_untyped?(reflection) ? "T.untyped" : "::#{reflection.klass.name}"
37
37
  assoc_type = (belongs_to_and_required?(reflection) || has_one_and_required?(reflection)) ? assoc_class : "T.nilable(#{assoc_class})"
38
38
 
39
+ params = [
40
+ Parameter.new("*args", type: "T.untyped"),
41
+ Parameter.new("&block", type: "T.nilable(T.proc.params(object: #{assoc_class}).void)")
42
+ ]
43
+
39
44
  assoc_module_rbi.create_method(
40
45
  assoc_name.to_s,
41
46
  return_type: assoc_type,
42
47
  )
48
+ assoc_module_rbi.create_method(
49
+ "build_#{assoc_name}",
50
+ parameters: params,
51
+ return_type: assoc_class,
52
+ )
53
+ assoc_module_rbi.create_method(
54
+ "create_#{assoc_name}",
55
+ parameters: params,
56
+ return_type: assoc_class,
57
+ )
58
+ assoc_module_rbi.create_method(
59
+ "create_#{assoc_name}!",
60
+ parameters: params,
61
+ return_type: assoc_class,
62
+ )
43
63
  assoc_module_rbi.create_method(
44
64
  "#{assoc_name}=",
45
65
  parameters: [
@@ -47,6 +67,10 @@ class SorbetRails::ModelPlugins::ActiveRecordAssoc < SorbetRails::ModelPlugins::
47
67
  ],
48
68
  return_type: nil,
49
69
  )
70
+ assoc_module_rbi.create_method(
71
+ "reload_#{assoc_name}",
72
+ return_type: assoc_type,
73
+ )
50
74
  end
51
75
 
52
76
  sig { params(reflection: T.untyped).returns(T::Boolean) }
@@ -114,9 +138,24 @@ class SorbetRails::ModelPlugins::ActiveRecordAssoc < SorbetRails::ModelPlugins::
114
138
  return_type: relation_class,
115
139
  )
116
140
  unless assoc_should_be_untyped?(reflection)
141
+ id_type = "T.untyped"
142
+
143
+ if reflection.klass.table_exists?
144
+ # For DB views, the PK column would not exist.
145
+ id_column = reflection.klass.primary_key
146
+
147
+ if id_column
148
+ id_column_def = reflection.klass.columns_hash[id_column]
149
+
150
+ # Normally the id_type is an Integer, but it could be a String if using
151
+ # UUIDs.
152
+ id_type = type_for_column_def(id_column_def).to_s if id_column_def
153
+ end
154
+ end
155
+
117
156
  assoc_module_rbi.create_method(
118
157
  "#{assoc_name.singularize}_ids",
119
- return_type: "T::Array[Integer]",
158
+ return_type: "T::Array[#{id_type}]",
120
159
  )
121
160
  end
122
161
  assoc_module_rbi.create_method(
@@ -2,23 +2,6 @@
2
2
  require ('sorbet-rails/model_plugins/base')
3
3
  class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugins::Base
4
4
 
5
- class ColumnType < T::Struct
6
- extend T::Sig
7
-
8
- const :base_type, T.any(Class, String)
9
- const :nilable, T.nilable(T::Boolean)
10
- const :array_type, T.nilable(T::Boolean)
11
-
12
- sig { returns(String) }
13
- def to_s
14
- type = base_type.to_s
15
- # A nullable array column should be T.nilable(T::Array[column_type]) not T::Array[T.nilable(column_type)]
16
- type = "T::Array[#{type}]" if array_type
17
- type = "T.nilable(#{type})" if nilable
18
- type
19
- end
20
- end
21
-
22
5
  sig { override.params(root: Parlour::RbiGenerator::Namespace).void }
23
6
  def generate(root)
24
7
  columns_hash = @model_class.table_exists? ? @model_class.columns_hash : {}
@@ -41,6 +24,8 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
41
24
  column_name,
42
25
  column_def,
43
26
  )
27
+ elsif serialization_coder_for_column(column_name)
28
+ next # handled by the ActiveRecordSerializedAttribute plugin
44
29
  else
45
30
  column_type = type_for_column_def(column_def)
46
31
  attribute_module_rbi.create_method(
@@ -90,18 +75,18 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
90
75
  # do not generate the methods for enums in strict_mode
91
76
  should_skip_setter_getter = config.strict_mode
92
77
 
93
- t_enum_type = "#{model_class_name}::#{config.class_name}"
94
-
95
- # define T::Enum class & values
96
- enum_values = T.must(model_defined_enums[column_name])
97
- t_enum_values = @model_class.gen_typed_enum_values(enum_values.keys)
98
- root.create_enum_class(
99
- t_enum_type,
100
- enums: t_enum_values.map { |k, v| [v, "%q{#{k}}"] },
101
- )
78
+ root.create_class(model_class_name) do |model_class|
79
+ # define T::Enum class & values
80
+ enum_values = T.must(model_defined_enums[column_name])
81
+ t_enum_values = @model_class.gen_typed_enum_values(enum_values.keys)
82
+ model_class.create_enum_class(
83
+ config.class_name,
84
+ enums: t_enum_values.map { |k, v| [v, "%q{#{k}}"] },
85
+ )
86
+ end
102
87
 
103
88
  # define t_enum setter/getter
104
- assignable_type = t_enum_type
89
+ assignable_type = "#{model_class_name}::#{config.class_name}"
105
90
  assignable_type = "T.nilable(#{assignable_type})" if nilable_column
106
91
  # add directly to model_class_rbi because they are included
107
92
  # by sorbet's hidden.rbi
@@ -140,91 +125,6 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
140
125
  end
141
126
  end
142
127
 
143
- sig { params(column_def: T.untyped).returns(ColumnType) }
144
- def type_for_column_def(column_def)
145
- cast_type = ActiveRecord::Base.connection.respond_to?(:lookup_cast_type_from_column) ?
146
- ActiveRecord::Base.connection.lookup_cast_type_from_column(column_def) :
147
- column_def.cast_type
148
-
149
- array_type = false
150
- if column_def.try(:array?)
151
- cast_type = cast_type.subtype if cast_type.try(:subtype)
152
- array_type = true
153
- end
154
- strict_type =
155
- active_record_type_to_sorbet_type(
156
- cast_type,
157
- time_zone_aware: time_zone_aware_column?(column_def, cast_type),
158
- )
159
-
160
- ColumnType.new(
161
- base_type: strict_type,
162
- nilable: nilable_column?(column_def),
163
- array_type: array_type,
164
- )
165
- end
166
-
167
- sig do
168
- params(
169
- klass: Object,
170
- time_zone_aware: T::Boolean,
171
- ).returns(T.any(String, Class))
172
- end
173
- def active_record_type_to_sorbet_type(klass, time_zone_aware: false)
174
- case klass
175
- when ActiveRecord::Type::Boolean
176
- "T::Boolean"
177
- when ActiveRecord::Type::DateTime, ActiveRecord::Type::Time
178
- time_zone_aware ? ActiveSupport::TimeWithZone : Time
179
- when ActiveRecord::Type::Date
180
- Date
181
- when ActiveRecord::Type::Decimal
182
- BigDecimal
183
- when ActiveRecord::Type::Float
184
- Float
185
- when ActiveRecord::Type::BigInteger, ActiveRecord::Type::Integer, ActiveRecord::Type::DecimalWithoutScale, ActiveRecord::Type::UnsignedInteger
186
- Integer
187
- when ActiveRecord::Type::Binary, ActiveRecord::Type::String, ActiveRecord::Type::Text
188
- String
189
- else
190
- # Json type is only supported in Rails 5.2 and above
191
- case
192
- when Object.const_defined?('ActiveRecord::Type::Json') && klass.is_a?(ActiveRecord::Type::Json)
193
- "T.any(T::Array[T.untyped], T::Boolean, Float, T::Hash[T.untyped, T.untyped], Integer, String)"
194
- when Object.const_defined?('ActiveRecord::Enum::EnumType') && klass.is_a?(ActiveRecord::Enum::EnumType)
195
- String
196
- else
197
- "T.untyped"
198
- end
199
- end
200
- end
201
-
202
- # True if this column is "time zone aware", which means it'll be converted on
203
- # access from its original class (e.g. `DateTime`) to something with better
204
- # support for time zones (usually `ActiveSupport::TimeWithZone`)
205
- sig do
206
- params(column_def: T.untyped, cast_type: T.untyped).returns(T::Boolean)
207
- end
208
- def time_zone_aware_column?(column_def, cast_type)
209
- # this private class method returns true if the attribute should be "time
210
- # zone aware"; it takes into account various global and model-specific
211
- # configuration options as described here:
212
- # https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html
213
- #
214
- # although it's private, it's better this than trying to reimplement the "is
215
- # this attribute tz aware?" logic ourselves
216
- @model_class.send(
217
- :create_time_zone_conversion_attribute?,
218
- column_def.name,
219
- cast_type
220
- )
221
- end
222
-
223
- sig { params(column_def: T.untyped).returns(T::Boolean) }
224
- def nilable_column?(column_def)
225
- !!(column_def.null && !attribute_has_unconditional_presence_validation?(column_def.name))
226
- end
227
-
228
128
  sig { params(column_type: ColumnType).returns(String) }
229
129
  def value_type_for_attr_writer(column_type)
230
130
  # it's safe - and convenient - to assign any "time like" object to a time zone
@@ -0,0 +1,68 @@
1
+ # typed: strict
2
+ require ('sorbet-rails/model_plugins/base')
3
+ class SorbetRails::ModelPlugins::ActiveRecordSerializedAttribute < SorbetRails::ModelPlugins::Base
4
+
5
+ sig { override.params(root: Parlour::RbiGenerator::Namespace).void }
6
+ def generate(root)
7
+ columns_hash = @model_class.table_exists? ? @model_class.columns_hash : {}
8
+ return unless any_serialized_columns?(columns_hash)
9
+
10
+ serialize_module_name = self.model_module_name('GeneratedSerializedAttributeMethods')
11
+ serialize_module_rbi = root.create_module(serialize_module_name)
12
+
13
+ model_class_rbi = root.create_class(self.model_class_name)
14
+ model_class_rbi.create_include(serialize_module_name)
15
+
16
+ columns_hash.sort.each do |column_name, column_def|
17
+ serialization_coder = serialization_coder_for_column(column_name)
18
+ next unless serialization_coder
19
+
20
+ nilable = nilable_column?(column_def)
21
+ attr_type = attr_types_for_coder(serialization_coder)
22
+
23
+ serialize_module_rbi.create_method(
24
+ column_name.to_s,
25
+ return_type: ColumnType.new(base_type: attr_type, nilable: nilable).to_s,
26
+ )
27
+
28
+ serialize_module_rbi.create_method(
29
+ "#{column_name}=",
30
+ parameters: [
31
+ Parameter.new('value', type: ColumnType.new(base_type: attr_type, nilable: nilable).to_s)
32
+ ],
33
+ return_type: nil,
34
+ )
35
+
36
+ serialize_module_rbi.create_method(
37
+ "#{column_name}?",
38
+ return_type: 'T::Boolean',
39
+ )
40
+ end
41
+ end
42
+
43
+ sig { params(columns_hash: T::Hash[String, ActiveRecord::ConnectionAdapters::Column]).returns(T::Boolean) }
44
+ def any_serialized_columns?(columns_hash)
45
+ columns_hash.keys.any? do |column_name|
46
+ !serialization_coder_for_column(column_name).nil?
47
+ end
48
+ end
49
+
50
+ sig { params(serialization_coder: T.nilable(Class)).returns(String) }
51
+ def attr_types_for_coder(serialization_coder)
52
+ case serialization_coder.to_s
53
+ when 'Hash'
54
+ # Hash uses YAML.load/YAML.dump, and permits pretty much any kind of Hash key
55
+ 'T::Hash[T.untyped, T.untyped]'
56
+ when 'Array'
57
+ # YAML.load/YAML.dump
58
+ 'T::Array[T.untyped]'
59
+ when 'JSON'
60
+ # ActiveSupport::JSON.encode/ActiveSupport::JSON.decode
61
+ # note that Hash keys are Strings since this is JSON
62
+ 'T.any(T::Array[T.untyped], T::Boolean, Float, T::Hash[String, T.untyped], Integer, String)'
63
+ else
64
+ # unknown uses YAML.load/YAML.dump
65
+ 'T.any(T::Array[T.untyped], T::Boolean, Float, T::Hash[T.untyped, T.untyped], Integer, String)'
66
+ end
67
+ end
68
+ end