sorbet-rails 0.5.9.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +4 -0
  3. data/.travis.yml +3 -3
  4. data/README.md +129 -52
  5. data/lib/bundled_rbi/active_record_base.rbi +83 -0
  6. data/lib/bundled_rbi/active_record_relation.rbi +43 -0
  7. data/lib/bundled_rbi/pluck_to_tstruct.rbi +0 -1
  8. data/lib/bundled_rbi/typed_params.rbi +9 -0
  9. data/lib/sorbet-rails.rb +1 -0
  10. data/lib/sorbet-rails/config.rb +0 -1
  11. data/lib/sorbet-rails/gem_plugins/elastic_search_plugin.rb +1 -1
  12. data/lib/sorbet-rails/gem_plugins/friendly_id_plugin.rb +1 -1
  13. data/lib/sorbet-rails/model_plugins/active_record_assoc.rb +3 -3
  14. data/lib/sorbet-rails/model_plugins/active_record_attribute.rb +85 -17
  15. data/lib/sorbet-rails/model_plugins/active_record_enum.rb +0 -2
  16. data/lib/sorbet-rails/model_plugins/active_record_finder_methods.rb +49 -12
  17. data/lib/sorbet-rails/model_plugins/active_record_querying.rb +1 -1
  18. data/lib/sorbet-rails/model_plugins/active_storage_methods.rb +1 -1
  19. data/lib/sorbet-rails/model_plugins/plugins.rb +0 -3
  20. data/lib/sorbet-rails/rails_mixins/active_record_overrides.rb +156 -5
  21. data/lib/sorbet-rails/rails_mixins/generated_url_helpers.rb +16 -0
  22. data/lib/sorbet-rails/railtie.rb +2 -1
  23. data/lib/sorbet-rails/tasks/rails_rbi.rake +2 -0
  24. data/lib/sorbet-rails/type_assert/actionpack.rbi +4 -0
  25. data/lib/sorbet-rails/type_assert/type_assert.rb +1 -1
  26. data/lib/sorbet-rails/typed_params.rb +22 -0
  27. data/lib/sorbet-rails/utils.rb +5 -0
  28. data/sorbet-rails.gemspec +5 -3
  29. data/spec/generators/rails-template.rb +7 -3
  30. data/spec/generators/sorbet_test_cases.rb +46 -1
  31. data/spec/sorbet_spec.rb +3 -1
  32. data/spec/support/v5.0/Gemfile.lock +36 -23
  33. data/spec/support/v5.0/app/models/wizard.rb +6 -3
  34. data/spec/support/v5.0/db/migrate/20190620000001_create_wizards.rb +1 -0
  35. data/spec/support/v5.0/db/schema.rb +1 -0
  36. data/spec/support/v5.0/sorbet_test_cases.rb +46 -1
  37. data/spec/support/v5.1/Gemfile.lock +37 -24
  38. data/spec/support/v5.1/app/models/wizard.rb +6 -3
  39. data/spec/support/v5.1/db/migrate/20190620000001_create_wizards.rb +1 -0
  40. data/spec/support/v5.1/db/schema.rb +1 -0
  41. data/spec/support/v5.1/sorbet_test_cases.rb +46 -1
  42. data/spec/support/v5.2/Gemfile.lock +75 -62
  43. data/spec/support/v5.2/app/models/wizard.rb +6 -3
  44. data/spec/support/v5.2/db/migrate/20190620000001_create_wizards.rb +1 -0
  45. data/spec/support/v5.2/db/schema.rb +1 -0
  46. data/spec/support/v5.2/sorbet_test_cases.rb +46 -1
  47. data/spec/support/v6.0/Gemfile.lock +89 -76
  48. data/spec/support/v6.0/app/models/wizard.rb +6 -3
  49. data/spec/support/v6.0/db/migrate/20190620000001_create_wizards.rb +1 -0
  50. data/spec/support/v6.0/db/schema.rb +1 -0
  51. data/spec/support/v6.0/sorbet_test_cases.rb +46 -1
  52. data/spec/test_data/v5.0/expected_internal_metadata.rbi +33 -54
  53. data/spec/test_data/v5.0/expected_potion.rbi +33 -54
  54. data/spec/test_data/v5.0/expected_robe.rbi +33 -54
  55. data/spec/test_data/v5.0/expected_schema_migration.rbi +33 -54
  56. data/spec/test_data/v5.0/expected_school.rbi +33 -54
  57. data/spec/test_data/v5.0/expected_spell_book.rbi +46 -52
  58. data/spec/test_data/v5.0/expected_squib.rbi +42 -54
  59. data/spec/test_data/v5.0/expected_wand.rbi +47 -52
  60. data/spec/test_data/v5.0/expected_wizard.rbi +119 -56
  61. data/spec/test_data/v5.0/expected_wizard_wo_spellbook.rbi +119 -56
  62. data/spec/test_data/v5.1/expected_internal_metadata.rbi +33 -54
  63. data/spec/test_data/v5.1/expected_potion.rbi +33 -54
  64. data/spec/test_data/v5.1/expected_robe.rbi +33 -54
  65. data/spec/test_data/v5.1/expected_schema_migration.rbi +33 -54
  66. data/spec/test_data/v5.1/expected_school.rbi +33 -54
  67. data/spec/test_data/v5.1/expected_spell_book.rbi +46 -52
  68. data/spec/test_data/v5.1/expected_squib.rbi +42 -54
  69. data/spec/test_data/v5.1/expected_wand.rbi +47 -52
  70. data/spec/test_data/v5.1/expected_wizard.rbi +119 -56
  71. data/spec/test_data/v5.1/expected_wizard_wo_spellbook.rbi +119 -56
  72. data/spec/test_data/v5.2/expected_attachment.rbi +33 -54
  73. data/spec/test_data/v5.2/expected_blob.rbi +33 -54
  74. data/spec/test_data/v5.2/expected_internal_metadata.rbi +33 -54
  75. data/spec/test_data/v5.2/expected_potion.rbi +33 -54
  76. data/spec/test_data/v5.2/expected_robe.rbi +33 -54
  77. data/spec/test_data/v5.2/expected_schema_migration.rbi +33 -54
  78. data/spec/test_data/v5.2/expected_school.rbi +33 -54
  79. data/spec/test_data/v5.2/expected_spell_book.rbi +46 -52
  80. data/spec/test_data/v5.2/expected_squib.rbi +42 -54
  81. data/spec/test_data/v5.2/expected_wand.rbi +47 -52
  82. data/spec/test_data/v5.2/expected_wizard.rbi +119 -56
  83. data/spec/test_data/v5.2/expected_wizard_wo_spellbook.rbi +119 -56
  84. data/spec/test_data/v6.0/expected_attachment.rbi +33 -54
  85. data/spec/test_data/v6.0/expected_blob.rbi +33 -54
  86. data/spec/test_data/v6.0/expected_internal_metadata.rbi +33 -54
  87. data/spec/test_data/v6.0/expected_potion.rbi +33 -54
  88. data/spec/test_data/v6.0/expected_robe.rbi +33 -54
  89. data/spec/test_data/v6.0/expected_schema_migration.rbi +33 -54
  90. data/spec/test_data/v6.0/expected_school.rbi +33 -54
  91. data/spec/test_data/v6.0/expected_spell_book.rbi +46 -52
  92. data/spec/test_data/v6.0/expected_squib.rbi +42 -54
  93. data/spec/test_data/v6.0/expected_wand.rbi +47 -52
  94. data/spec/test_data/v6.0/expected_wizard.rbi +119 -56
  95. data/spec/test_data/v6.0/expected_wizard_wo_spellbook.rbi +119 -56
  96. data/spec/typed_enum_spec.rb +55 -0
  97. data/spec/typed_params_spec.rb +91 -0
  98. metadata +41 -4
  99. data/lib/sorbet-rails/model_plugins/active_record_factory_methods.rb +0 -28
