sorbet-rails 0.7.2 → 0.7.3

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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/lib/sorbet-rails/config.rb +1 -0
  4. data/lib/sorbet-rails/dependent_gem_rbis/activerecord.rbi +3 -0
  5. data/lib/sorbet-rails/model_plugins/active_record_assoc.rb +17 -5
  6. data/lib/sorbet-rails/model_plugins/active_record_attribute.rb +2 -0
  7. data/lib/sorbet-rails/model_plugins/active_record_serialized_attribute.rb +68 -0
  8. data/lib/sorbet-rails/model_plugins/base.rb +8 -0
  9. data/lib/sorbet-rails/model_plugins/plugins.rb +3 -0
  10. data/lib/sorbet-rails/model_rbi_formatter.rb +3 -3
  11. data/lib/sorbet-rails/rails_mixins/pluck_to_tstruct.rb +39 -2
  12. data/lib/sorbet-rails/routes_rbi_formatter.rb +4 -0
  13. data/lib/sorbet-rails/tasks/rails_rbi.rake +4 -3
  14. data/sorbet-rails.gemspec +1 -1
  15. data/spec/bin/run_spec.sh +1 -1
  16. data/spec/generators/rails-template.rb +16 -0
  17. data/spec/generators/sorbet_test_cases.rb +15 -0
  18. data/spec/pluck_to_tstruct_spec.rb +51 -25
  19. data/spec/sorbet_spec.rb +1 -1
  20. data/spec/support/v5.0/Gemfile.lock +4 -4
  21. data/spec/support/v5.0/app/controllers/application_controller.rb +1 -1
  22. data/spec/support/v5.0/app/models/headmaster.rb +1 -1
  23. data/spec/support/v5.0/app/models/potion.rb +1 -1
  24. data/spec/support/v5.0/app/models/robe.rb +1 -1
  25. data/spec/support/v5.0/app/models/school.rb +1 -1
  26. data/spec/support/v5.0/app/models/spell.rb +1 -1
  27. data/spec/support/v5.0/app/models/subject.rb +1 -1
  28. data/spec/support/v5.0/app/models/wizard.rb +5 -0
  29. data/spec/support/v5.0/config/environments/development.rb +1 -1
  30. data/spec/support/v5.0/config/environments/production.rb +1 -1
  31. data/spec/support/v5.0/config/environments/test.rb +1 -1
  32. data/spec/support/v5.0/config/initializers/wrap_parameters.rb +1 -1
  33. data/spec/support/v5.0/config/routes.rb +1 -1
  34. data/spec/support/v5.0/db/migrate/20190620000015_add_serialized_to_wizards.rb +9 -0
  35. data/spec/support/v5.0/db/schema.rb +8 -4
  36. data/spec/support/v5.0/sorbet_test_cases.rb +15 -0
  37. data/spec/support/v5.1/Gemfile.lock +4 -4
  38. data/spec/support/v5.1/app/controllers/application_controller.rb +1 -1
  39. data/spec/support/v5.1/app/models/headmaster.rb +1 -1
  40. data/spec/support/v5.1/app/models/school.rb +1 -1
  41. data/spec/support/v5.1/app/models/wizard.rb +5 -0
  42. data/spec/support/v5.1/config/environments/production.rb +1 -1
  43. data/spec/support/v5.1/config/initializers/wrap_parameters.rb +1 -1
  44. data/spec/support/v5.1/config/routes.rb +1 -1
  45. data/spec/support/v5.1/db/migrate/20190620000015_add_serialized_to_wizards.rb +9 -0
  46. data/spec/support/v5.1/db/schema.rb +5 -1
  47. data/spec/support/v5.1/sorbet_test_cases.rb +15 -0
  48. data/spec/support/v5.2/Gemfile.lock +4 -4
  49. data/spec/support/v5.2/app/models/headmaster.rb +1 -1
  50. data/spec/support/v5.2/app/models/school.rb +1 -1
  51. data/spec/support/v5.2/app/models/wizard.rb +5 -0
  52. data/spec/support/v5.2/config/environments/development.rb +1 -1
  53. data/spec/support/v5.2/config/environments/production.rb +1 -1
  54. data/spec/support/v5.2/config/environments/test.rb +1 -1
  55. data/spec/support/v5.2/config/initializers/wrap_parameters.rb +1 -1
  56. data/spec/support/v5.2/config/routes.rb +1 -1
  57. data/spec/support/v5.2/db/migrate/20190620000015_add_serialized_to_wizards.rb +9 -0
  58. data/spec/support/v5.2/db/schema.rb +5 -1
  59. data/spec/support/v5.2/sorbet_test_cases.rb +15 -0
  60. data/spec/support/v6.0/Gemfile.lock +4 -4
  61. data/spec/support/v6.0/app/models/wizard.rb +5 -0
  62. data/spec/support/v6.0/config/environments/development.rb +1 -1
  63. data/spec/support/v6.0/config/environments/production.rb +1 -1
  64. data/spec/support/v6.0/config/environments/test.rb +1 -1
  65. data/spec/support/v6.0/config/routes.rb +1 -1
  66. data/spec/support/v6.0/db/migrate/20190620000015_add_serialized_to_wizards.rb +9 -0
  67. data/spec/support/v6.0/db/schema.rb +5 -1
  68. data/spec/support/v6.0/sorbet_test_cases.rb +15 -0
  69. data/spec/test_data/v5.0/expected_headmaster.rbi +6 -0
  70. data/spec/test_data/v5.0/expected_potion.rbi +3 -0
  71. data/spec/test_data/v5.0/expected_robe.rbi +3 -0
  72. data/spec/test_data/v5.0/expected_routes.rbi +4 -0
  73. data/spec/test_data/v5.0/expected_school.rbi +3 -0
  74. data/spec/test_data/v5.0/expected_spell/habtm_spell_books.rbi +6 -0
  75. data/spec/test_data/v5.0/expected_spell_book.rbi +3 -0
  76. data/spec/test_data/v5.0/expected_spell_book/habtm_spells.rbi +6 -0
  77. data/spec/test_data/v5.0/expected_squib.rbi +45 -0
  78. data/spec/test_data/v5.0/expected_subject/habtm_wizards.rbi +6 -0
  79. data/spec/test_data/v5.0/expected_wand.rbi +3 -0
  80. data/spec/test_data/v5.0/expected_wizard.rbi +45 -0
  81. data/spec/test_data/v5.0/expected_wizard/habtm_subjects.rbi +6 -0
  82. data/spec/test_data/v5.0/expected_wizard_wo_spellbook.rbi +45 -0
  83. data/spec/test_data/v5.1/expected_headmaster.rbi +6 -0
  84. data/spec/test_data/v5.1/expected_potion.rbi +3 -0
  85. data/spec/test_data/v5.1/expected_robe.rbi +3 -0
  86. data/spec/test_data/v5.1/expected_routes.rbi +4 -0
  87. data/spec/test_data/v5.1/expected_school.rbi +3 -0
  88. data/spec/test_data/v5.1/expected_spell/habtm_spell_books.rbi +6 -0
  89. data/spec/test_data/v5.1/expected_spell_book.rbi +3 -0
  90. data/spec/test_data/v5.1/expected_spell_book/habtm_spells.rbi +6 -0
  91. data/spec/test_data/v5.1/expected_squib.rbi +45 -0
  92. data/spec/test_data/v5.1/expected_subject/habtm_wizards.rbi +6 -0
  93. data/spec/test_data/v5.1/expected_wand.rbi +3 -0
  94. data/spec/test_data/v5.1/expected_wizard.rbi +45 -0
  95. data/spec/test_data/v5.1/expected_wizard/habtm_subjects.rbi +6 -0
  96. data/spec/test_data/v5.1/expected_wizard_wo_spellbook.rbi +45 -0
  97. data/spec/test_data/v5.2/expected_attachment.rbi +6 -0
  98. data/spec/test_data/v5.2/expected_blob.rbi +6 -0
  99. data/spec/test_data/v5.2/expected_headmaster.rbi +6 -0
  100. data/spec/test_data/v5.2/expected_potion.rbi +3 -0
  101. data/spec/test_data/v5.2/expected_robe.rbi +3 -0
  102. data/spec/test_data/v5.2/expected_routes.rbi +4 -0
  103. data/spec/test_data/v5.2/expected_school.rbi +3 -0
  104. data/spec/test_data/v5.2/expected_spell/habtm_spell_books.rbi +6 -0
  105. data/spec/test_data/v5.2/expected_spell_book.rbi +3 -0
  106. data/spec/test_data/v5.2/expected_spell_book/habtm_spells.rbi +6 -0
  107. data/spec/test_data/v5.2/expected_squib.rbi +51 -0
  108. data/spec/test_data/v5.2/expected_subject/habtm_wizards.rbi +6 -0
  109. data/spec/test_data/v5.2/expected_wand.rbi +3 -0
  110. data/spec/test_data/v5.2/expected_wizard.rbi +51 -0
  111. data/spec/test_data/v5.2/expected_wizard/habtm_subjects.rbi +6 -0
  112. data/spec/test_data/v5.2/expected_wizard_wo_spellbook.rbi +51 -0
  113. data/spec/test_data/v6.0/expected_attachment.rbi +6 -0
  114. data/spec/test_data/v6.0/expected_blob.rbi +6 -0
  115. data/spec/test_data/v6.0/expected_headmaster.rbi +6 -0
  116. data/spec/test_data/v6.0/expected_potion.rbi +3 -0
  117. data/spec/test_data/v6.0/expected_robe.rbi +3 -0
  118. data/spec/test_data/v6.0/expected_routes.rbi +4 -0
  119. data/spec/test_data/v6.0/expected_school.rbi +3 -0
  120. data/spec/test_data/v6.0/expected_spell/habtm_spell_books.rbi +6 -0
  121. data/spec/test_data/v6.0/expected_spell_book.rbi +3 -0
  122. data/spec/test_data/v6.0/expected_spell_book/habtm_spells.rbi +6 -0
  123. data/spec/test_data/v6.0/expected_squib.rbi +51 -0
  124. data/spec/test_data/v6.0/expected_subject/habtm_wizards.rbi +6 -0
  125. data/spec/test_data/v6.0/expected_wand.rbi +3 -0
  126. data/spec/test_data/v6.0/expected_wizard.rbi +51 -0
  127. data/spec/test_data/v6.0/expected_wizard/habtm_subjects.rbi +6 -0
  128. data/spec/test_data/v6.0/expected_wizard_wo_spellbook.rbi +51 -0
  129. data/spec/tstruct_comparable.rb +13 -0
  130. metadata +12 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca33522fb17d2e6461e49a4d0a4b851a046193c046ec2460d7ff0f1b5363fd8f
