I've got a few classes with a constant SCHEMA
class Consumable::ProbeDesign < Consumable
SCHEMA = {
"type": "object",
"properties": { },
"required": []
}
end
class DataModule::WaterDeprivationLog < DataModule
SCHEMA = {
"type": "object",
"properties": {
"water_amount": {"type": "decimal"},
"notes": {"type": "string"}
},
"required": []
}
end
which are children of a base class in an STI scheme
class Consumable < ApplicationRecord
include SingleTableInheritable
end
class DataModule < ApplicationRecord
include SingleTableInheritable
end
and then I have a module which needs to access that constant dynamically for all of the classes inherited from classes which include the module
module SingleTableInheritable
extend ActiveSupport::Concern
included do
def self.inherited(subclass)
subclass.class_eval do
schema = subclass::SCHEMA # NameError: uninitialized constant #<Class:0x0000560848920be8>::SCHEMA
# then do some validations that rely on that schema value
end
super
end
end
end
But at the time of execution and within the context of how it is called it cannot find the module and returns NameError: uninitialized constant #<Class:0x0000560848920be8>::SCHEMA
Note that the subclass.const_get("SCHEMA")
also fails
edit: This is an loading order problem. Right after this runs on a class, the constant is available because the class is then loaded. But by trying to eager load this class, the module gets inherited from the parent class on eager load and the module code still runs before the constant is set.
Is there some kind of hook like inherited but that allows everything to preload?
The issue here is really that Module#included
will always be run before the the body of the subclass is evaluated. But Module#included
is not the only way to add validations or callbacks.
Your can define your own "hook" instead:
module SingleTableInheritance
extend ActiveSupport::Concern
class_methods do
def define_schema(hash)
class_eval do
const_set(:SCHEMA, hash)
if self::SCHEMA["type"] == "object"
validates :something ...
end
end
end
end
end
define_schema
is just a plain old class method that opens up the eigenclass. This is the same pattern that's used everywhere in Rails and Ruby in general for everything from generating setters and getters to validations and even callbacks.
Usage:
class DataModule::WaterDeprivationLog < DataModule
define_schema({
type: "object",
properties: {
water_amount: { type: "decimal"},
notes: { type: "string"}
},
required: []
})
end
You should also be aware that the "short" hash syntax which you are using coerces the keys into symbols:
irb(main):033:0> {"foo": "bar" }
=> {:foo=>"bar"}
If you want to have strings as keys use hashrockets =>
instead. {"foo": "bar" }
is regarded as bad form as the intent is very unclear.