@@ -6,5 +6,6 @@ module SorbetRails
6
6
  require 'sorbet-rails/type_assert/type_assert'
7
7
  require 'sorbet-rails/custom_types/integer_string'
8
8
  require 'sorbet-rails/custom_types/boolean_string'
9
+ require 'sorbet-rails/typed_params'
9
10
  end
10
11
  end
@@ -48,7 +48,6 @@ module SorbetRails
48
48
  :active_record_attribute,
49
49
  :active_record_assoc,
50
50
  :active_record_finder_methods,
51
- :active_record_factory_methods,
52
51
  :custom_finder_methods,
53
52
  :enumerable_collections,
54
53
  ]
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  class ElasticSearchPlugin < SorbetRails::ModelPlugins::Base
3
3
  sig { override.params(root: Parlour::RbiGenerator::Namespace).void }
4
4
  def generate(root)
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  class FriendlyIdPlugin < SorbetRails::ModelPlugins::Base
3
3
  sig { override.params(root: Parlour::RbiGenerator::Namespace).void }
4
4
  def generate(root)
@@ -70,11 +70,11 @@ class SorbetRails::ModelPlugins::ActiveRecordAssoc < SorbetRails::ModelPlugins::
70
70
 
71
71
  if rails_required_config && !db_required_config