4
- data.tar.gz: b59025b18da945ed7c81e2e9dbee17f4994f9aa439b071568aba0161034fd4cb
3
+ metadata.gz: 04e15a0b91862375e9b3f853928e0f46a3c44e6176cb10ecb813a337ac50a45a
4
+ data.tar.gz: e6a5b10b3ac40664e0dce8a51c80795d8fd2e0c7ef4cc665c6ad169d6cd3f0dc
5
5
  SHA512:
6
- metadata.gz: a50fdea8e48fabf8da5c5bfc619de8b6cfc72410f17a4011c723fd6f45d8bbd5f1d65057289afcbdec4cb1691afd00385c90c987be753cf04d7d1d28d565190f
7
- data.tar.gz: ded509eaa92c25474c8e098269ea85436307f86689c0363c880e08d7100699f64e05cefb0a374dccdef32245d112e4d91c2a44859a0034defb47df7bed9a5d5f
6
+ metadata.gz: 3b3bddd29abcb7d6496ece148526fc204c9a7e2bd51f54a264d526e3c10bcacafe6118a49e9ad9c36f6edb9927fe80cd31de28ce4ddda035811f5d15520e2599
7
+ data.tar.gz: '05249019b6251b14ab95a71599c666710478c8468702546c0619ff9111021447d246ea47bc9f62bb038e04616e6b306647b4c669315ff59672b93a1682e6eb39'
@@ -16,7 +16,7 @@ rvm:
16
16
  matrix:
