sorbet-rails 0.7.2 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
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