class Parent
VALID_COLORS = ["blue"]
MyStruct = Struct.new(:color) do
def initialize(color:)
raise "invalid color" unless self.class::VALID_COLORS.include?(color)
super(color: color)
end
end
end
class Subclass < Parent
VALID_COLORS = ["red"]
def call
MyStruct.new(color: "red")
end
end
> Subclass.new.call
=> uninitialized constant Parent::MyStruct::VALID_COLORS
How can I access the correct VALID_COLORS
constant in my struct initialization? Normally self.class::CONSTANT
would allow you to access the subclass's constant while in the parent class, but it looks like self.class
will bring up the struct instead while inside the struct. There are some workarounds I've thought of like 1) adding a new parameter to the struct to take in valid colors; or 2) setting VALID_COLORS
as an instance variable instead of class variable. But I'm curious if there's a way to make my current implementation work as intended?
CodePudding user response:
I'm curious if there's a way to make my current implementation work as intended?
Since you asked for it, let's give it a try.
First of all, your Parent::MyStruct
already isn't working:
Parent::MyStruct.new(color: 'blue')
#=> NameError: uninitialized constant Parent::MyStruct::VALID_COLORS
That's because VALID_COLORS
is defined in Parent
, not in Parent::MyStruct
. To dynamically reference the parent's namespace (in terms of nesting), Rails has module_parent
:
Parent::MyStruct.module_parent #=> Parent
Applied to your code: (I've also added keyword_init: true
to make the struct take actual keyword arguments, see the docs for Struct.new
)
class Parent
VALID_COLORS = ["blue"]
MyStruct = Struct.new(:color, keyword_init: true) do
def initialize(color:)
raise "invalid color" unless self.class.module_parent::VALID_COLORS.include?(color)
# ^^^^^^^^^^^^^
super
end
end
end
Which gives:
Parent::MyStruct.new(color: 'blue')
#=> #<struct Parent::MyStruct color="blue">
Now that we've fixed that one, let's try to create a struct through the subclass:
Subclass::MyStruct.new(color: 'red')
#=> RuntimeError: invalid color
This error occurs, because Subclass::MyStruct
is in fact Parent::MyStruct
:
Subclass::MyStruct #=> Parent::MyStruct
To fix this, Subclass
needs its very own MyStruct
. We can solve it by creating a new class which inherits from Parent::MyStruct
:
class Subclass < Parent
VALID_COLORS = ["red"]
MyStruct = Class.new(Parent::MyStruct)
end
The Parent::
prefix is actually optional, but MyStruct = Class.new(MyStruct)
looks too confusing.
The above gives:
struct = Subclass::MyStruct.new(color: 'red')
#=> #<struct Subclass::MyStruct color="red">
struct.is_a?(Subclass:MyStruct) #=> true
struct.is_a?(Parent::MyStruct) #=> true
If you really want to use this approach, you could even move the dynamic class creation into Parent
using the inherited
callback:
class Parent
def self.inherited(subclass)
subclass.const_set(:MyStruct, Class.new(self::MyStruct))
end
# ...
end
CodePudding user response:
A VALID_COLORS
is defined inside Parent
and inside Subclass
(which is irrelevant here). The initializer of Parent::MyStruct
also tries to access a constant of this name, but MyStruct
does not have a VALID_COLORS
.
I don't know what exactly you want to achieve, but depending on what you want, you can either do a
... unless Parent::VALID_COLORS.include?(color)
or a
... unless Subclass::VALID_COLORS.include?(color)
or provide another VALID_COLORS
constant for MyStruct
.
CodePudding user response:
The issue here is that the MyStruct
constant is literally a reference to the object defined on Parent
. When Parent
is subclassed, the MyStruct
object is not redefined, the reference is just copied. We can verify this with the following:
irb(main):014:0> Parent::MyStruct.object_id
=> 70326616692980
irb(main):015:0> Subclass::MyStruct.object_id
=> 70326616692980
To get around this, you could create a method that initializes the class object on the fly:
class Parent
private
def my_struct
@my_struct ||= Struct.new(:color) do
def initialize(color:)
raise "invalid color" unless self.class::VALID_COLORS.include?(color)
super(color: color)
end
end
end
end
If you're willing to do a bit more work, making color it's own class instead of a struct would be a lot more robust:
class Color
attr_accessor :color
def initialize(color: color, valid_colors: Parent:: VALID_COLORS)
raise "invalid color" unless valid_colors.include?(color)
@color = color
end
end
class Parent
def self.color(color)
Color.new(color: color, valid_colors: self.class::VALID_COLORS)
end
end
Or you could try to investigate the caller to load the correct constant (I highly advise against this):
MyStruct = Struct.new(:color) do
def initialize(color:)
caller_path = caller_locations(1,1)[0].absolute_path
valid_color_lib = {
"parent.rb" => Parent::VALID_COLORS,
"subclass.rb" => Subclass::VALID_COLORS
}[caller_path]
raise "invalid color" unless valid_color_lib.include?(color)
super(color: color)
end
end