17
17
  include:
18
18
  - rvm: 2.5
19
- env: RAILS_VERSION=5.2 SORBET_VERSION=0.5.5737
19
+ env: RAILS_VERSION=5.2 SORBET_VERSION=0.5.6210
20
20
  exclude:
21
21
  - rvm: 2.3
22
22
  env: RAILS_VERSION=6.0
@@ -54,6 +54,7 @@ module SorbetRails
54
54
  :active_record_named_scope,
55
55
  :active_record_querying,
56
56
  :active_relation_where_not,
57
+ :active_record_serialized_attribute,
57
58
  :active_record_attribute,
58
59
  :active_record_assoc,
59
60
  :custom_finder_methods,
@@ -12,6 +12,9 @@ class ActiveRecord::Base < Object
12
12
  sig { returns(T::Hash[String, T.untyped]) }
13
13
  def self.columns_hash; end
14
14
 
15
+ sig { params(column_name: String).returns(T.nilable(T.any(ActiveModel::Type::Value, ActiveRecord::Type::Serialized))) }
16
+ def self.type_for_attribute(column_name); end
17
+
15
18
  sig { returns(T::Hash[String, T.untyped]) }
16
19
  def self.reflections; end
17
20
 
@@ -67,6 +67,10 @@ class SorbetRails::ModelPlugins::ActiveRecordAssoc < SorbetRails::ModelPlugins::
67
67
  ],
