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.
- checksums.yaml +4 -4
- data/.gitattributes +4 -0
- data/.travis.yml +3 -3
- data/README.md +129 -52
- data/lib/bundled_rbi/active_record_base.rbi +83 -0
- data/lib/bundled_rbi/active_record_relation.rbi +43 -0
- data/lib/bundled_rbi/pluck_to_tstruct.rbi +0 -1
- data/lib/bundled_rbi/typed_params.rbi +9 -0
- data/lib/sorbet-rails.rb +1 -0
- data/lib/sorbet-rails/config.rb +0 -1
- data/lib/sorbet-rails/gem_plugins/elastic_search_plugin.rb +1 -1
- data/lib/sorbet-rails/gem_plugins/friendly_id_plugin.rb +1 -1
- data/lib/sorbet-rails/model_plugins/active_record_assoc.rb +3 -3
- data/lib/sorbet-rails/model_plugins/active_record_attribute.rb +85 -17
- data/lib/sorbet-rails/model_plugins/active_record_enum.rb +0 -2
- data/lib/sorbet-rails/model_plugins/active_record_finder_methods.rb +49 -12
- data/lib/sorbet-rails/model_plugins/active_record_querying.rb +1 -1
- data/lib/sorbet-rails/model_plugins/active_storage_methods.rb +1 -1
- data/lib/sorbet-rails/model_plugins/plugins.rb +0 -3
- data/lib/sorbet-rails/rails_mixins/active_record_overrides.rb +156 -5
- data/lib/sorbet-rails/rails_mixins/generated_url_helpers.rb +16 -0
- data/lib/sorbet-rails/railtie.rb +2 -1
- data/lib/sorbet-rails/tasks/rails_rbi.rake +2 -0
- data/lib/sorbet-rails/type_assert/actionpack.rbi +4 -0
- data/lib/sorbet-rails/type_assert/type_assert.rb +1 -1
- data/lib/sorbet-rails/typed_params.rb +22 -0
- data/lib/sorbet-rails/utils.rb +5 -0
- data/sorbet-rails.gemspec +5 -3
- data/spec/generators/rails-template.rb +7 -3
- data/spec/generators/sorbet_test_cases.rb +46 -1
- data/spec/sorbet_spec.rb +3 -1
- data/spec/support/v5.0/Gemfile.lock +36 -23
- data/spec/support/v5.0/app/models/wizard.rb +6 -3
- data/spec/support/v5.0/db/migrate/20190620000001_create_wizards.rb +1 -0
- data/spec/support/v5.0/db/schema.rb +1 -0
- data/spec/support/v5.0/sorbet_test_cases.rb +46 -1
- data/spec/support/v5.1/Gemfile.lock +37 -24
- data/spec/support/v5.1/app/models/wizard.rb +6 -3
- data/spec/support/v5.1/db/migrate/20190620000001_create_wizards.rb +1 -0
- data/spec/support/v5.1/db/schema.rb +1 -0
- data/spec/support/v5.1/sorbet_test_cases.rb +46 -1
- data/spec/support/v5.2/Gemfile.lock +75 -62
- data/spec/support/v5.2/app/models/wizard.rb +6 -3
- data/spec/support/v5.2/db/migrate/20190620000001_create_wizards.rb +1 -0
- data/spec/support/v5.2/db/schema.rb +1 -0
- data/spec/support/v5.2/sorbet_test_cases.rb +46 -1
- data/spec/support/v6.0/Gemfile.lock +89 -76
- data/spec/support/v6.0/app/models/wizard.rb +6 -3
- data/spec/support/v6.0/db/migrate/20190620000001_create_wizards.rb +1 -0
- data/spec/support/v6.0/db/schema.rb +1 -0
- data/spec/support/v6.0/sorbet_test_cases.rb +46 -1
- data/spec/test_data/v5.0/expected_internal_metadata.rbi +33 -54
- data/spec/test_data/v5.0/expected_potion.rbi +33 -54
- data/spec/test_data/v5.0/expected_robe.rbi +33 -54
- data/spec/test_data/v5.0/expected_schema_migration.rbi +33 -54
- data/spec/test_data/v5.0/expected_school.rbi +33 -54
- data/spec/test_data/v5.0/expected_spell_book.rbi +46 -52
- data/spec/test_data/v5.0/expected_squib.rbi +42 -54
- data/spec/test_data/v5.0/expected_wand.rbi +47 -52
- data/spec/test_data/v5.0/expected_wizard.rbi +119 -56
- data/spec/test_data/v5.0/expected_wizard_wo_spellbook.rbi +119 -56
- data/spec/test_data/v5.1/expected_internal_metadata.rbi +33 -54
- data/spec/test_data/v5.1/expected_potion.rbi +33 -54
- data/spec/test_data/v5.1/expected_robe.rbi +33 -54
- data/spec/test_data/v5.1/expected_schema_migration.rbi +33 -54
- data/spec/test_data/v5.1/expected_school.rbi +33 -54
- data/spec/test_data/v5.1/expected_spell_book.rbi +46 -52
- data/spec/test_data/v5.1/expected_squib.rbi +42 -54
- data/spec/test_data/v5.1/expected_wand.rbi +47 -52
- data/spec/test_data/v5.1/expected_wizard.rbi +119 -56
- data/spec/test_data/v5.1/expected_wizard_wo_spellbook.rbi +119 -56
- data/spec/test_data/v5.2/expected_attachment.rbi +33 -54
- data/spec/test_data/v5.2/expected_blob.rbi +33 -54
- data/spec/test_data/v5.2/expected_internal_metadata.rbi +33 -54
- data/spec/test_data/v5.2/expected_potion.rbi +33 -54
- data/spec/test_data/v5.2/expected_robe.rbi +33 -54
- data/spec/test_data/v5.2/expected_schema_migration.rbi +33 -54
- data/spec/test_data/v5.2/expected_school.rbi +33 -54
- data/spec/test_data/v5.2/expected_spell_book.rbi +46 -52
- data/spec/test_data/v5.2/expected_squib.rbi +42 -54
- data/spec/test_data/v5.2/expected_wand.rbi +47 -52
- data/spec/test_data/v5.2/expected_wizard.rbi +119 -56
- data/spec/test_data/v5.2/expected_wizard_wo_spellbook.rbi +119 -56
- data/spec/test_data/v6.0/expected_attachment.rbi +33 -54
- data/spec/test_data/v6.0/expected_blob.rbi +33 -54
- data/spec/test_data/v6.0/expected_internal_metadata.rbi +33 -54
- data/spec/test_data/v6.0/expected_potion.rbi +33 -54
- data/spec/test_data/v6.0/expected_robe.rbi +33 -54
- data/spec/test_data/v6.0/expected_schema_migration.rbi +33 -54
- data/spec/test_data/v6.0/expected_school.rbi +33 -54
- data/spec/test_data/v6.0/expected_spell_book.rbi +46 -52
- data/spec/test_data/v6.0/expected_squib.rbi +42 -54
- data/spec/test_data/v6.0/expected_wand.rbi +47 -52
- data/spec/test_data/v6.0/expected_wizard.rbi +119 -56
- data/spec/test_data/v6.0/expected_wizard_wo_spellbook.rbi +119 -56
- data/spec/typed_enum_spec.rb +55 -0
- data/spec/typed_params_spec.rb +91 -0
- metadata +41 -4
- data/lib/sorbet-rails/model_plugins/active_record_factory_methods.rb +0 -28
data/lib/sorbet-rails.rb
CHANGED
data/lib/sorbet-rails/config.rb
CHANGED
@@ -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
|
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 "
|
77
|
-
DB level but **
|
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
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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
|
-
|
59
|
-
parameters: [ Parameter.new("
|
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
|
|
@@ -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
|
21
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|