72
72
  puts "Warning: belongs_to association #{reflection.name} is required at the application
73
- level but **nullable** at the DB level. Add a constraint at the DB level
73
+ level but **nullable** at the DB level.\n Add a constraint at the DB level
74
74
  (using `null: false` and foreign key constraint) to ensure it is enforced.".squish!
75
75
  elsif !rails_required_config && db_required_config
76
- puts "Warning: belongs_to association #{reflection.name} is specified as not-null at the
77
- DB level but **not required** at the application level. Add a constraint at the app level
76
+ puts "Note: belongs_to association #{reflection.name} is specified as not-null at the
77
+ DB level but **optional** at the application level.\n Add a constraint at the app level
78
78
  (using `optional: false`) as a validation hint to Rails.".squish!
79
79
  end
80
80
 
@@ -13,25 +13,17 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
13
13
 
14
14
  model_class_rbi = root.create_class(self.model_class_name)
15
15
  model_class_rbi.create_include(attribute_module_name)
16
+ model_defined_enums = @model_class.defined_enums
16
17
 
17
18
  columns_hash.sort.each do |column_name, column_def|
18
- if @model_class.defined_enums.has_key?(column_name)
19
- # enum attribute is treated differently
20
- assignable_type = "T.any(Integer, String, Symbol)"
21
- assignable_type = "T.nilable(#{assignable_type})" if column_def.null
22
- return_type = "String"
23
- return_type = "T.nilable(#{return_type})" if column_def.null
24
-
25
- attribute_module_rbi.create_method(
26
- column_name.to_s,
27
- return_type: return_type,
28
- )
29
- attribute_module_rbi.create_method(
30
- "#{column_name}=",
31
- parameters: [
32
- Parameter.new("value", type: assignable_type)
33
- ],
34
- return_type: nil,
19
+ if model_defined_enums.has_key?(column_name)
20
+ generate_enum_methods(
21
+ root,
22
+ model_class_rbi,
23
+ attribute_module_rbi,
24
+ model_defined_enums,
25
+ column_name,
26
+ column_def,
35
27
  )
36
28
  else
37
29
  column_type = type_for_column_def(column_def)
@@ -55,6 +47,82 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
55
47
  end
56
48
  end
57
49
 
50
+ sig {
51
+ params(
52
+ root: Parlour::RbiGenerator::Namespace,
53
+ model_class_rbi: Parlour::RbiGenerator::Namespace,
54
+ attribute_module_rbi: Parlour::RbiGenerator::Namespace,
55
+ model_defined_enums: T::Hash[String, T::Hash[String, T.untyped]],
56
+ column_name: String,
57
+ column_def: T.untyped,
58
+ ).void
59
+ }
60
+ def generate_enum_methods(
61
+ root,
62
+ model_class_rbi,
63
+ attribute_module_rbi,
64
+ model_defined_enums,
65
+ column_name,
66
+ column_def
67
+ )
68
+ should_skip_setter_getter = false
69
+
70
+ if @model_class.is_a?(::ActiveRecord::Enum)
71
+ config = @model_class.typed_enum_reflections[column_name]
72
+ if config.present?
73
+ # do not generate the methods for enums in strict_mode
74
+ should_skip_setter_getter = config.strict_mode
75
+
76
+ t_enum_type = "#{@model_class.name}::#{config.class_name}"
77
+
78
+ # define T::Enum class & values
79
+ enum_values = T.must(model_defined_enums[column_name])
80
+ t_enum_values = @model_class.gen_typed_enum_values(enum_values.keys)
81
+ root.create_enum_class(
82
+ t_enum_type,
83
+ enums: t_enum_values.map { |k, v| [v, "'#{k}'"] },
84
+ )
85
+
86
+ # define t_enum setter/getter
87
+ assignable_type = t_enum_type
88
+ assignable_type = "T.nilable(#{assignable_type})" if column_def.null
89
+ # add directly to model_class_rbi because they are included
90
+ # by sorbet's hidden.rbi
91
+ model_class_rbi.create_method(
92
+ "typed_#{column_name}",
93
+ return_type: assignable_type,
94
+ )
95
+ model_class_rbi.create_method(
96
+ "typed_#{column_name}=",
97
+ parameters: [
98
+ Parameter.new("value", type: assignable_type)
99
+ ],
100
+ return_type: nil,
101
+ )
102
+ end
103
+ end
104
+
105
+ if !should_skip_setter_getter
106
+ # enum attribute is treated differently
107
+ assignable_type = "T.any(Integer, String, Symbol)"
108
+ assignable_type = "T.nilable(#{assignable_type})" if column_def.null
109
+ return_type = "String"
110
+ return_type = "T.nilable(#{return_type})" if column_def.null
111
+
112
+ attribute_module_rbi.create_method(
113
+ column_name.to_s,
114
+ return_type: return_type,
115
+ )
116
+ attribute_module_rbi.create_method(
117
+ "#{column_name}=",
118
+ parameters: [
119
+ Parameter.new("value", type: assignable_type)
120
+ ],
121
+ return_type: nil,
122
+ )
123
+ end
124
+ end
125
+
58
126
  sig { params(column_def: T.untyped).returns(T.any(String, Class)) }
59
127
  def type_for_column_def(column_def)
60
128
  cast_type = ActiveRecord::Base.connection.respond_to?(:lookup_cast_type_from_column) ?
@@ -14,8 +14,6 @@ class SorbetRails::ModelPlugins::ActiveRecordEnum < SorbetRails::ModelPlugins::B
14
14
  model_class_rbi = root.create_class(self.model_class_name)
15
15
  model_class_rbi.create_include(enum_module_name)
16
16
 
17
- enum_calls = ActiveRecordOverrides.instance.enum_calls[self.model_class_name]
18
-
19
17
  # TODO: add any method for signature verification?
20
18
  model_class.defined_enums.sort.each do |enum_name, enum_hash|
21
19
  value_type = enum_hash.values.map { |v| v.is_a?(Integer) ? 'Integer' : v.class.name }.uniq
@@ -5,7 +5,7 @@ class SorbetRails::ModelPlugins::ActiveRecordFinderMethods < SorbetRails::ModelP
5
5
  sig { override.params(root: Parlour::RbiGenerator::Namespace).void }
6
6
  def generate(root)
7
7
  model_class_rbi = root.create_class(self.model_class_name)
8
- create_finder_methods_for(model_class_rbi, class_method: true)
8
+ create_finder_methods_for(model_class_rbi, class_method: true, include_methods_that_return_self: false)
9
9
 
10
10
  model_relation_class_rbi = root.create_class(self.model_relation_class_name)
11
11
  create_finder_methods_for(model_relation_class_rbi, class_method: false, include_methods_that_return_self: false)
@@ -47,27 +47,64 @@ class SorbetRails::ModelPlugins::ActiveRecordFinderMethods < SorbetRails::ModelP
47
47
  return_type: self.model_class_name,
48
48
  class_method: class_method
49
49
  )