68
68
  return_type: nil,
69
69
  )
70
+ assoc_module_rbi.create_method(
71
+ "reload_#{assoc_name}",
72
+ return_type: assoc_type,
73
+ )
70
74
  end
71
75
 
72
76
  sig { params(reflection: T.untyped).returns(T::Boolean) }
@@ -134,13 +138,21 @@ class SorbetRails::ModelPlugins::ActiveRecordAssoc < SorbetRails::ModelPlugins::
134
138
  return_type: relation_class,
135
139
  )
136
140
  unless assoc_should_be_untyped?(reflection)
141
+ id_type = "T.untyped"
142
+
137
143
  if reflection.klass.table_exists?
138
- # Normally the id_type is an Integer, but it could be a String if using
139
- # UUIDs.
140
- id_type = type_for_column_def(reflection.klass.columns_hash[reflection.klass.primary_key]).to_s
141
- else
142
- id_type = "T.untyped"
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
143
154
  end
155
+
144
156
  assoc_module_rbi.create_method(
145
157
  "#{assoc_name.singularize}_ids",
146
158
  return_type: "T::Array[#{id_type}]",
@@ -24,6 +24,8 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
24
24
  column_name,
25
25
  column_def,
26
26
  )
27
+ elsif serialization_coder_for_column(column_name)
28
+ next # handled by the ActiveRecordSerializedAttribute plugin
27
29
  else
28
30
  column_type = type_for_column_def(column_def)
29
31
  attribute_module_rbi.create_method(
@@ -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
@@ -29,5 +29,13 @@ module SorbetRails::ModelPlugins
29
29
  @model_class = T.let(model_class, T.class_of(ActiveRecord::Base))
30
30
  @available_classes = T.let(available_classes, T::Set[String])
31
31
  end
32
+
33
+ sig { params(column_name: String).returns(T.nilable(Class)) }
34
+ def serialization_coder_for_column(column_name)
35
+ column_type = @model_class.type_for_attribute(column_name)
36
+ return unless column_type.is_a?(ActiveRecord::Type::Serialized)
37
+
38
+ column_type.coder.try(:object_class) || Object
39
+ end
32
40
  end
33
41
  end
@@ -6,6 +6,7 @@ require('sorbet-rails/model_plugins/active_relation_where_not')
6
6
  require('sorbet-rails/model_plugins/active_record_named_scope')
7
7
  require('sorbet-rails/model_plugins/active_record_attribute')
8
8
  require('sorbet-rails/model_plugins/active_record_assoc')
9
+ require('sorbet-rails/model_plugins/active_record_serialized_attribute')
9
10
  require('sorbet-rails/model_plugins/custom_finder_methods')
10
11
  require('sorbet-rails/model_plugins/enumerable_collections')
11
12
  require('sorbet-rails/model_plugins/active_storage_methods')
@@ -47,6 +48,8 @@ module SorbetRails::ModelPlugins
47
48
  ActiveRecordQuerying
48
49
  when :active_relation_where_not
49
50
  ActiveRelationWhereNot
51
+ when :active_record_serialized_attribute
52
+ ActiveRecordSerializedAttribute
50
53
  when :active_record_attribute
51
54
  ActiveRecordAttribute
52
55
  when :active_record_assoc
@@ -42,7 +42,7 @@ class SorbetRails::ModelRbiFormatter
42
42
  end
43
43
 
44
44
  generator = Parlour::RbiGenerator.new(break_params: 3)
45
- run_plugins(plugin_instances, generator, allow_failure: true)
45
+ run_plugins(plugin_instances, generator)
46
46
  # Generate the base after the plugins because when ConflictResolver merge the modules,
47
47
  # it'll put the modules at the last position merged. Putting the base stuff
48
48
  # last will keep the order consistent and minimize changes when new plugins are added.
@@ -115,11 +115,11 @@ class SorbetRails::ModelRbiFormatter
115
115
  params(
116
116
  plugins: T::Array[Parlour::Plugin],
117
117
  generator: Parlour::RbiGenerator,
118
- allow_failure: T::Boolean,
119
118
  ).
120
119
  void
121
120
  }
