Skip to content

Commit cd285a7

Browse files
committed
Add Enum DSL Compiler
1 parent 0422cac commit cd285a7

File tree

4 files changed

+434
-0
lines changed

4 files changed

+434
-0
lines changed

lib/boba.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
module Boba
55
require "boba/version"
66
require "boba/relations_railtie" if defined?(Rails)
7+
require "boba/enums_railtie" if defined?(Rails)
78
end

lib/boba/enums_railtie.rb

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
require "rails/railtie"
5+
6+
module Boba
7+
class EnumsRailtie < Rails::Railtie
8+
railtie_name(:boba)
9+
10+
initializer("boba.add_enum_classes") do
11+
ActiveSupport.on_load(:active_record) do
12+
module ActiveRecordTypedEnum
13+
extend T::Sig
14+
extend T::Helpers
15+
16+
module ClassMethods
17+
extend T::Sig
18+
19+
sig { params(definitions: T::Hash[T.untyped, T.untyped]).returns(T.untyped) }
20+
def enum(definitions)
21+
# Call the original Rails enum method first
22+
result = super
23+
24+
# Add typed enum methods for each enum
25+
definitions.each_key do |name|
26+
enum_class_name = name.to_s.camelize
27+
28+
# Create a simple T::Enum subclass first
29+
enum_class = Class.new(T::Enum) { extend T::Sig }
30+
31+
T.unsafe(self).const_set(enum_class_name, enum_class)
32+
33+
# Now that the enum is defined in Rails, we can access the values
34+
enum_values = T.unsafe(self).defined_enums[name.to_s]
35+
36+
if enum_values
37+
# Create a local variable to capture the values for the block
38+
values_to_define = enum_values.keys
39+
40+
# Populate the enum values inside the enums block
41+
enum_class.class_eval do
42+
enums { values_to_define.each { |key| const_set(key.to_s.camelize, new(key.to_s)) } }
43+
end
44+
end
45+
46+
# Define typed getter method
47+
T
48+
.unsafe(self)
49+
.define_method("typed_#{name}") do
50+
value = T.unsafe(self).send(name)
51+
return if value.nil?
52+
53+
# Use the model class directly instead of const_get
54+
enum_klass = T.unsafe(self).class.const_get(enum_class_name)
55+
T.unsafe(enum_klass).try_deserialize(value)
56+
end
57+
58+
# Define typed setter method
59+
T
60+
.unsafe(self)
61+
.define_method("typed_#{name}=") do |typed_value|
62+
if typed_value.nil?
63+
T.unsafe(self).send("#{name}=", nil)
64+
else
65+
T.unsafe(self).send("#{name}=", T.unsafe(typed_value).serialize)
66+
end
67+
end
68+
end
69+
70+
result
71+
end
72+
end
73+
74+
mixes_in_class_methods(ClassMethods)
75+
end
76+
77+
module ::ActiveRecord
78+
class Base
79+
include ActiveRecordTypedEnum
80+
end
81+
end
82+
end
83+
end
84+
end
85+
end
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
module Tapioca
5+
module Dsl
6+
module Compilers
7+
class ActiveRecordTypedEnum < Tapioca::Dsl::Compiler
8+
extend T::Sig
9+
10+
ConstantType = type_member { { fixed: T.class_of(ActiveRecord::Base) } }
11+
12+
class << self
13+
extend T::Sig
14+
15+
sig { override.returns(T::Enumerable[Module]) }
16+
def gather_constants
17+
ActiveRecord::Base.descendants.select do |klass|
18+
klass.respond_to?(:defined_enums) && klass.defined_enums.any? && klass.table_exists?
19+
end
20+
end
21+
end
22+
23+
sig { override.void }
24+
def decorate
25+
return unless constant.respond_to?(:defined_enums)
26+
27+
constant.defined_enums.each do |enum_name, enum_values|
28+
# Create the enum class
29+
enum_class_name = enum_name.camelize
30+
31+
root.create_path(constant) do |model|
32+
# Create the T::Enum using RBI::TEnum
33+
enum_class =
34+
RBI::TEnum.new(enum_class_name) do |tenum|
35+
# Try to create a TEnumBlock for the enums do block
36+
enum_block =
37+
RBI::TEnumBlock.new do |block|
38+
enum_values.each_key do |key|
39+
# Add each enum value
40+
block.create_constant(key.to_s.camelize, value: "new")
41+
end
42+
end
43+
44+
tenum << enum_block
45+
end
46+
47+
model << enum_class
48+
49+
column = constant.columns_hash[enum_name]
50+
nullability = column.null
51+
base_enum_type = "#{constant}::#{enum_class_name}"
52+
enum_type = nullability ? "T.nilable(#{base_enum_type})" : base_enum_type
53+
54+
# Generate typed getter
55+
model.create_method("typed_#{enum_name}", return_type: enum_type)
56+
57+
# Generate typed setter
58+
model.create_method(
59+
"typed_#{enum_name}=",
60+
parameters: [create_param("value", type: enum_type)],
61+
return_type: "void",
62+
)
63+
end
64+
end
65+
end
66+
end
67+
end
68+
end
69+
end

0 commit comments

Comments
 (0)