50
+ class_rbi.create_method(
51
+ "find_or_initialize_by",
52
+ parameters: [
53
+ Parameter.new("attributes", type: "T.untyped") ,
54
+ Parameter.new(
55
+ "&block",
56
+ type: "T.nilable(T.proc.params(object: #{self.model_class_name}).void)",
57
+ ),
58
+ ],
59
+ return_type: self.model_class_name,
60
+ class_method: class_method
61
+ )
62
+ class_rbi.create_method(
63
+ "find_or_create_by",
64
+ parameters: [
65
+ Parameter.new("attributes", type: "T.untyped") ,
66
+ Parameter.new(
67
+ "&block",
68
+ type: "T.nilable(T.proc.params(object: #{self.model_class_name}).void)"
69
+ ),
70
+ ],
71
+ return_type: self.model_class_name,
72
+ class_method: class_method
73
+ )
74
+ class_rbi.create_method(
75
+ "find_or_create_by!",
76
+ parameters: [
77
+ Parameter.new("attributes", type: "T.untyped") ,
78
+ Parameter.new(
79
+ "&block",
80
+ type: "T.nilable(T.proc.params(object: #{self.model_class_name}).void)",
81
+ ),
82
+ ],
83
+ return_type: self.model_class_name,
84
+ class_method: class_method
85
+ )
50
86
 