122
- def run_plugins(plugins, generator, allow_failure: true)
121
+ def run_plugins(plugins, generator)
122
+ allow_failure = ENV["SBR_DEBUG_MODE"] == "true"
123
123
  plugins.each do |plugin|
124
124
  begin
125
125
  generator.current_plugin = plugin
@@ -3,6 +3,9 @@ require 'sorbet-runtime'
3
3
 
4
4
  module SorbetRails::PluckToTStruct
5
5
  extend T::Sig
6
+
7
+ NILCLASS_STRING = "NilClass".freeze
8
+
6
9
  sig {
7
10
  type_parameters(:U).
8
11
  params(
@@ -18,7 +21,8 @@ module SorbetRails::PluckToTStruct
18
21
  raise UnexpectedType.new("pluck_to_tstruct expects a tstruct subclass, given #{tstruct}")
19
22
  end
20
23
 
21
- tstruct_keys = tstruct.props.keys
24
+ tstruct_props = tstruct.props
25
+ tstruct_keys = tstruct_props.keys
22
26
  associations_keys = associations.keys
23
27
  invalid_keys = associations_keys - tstruct_keys
24
28
 
@@ -33,11 +37,44 @@ module SorbetRails::PluckToTStruct
33
37
  keys_one = pluck_keys.size == 1
34
38
  pluck(*pluck_keys).map do |row|
35
39
  row = [row] if keys_one
36
- value = Hash[tstruct_keys.zip(row)]
40
+ value = Hash[map_nil_values_to_default(tstruct_props, tstruct_keys.zip(row))]
37
41
  tstruct.new(value)
38
42
  end
39
43
  end
40
44
 
45
+ sig {params(type_object: T::Types::Base).returns(T::Boolean)}
46
+ private def nilable?(type_object)
47
+ return false unless type_object.is_a?(T::Types::Union)
48
+
49
+ type_object.types.any? { |type| type.name == NILCLASS_STRING }
50
+ end
51
+
52
+ sig {
53
+ params(
54
+ tstruct_props: T::Hash[Symbol, T.untyped],
55
+ zipped_rows: T::Array[[Symbol, T.untyped]]
56
+ )
57
+ .returns(T::Array[[Symbol, T.untyped]])
58
+ }
59
+ private def map_nil_values_to_default(tstruct_props, zipped_rows)
60
+ # we use the default value defined on a prop if
61
+ # 1. the plucked value is nil
62
+ # 2. the prop has a default
63
+ # 3. the prop's type isn't nilable
64
+
65
+ zipped_rows.map do |key, value|
66
+ next [key, value] unless value.nil?
67
+
68
+ default = tstruct_props.dig(key, :default)
69
+ next [key, value] unless default
70
+
71
+ type_object = tstruct_props.dig(key, :type_object)
72
+ next [key, value] if nilable?(type_object)
73
+
74
+ [key, default]
75
+ end
76
+ end
77
+
41
78
  class UnexpectedType < StandardError; end
42
79
  class UnexpectedAssociations < StandardError; end
43
80
  end
@@ -28,6 +28,10 @@ class SorbetRails::RoutesRbiFormatter
28
28
  klass.create_include('GeneratedUrlHelpers')
29
29
  end
30
30
 
31
+ @parlour.root.create_class('ActionController::API') do |klass|
32
+ klass.create_include('GeneratedUrlHelpers')
33
+ end
34
+
31
35
  @parlour.root.create_module('ActionView::Helpers') do |mod|
32
36
  mod.create_include('GeneratedUrlHelpers')
33
37
  end
@@ -97,17 +97,18 @@ namespace :rails_rbi do
97
97
  task helpers: :environment do |t, args|
98
98
  SorbetRails::Utils.rails_eager_load_all!
99
99
 
100
- if ApplicationController.methods.include?(:modules_for_helpers)
100
+ # API controller does not include ActionController::Helpers
101
+ if ApplicationController < ActionController::Helpers
101
102
  helpers = ApplicationController.modules_for_helpers([:all])
102
103
  end
103
104
 
104
105
  # If ApplicationController doesn't work or doesn't return any helpers,
105
106
  # try using ActionController::Base.
106
- if ActionController::Base.methods.include?(:modules_for_helpers) && (helpers.nil? || helpers.length == 0)
107
+ if ApplicationController < ActionController::Helpers && helpers.blank?
107
108
  helpers = ActionController::Base.modules_for_helpers([:all])
108
109
  end
109
110
 
110
- if helpers.nil? || helpers.length == 0
111
+ if helpers.blank?
111
112
  puts 'No helpers found.'
112
113
  else
113
114
  formatter = SorbetRails::HelperRbiFormatter.new(helpers)
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = %q{sorbet-rails}
3
- s.version = "0.7.2"
3
+ s.version = "0.7.3"
4
4
  s.date = %q{2019-04-18}
5
5
  s.summary = %q{Set of tools to make Sorbet work with Rails seamlessly.}
6
6
  s.authors = ["Chan Zuckerberg Initiative"]
@@ -30,5 +30,5 @@ if [[ -z $RAILS_VERSION ]]; then
30
30
  else
31
31
  echo "---- Run $RAILS_VERSION ----"
32
32
  RAILS_VERSION=$RAILS_VERSION bundle update
33
- RAILS_VERSION=$RAILS_VERSION DISABLE_DATABASE_ENVIRONMENT_CHECK=1 bundle exec rake
33
+ RAILS_VERSION=$RAILS_VERSION DISABLE_DATABASE_ENVIRONMENT_CHECK=1 SBR_DEBUG_MODE=true bundle exec rake
34
34
  fi
@@ -180,6 +180,11 @@ def create_models
180
180
  blue: 2,
181
181
  }, _prefix: :color, _suffix: :eyes
182
182
 
183
+ serialize :owl_results, Hash
184
+ serialize :newt_subjects # no specific data type, uses the default YAML Object coder
185
+ serialize :pets, Array
186
+ serialize :patronus_characteristics, JSON
187
+
183
188
  has_one :wand
184
189
  has_many :spell_books
185
190
  # habtm which is optional at the db level
@@ -417,6 +422,17 @@ def create_migrations
417
422
  end
418
423
  RUBY
419
424
 
425
+ file "db/migrate/20190620000015_add_serialized_to_wizards.rb", <<~RUBY
426
+ class AddSerializedToWizards < #{migration_superclass}
427
+ def change
428
+ add_column :wizards, :owl_results, :text # Hash
429
+ add_column :wizards, :newt_subjects, :text # generic
430
+ add_column :wizards, :pets, :text # Array
431
+ add_column :wizards, :patronus_characteristics, :text # serialized as JSON, but not a JSON column type
432
+ end
433
+ end
434
+ RUBY
435
+
420
436
  end
421
437
 
422
438
  def create_mailers
@@ -305,6 +305,21 @@ T.assert_type!(Wizard::House::Hufflepuff, Wizard::House)
305
305
  T.assert_type!(Wizard::House::Ravenclaw, Wizard::House)
306
306
  T.assert_type!(Wizard::House::Slytherin, Wizard::House)
307
307
 
308
+ # Serialization
309
+ T.assert_type!(
310
+ wizard.owl_results,
311
+ T.nilable(T::Hash[T.untyped, T.untyped])
312
+ )
313
+ T.assert_type!(
314
+ wizard.newt_subjects,
315
+ T.nilable(T.any(T::Array[T.untyped], T::Boolean, Float, T::Hash[T.untyped, T.untyped], Integer, String))
316
+ )
317
+ T.assert_type!(
318
+ wizard.patronus_characteristics,
319
+ T.nilable(T.any(T::Array[T.untyped], T::Boolean, Float, T::Hash[String, T.untyped], Integer, String))
320
+ )
321
+ T.assert_type!(wizard.pets, T.nilable(T::Array[T.untyped]))
322
+
308
323
  # Mythical plugin
309
324
  T.assert_type!(Wand.mythicals, T::Array[Wand])
310
325
 
@@ -1,5 +1,6 @@
1
1
  require 'rails_helper'
2
2
  require 'sorbet-rails/model_rbi_formatter'
3
+ require 'tstruct_comparable'
3
4
 
4
5
  RSpec.describe SorbetRails::PluckToTStruct do
5
6
  let!(:harry) do
@@ -41,45 +42,40 @@ RSpec.describe SorbetRails::PluckToTStruct do
41
42
  end
42
43
 
43
44
  class WizardName < T::Struct
44
- const :name, String
45
-
46
- def ==(other)
47
- return false unless other.is_a?(self.class)
48
- name == other.name
49
- end
45
+ include TStructComparable
50
46
 
51
- def eql?(other)
52
- self == other
53
- end
47
+ const :name, String
54
48
  end
55
49
 
56
50
  class WizardT < T::Struct
51
+ include TStructComparable
52
+
57
53
  const :name, String
58
54
  const :house, String
59
-
60
- def ==(other)
61
- return false unless other.is_a?(self.class)
62
- name == other.name && house == other.house
63
- end
64
-
65
- def eql?(other)
66
- self == other
67
- end
68
55
  end
69
56
 
70
57
  class WizardWithWandT < T::Struct
58
+ include TStructComparable
59
+
71
60
  const :name, String
72
61
  const :house, String
73
62
  const :wand_wood_type, String
63
+ end
74
64
 
75
- def ==(other)
76
- return false unless other.is_a?(self.class)
77
- name == other.name && house == other.house
78
- end
65
+ class WizardWithDefaultParentEmailT < T::Struct
66
+ include TStructComparable
79
67
 
80
- def eql?(other)
81
- self == other
82
- end
68
+ const :name, String
69
+ const :house, String
70
+ const :parent_email, String, default: "hagrid@hogwarts.com"
71
+ end
72
+
73
+ class WizardWithDefaultNilableParentEmailT < T::Struct
74
+ include TStructComparable
75
+
76
+ const :name, String
77
+ const :house, String
78
+ const :parent_email, T.nilable(String), default: "hagrid@hogwarts.com"
83
79
  end
84
80
 
85
81
  shared_examples 'pluck_to_tstruct' do |struct_type, expected_values|
@@ -149,4 +145,34 @@ RSpec.describe SorbetRails::PluckToTStruct do
149
145
 
150
146
  it_should_behave_like 'pluck_to_tstruct with associations', WizardWithWandT, associations, expected
151
147
  end
148
+
149
+ context 'uses default value if prop type is not nilable and has default value' do
150
+ it_should_behave_like 'pluck_to_tstruct', WizardWithDefaultParentEmailT, [
151
+ WizardWithDefaultParentEmailT.new(
152
+ name: "Harry Potter",
153
+ house: "Gryffindor",
154
+ parent_email: "hagrid@hogwarts.com"
155
+ ),
156
+ WizardWithDefaultParentEmailT.new(
157
+ name: "Hermione Granger",
158
+ house: "Gryffindor",
159
+ parent_email: "hagrid@hogwarts.com"
160
+ ),
161
+ ]
162
+ end
163
+
164
+ context 'doesnt use default value if prop type is nilable even with a default value' do
165
+ it_should_behave_like 'pluck_to_tstruct', WizardWithDefaultNilableParentEmailT, [
166
+ WizardWithDefaultNilableParentEmailT.new(
167
+ name: "Harry Potter",
168
+ house: "Gryffindor",
169
+ parent_email: nil
170
+ ),
171
+ WizardWithDefaultNilableParentEmailT.new(
172
+ name: "Hermione Granger",
173
+ house: "Gryffindor",
174
+ parent_email: nil
175
+ ),
176
+ ]
177
+ end
152
178
  end