51
87
  ["first", "second", "third", "third_to_last", "second_to_last", "last"].
52
88
  each do |method_name|
53
89
  create_finder_method_pair(class_rbi, method_name, class_method)
54
90
  end
91
+ end
55
92
 
56
- # Checker methods
93
+ # Checker methods
94
+ class_rbi.create_method(
95
+ "exists?",
96
+ parameters: [ Parameter.new("conditions", type: "T.untyped", default: "nil") ],
97
+ return_type: "T::Boolean",
98
+ class_method: class_method,
99
+ )
100
+
101
+ ["any?", "many?", "none?", "one?"].each do |method_name|
57
102
  class_rbi.create_method(
58
- "exists?",
59
- parameters: [ Parameter.new("conditions", type: "T.untyped", default: "nil") ],
103
+ method_name,
104
+ parameters: [ Parameter.new("*args", type: "T.untyped") ],
60
105
  return_type: "T::Boolean",
61
106
  class_method: class_method,
62
107
  )
63
- ["any?", "many?", "none?", "one?"].each do |method_name|
64
- class_rbi.create_method(
65
- method_name,
66
- parameters: [ Parameter.new("*args", type: "T.untyped") ],
67
- return_type: "T::Boolean",
68
- class_method: class_method,
69
- )
70
- end
71
108
  end
72
109
  end
73
110
 
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  require ('sorbet-rails/model_plugins/base')
3
3
  class SorbetRails::ModelPlugins::ActiveRecordQuerying < SorbetRails::ModelPlugins::Base
4
4
 
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  require ('sorbet-rails/model_plugins/base')
3
3
  class SorbetRails::ModelPlugins::ActiveStorageMethods < SorbetRails::ModelPlugins::Base
4
4
  sig { params(model_class: T.class_of(ActiveRecord::Base), available_classes: T::Set[String]).void }
@@ -7,7 +7,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
9
  require('sorbet-rails/model_plugins/active_record_finder_methods')
10
- require('sorbet-rails/model_plugins/active_record_factory_methods')
11
10
  require('sorbet-rails/model_plugins/custom_finder_methods')
12
11
  require('sorbet-rails/model_plugins/enumerable_collections')
13
12
  require('sorbet-rails/model_plugins/active_storage_methods')
@@ -55,8 +54,6 @@ module SorbetRails::ModelPlugins
55
54
  ActiveRecordAssoc
56
55
  when :active_record_finder_methods
57
56
  ActiveRecordFinderMethods
58
- when :active_record_factory_methods
59
- ActiveRecordFactoryMethods
60
57
  when :custom_finder_methods
61
58
  CustomFinderMethods
62
59
  when :enumerable_collections
@@ -17,8 +17,10 @@ class ActiveRecordOverrides
17
17
  # modeling the logic in
18
18
  # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/enum.rb#L152
19
19
  kwargs.each do |name, values|
20
- next if [:_prefix, :_suffix, :_scopes].include?(name)
21
- @enum_calls[class_name][name] = kwargs
20
+ next if ::ActiveRecord::Enum::SR_ENUM_KEYWORDS.include?(name)
21
+
22
+ # calling dup is required, because Rails internally mutates `kwargs` (the args you passed to `enum`)
23
+ @enum_calls[class_name][name] = kwargs.dup
22
24
  end
23
25
  end
24
26
 
@@ -31,11 +33,160 @@ class ActiveRecordOverrides
31
33
  end
32
34
  end
33
35
 
36
+ class SorbetRails::TypedEnumConfig < T::Struct
37
+ # set the method to be true or false
38
+ const :strict_mode, T::Boolean
39
+ const :class_name, String
40
+ end
41
+
34
42
  module ::ActiveRecord::Enum
43
+ extend T::Sig
44
+ include Kernel
45
+
35
46
  alias old_enum enum
36
47
 
37
- def enum(*args, **kwargs)
38
- ActiveRecordOverrides.instance.store_enum_call(self, kwargs)
39
- old_enum(*args, **kwargs)
48
+ SR_ENUM_KEYWORDS = [
49
+ # keywords from https://github.com/rails/rails/blob/master/activerecord/lib/active_record/enum.rb
50
+ :_prefix,
51
+ :_suffix,
52
+ :_scopes
53
+ ]
54
+
55
+ sig { params(definitions: T::Hash[Symbol, T.untyped]).void }
56
+ def _define_enum(definitions)
57
+ ActiveRecordOverrides.instance.store_enum_call(self, definitions)
58
+ old_enum(definitions)
59
+ end
60
+
61
+ sig { params(definitions: T::Hash[Symbol, T.untyped]).void }
62
+ def enum(definitions)
63
+ _define_enum(definitions)
64
+ definitions.each do |enum_name, values|
65
+ begin
66
+ # skip irrelevant keywords
67
+ next if SR_ENUM_KEYWORDS.include?(enum_name)
68
+ _define_typed_enum(enum_name, extract_enum_values(values))
69
+ rescue ArgumentError, ConflictTypedEnumNameError, TypeError => ex
70
+ # known errors
71
+ # do nothing if we cannot define t_enum
72
+ puts "warning: #{ex.message}"
73
+ rescue => ex
74
+ # rescue any other kind of error to unblock the application
75
+ # can be disabled in development
76
+ puts "warning: #{ex.message}"
77
+ # raise ex
78
+ end
79
+ end
80
+ end
81
+
82
+ sig { params(definitions: T::Hash[Symbol, T.untyped]).void }
83
+ def typed_enum(definitions)
84
+ enum_names = definitions.keys - SR_ENUM_KEYWORDS
85
+
86
+ if enum_names.size != 1
87
+ raise MultipleEnumsDefinedError.new(
88
+ "typed_enum only supports 1 enum defined at a time,
89
+ given #{enum_names.count}: #{enum_names.join(', ')}".squish!
90
+ )
91
+ end
92
+
93
+ enum_name = enum_names[0]
94
+
95
+ _define_enum(definitions)
96
+ _define_typed_enum(
97
+ T.must(enum_name),
98
+ extract_enum_values(definitions[enum_name]),
99
+ strict_mode: true,
100
+ )
101
+ end
102
+
103
+ # this config is for sorbet-rails to inflect on the settings
104
+ sig { returns(T::Hash[String, SorbetRails::TypedEnumConfig]) }
105
+ def typed_enum_reflections
106
+ @typed_enum_reflections ||= {}
40
107
  end
108
+
109
+ sig {
110
+ params(
111
+ enum_name: Symbol,
112
+ enum_values: T::Array[Symbol],
113
+ strict_mode: T::Boolean,
114
+ ).
115
+ void
116
+ }
117
+ def _define_typed_enum(
118
+ enum_name,
119
+ enum_values,
120
+ strict_mode: false
121
+ )
122
+ enum_klass_name = enum_name.to_s.camelize
123
+
124
+ # we don't need to use the actual enum value
125
+ typed_enum_values = gen_typed_enum_values(enum_values.map(&:to_s))
126
+
127
+ # create dynamic T::Enum definition
128
+ if const_defined?(enum_klass_name)
129
+ # append Enum to avoid conflict
130
+ enum_klass_name = "#{enum_klass_name}Enum"
131
+ if const_defined?(enum_klass_name)
132
+ raise ConflictTypedEnumNameError.new(
133
+ "Unable to define enum class #{enum_klass_name} because
134
+ it's already defined".squish!
135
+ )
136
+ end
137
+ end
138
+ enum_klass = Class.new(T::Enum) do
139
+ enums do
140
+ typed_enum_values.each do |enum_key_name, typed_enum_value|
141
+ const_set(typed_enum_value, new(enum_key_name))
142
+ end
143
+ end
144
+ end
145
+ const_set(enum_klass_name, enum_klass)
146
+
147
+ # create t_enum getter to get T::Enum value
148
+ # assuming there shouldn't be any conflict
149
+ typed_enum_getter_name = "typed_#{enum_name}"
150
+ detect_enum_conflict!(enum_name, typed_enum_getter_name)
151
+ define_method(typed_enum_getter_name) do
152
+ T.unsafe(enum_klass).try_deserialize(send(enum_name))
153
+ end
154
+
155
+ # override the setter to accept T::Enum values
156
+ enum_setter_name = "#{enum_name}="
157
+ typed_enum_setter_name = "typed_#{enum_name}="
158
+ detect_enum_conflict!(enum_name, typed_enum_setter_name)
159
+ define_method(typed_enum_setter_name) do |value|
160
+ send(enum_setter_name, value&.serialize)
161
+ end
162
+
163
+ # add to the config for RBI generation only if it works
164
+ typed_enum_reflections[enum_name.to_s] = SorbetRails::TypedEnumConfig.new(
165
+ strict_mode: strict_mode || false,
166
+ class_name: enum_klass_name,
167
+ )
168
+ end
169
+
170
+ sig { params(enum_values: T::Array[String]).returns(T::Hash[String, String]) }
171
+ def gen_typed_enum_values(enum_values)
172
+ Hash[enum_values.map do |val|
173
+ [val, val.to_s.gsub(/[^0-9a-z_]/i, '').camelize]
174
+ end]
175
+ end
176
+
177
+ sig {
178
+ params(
179
+ enum_def: T.any(
180
+ T::Array[T.untyped],
181
+ T::Hash[T.untyped, T.untyped],
182
+ ActiveSupport::HashWithIndifferentAccess
183
+ ),
184
+ ).returns(T::Array[Symbol])
185
+ }
186
+ def extract_enum_values(enum_def)
187
+ enum_def.is_a?(Array) ? enum_def.map(&:to_sym) : enum_def.keys.map(&:to_sym)
188
+ end
189
+
190
+ class MultipleEnumsDefinedError < StandardError; end
191
+ class ConflictTypedEnumNameError < StandardError; end
41
192
  end