Module: Familia::Horreum::RelatedFieldsManagement
- Included in:
- DefinitionMethods, ManagementMethods
- Defined in:
- lib/familia/horreum/related_fields.rb
Overview
RelatedFieldsManagement - Class-level methods for defining DataType relationships
This module uses metaprogramming to dynamically create field definition methods that generate both class-level and instance-level accessor methods for DataTypes (e.g., list, set, zset, hashkey, string).
When included in a class via ManagementMethods, it provides class methods like:
- Customer.list :recent_orders # defines class method for class-level list
- customer.recent_orders # creates instance method returning list instance
Key metaprogramming features:
- Dynamically defines DSL methods for each Database type (e.g., set, list, hashkey)
- Each DSL method creates corresponding instance/class accessor methods
- Provides query methods for checking relation types
Usage: Include this module in classes that need DataType management Call setup_related_fields_definition_methods to initialize the feature
Defined Under Namespace
Modules: RelatedFieldsAccessors
Instance Method Summary collapse
-
#attach_class_related_field(name, klass, opts) ⇒ Object
Creates a class-level relation.
-
#attach_instance_related_field(name, klass, opts) ⇒ Object
Creates an instance-level relation.
-
#configure_related_field(name, scope: nil, **opts) ⇒ Familia::RelatedFieldDefinition
Reconfigures a related field after the class body has run.
Instance Method Details
#attach_class_related_field(name, klass, opts) ⇒ Object
Creates a class-level relation
The DataType is built lazily on first access rather than at declaration, so configure_related_field can still adjust the definition between the class body and first use. The first access freezes that definition's opts, closing the window for this field only (Klass.instances is touched on every save and must not close it for unrelated fields).
Because the build is deferred, options are validated here so a bad
max_length: still fails at the declaration line, not on first
access. Re-declaring a name whose collection has already been built
raises RelatedFieldFrozenError: the cached DataType would keep
serving the old options while the registry showed the new ones.
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/familia/horreum/related_fields.rb', line 255 def (name, klass, opts) Familia.trace :attach_class_related_field, "#{name} #{klass}", opts if Familia.debug? raise ArgumentError, 'Name is blank (klass)' if name.to_s.empty? name = name.to_s.to_sym opts = opts.nil? ? {} : opts.dup opts[:parent] = self unless opts.key?(:parent) (opts, klass) # Accessors before the registry entry, for the same reason as # attach_instance_related_field: readers of a registry snapshot # (guard_atomic_write_database!, the class-level destroy!) must never # see a name whose accessor does not exist yet. define_singleton_method name do (name) end define_singleton_method :"#{name}=" do |v| send(name).replace v end define_singleton_method :"#{name}?" do !send(name).empty? end # Check-and-replace under the lock materialize_class_related_field # freezes under; see attach_instance_related_field for the race. .synchronize do ([name], "#{self}.#{name}") [name] = RelatedFieldDefinition.new(name, klass, opts) end end |
#attach_instance_related_field(name, klass, opts) ⇒ Object
Creates an instance-level relation
Options are validated here, at declaration, with the same checks the DataType constructor runs (see validate_related_field_opts!).
Re-declaring a name whose definition has already materialized (an
instance of this class exists) raises RelatedFieldFrozenError:
instances built before and after the re-declaration would otherwise
disagree about the field's options. Re-declaring BEFORE first use
replaces the definition, as it always has. The same name may also
exist at class level (zset :instances alongside the automatic
class_sorted_set :instances); configure_related_field takes a
+scope:+ keyword to reach the class-level one.
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 |
# File 'lib/familia/horreum/related_fields.rb', line 178 def (name, klass, opts) Familia.trace :attach_instance_related_field, name, klass, opts if Familia.debug? raise ArgumentError, "Name is blank (#{klass})" if name.to_s.empty? name = name.to_s.to_sym # Dup: the lifecycle freezes this Hash at first materialization and # must not freeze an object the caller still owns. opts = opts.nil? ? {} : opts.dup (opts, klass) # Accessors first, registry entry second. The cascades # (update_expiration, persist!, ttl_report, destroy!) snapshot the # registry and call +send(name)+ on the instance; a definition that # landed before define_method ran would let a cascade snapshotting # in between raise NoMethodError. The accessors depend only on the # name, so redefining them for a re-declaration the check below # refuses is harmless. # # Lazy-initializing accessor. Three paths: # # 1. @<name> is set: return it (every access after the first). # 2. Relatives never initialized (initialize overridden without # super, or the load path): run initialize_relatives, which builds # every definition in the registry, this one included. # 3. Relatives initialized but @<name> still nil: the field was # declared after this instance took its snapshot (participates_in # at load, or any late declaration). Materialize just this one # from the current definition (see materialize_related_field); # the instance is not recreated and the cascades that walk the # current registry keep working on it. define_method name do ivar = :"@#{name}" value = instance_variable_get(ivar) return value unless value.nil? # Check singleton class to avoid polluting instance variables unless singleton_class.instance_variable_defined?(:@relatives_initialized) initialize_relatives value = instance_variable_get(ivar) return value unless value.nil? end (name) end define_method :"#{name}=" do |val| send(name).replace val end define_method :"#{name}?" do !send(name).empty? end # Check-and-replace under the same lock initialize_relatives freezes # under. Unlocked, a re-declaration could pass the frozen? check, # then the first instance builds and freezes the OLD definition, then # the replacement lands: first instance on 10, registry (and every # later instance) on 20. .synchronize do ([name], "#{self}##{name}") [name] = RelatedFieldDefinition.new(name, klass, opts) end end |
#configure_related_field(name, scope: nil, **opts) ⇒ Familia::RelatedFieldDefinition
Reconfigures a related field after the class body has run.
Lifecycle:
- Declare in the class body (
sorted_set :events, max_length: 100). - Reconfigure at boot, before any instance is created or the class-level collection is accessed.
- Frozen at first use: instance-level definitions freeze together at the end of the first initialize_relatives; each class-level definition freezes on its own first accessor call. A later call raises rather than leaving already-built DataTypes on old options.
New declarations on a materialized class remain allowed (participation relies on that); only reconfiguring or re-declaring a frozen definition is refused.
A name can be declared at both levels (zset :instances next to the
automatic class_sorted_set :instances). Without +scope:+ the
instance-level definition wins when both exist and the class-level
one is used when only it exists; pass +scope: :class+ (or
+scope: :instance+) to address one level explicitly, in which case
only that registry is consulted.
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 |
# File 'lib/familia/horreum/related_fields.rb', line 331 def (name, scope: nil, **opts) name = name.to_s.to_sym .synchronize do registry = (name, scope) unless registry.key?(name) level = { instance: 'instance-level ', class: 'class-level ' }[scope] raise ArgumentError, "#{self} has no #{level}related field #{name.inspect}" end definition = registry[name] if definition.opts.frozen? scope = if registry.equal?() "instance-level #{self}##{name}: instances already exist" else "class-level #{self}.#{name}: the collection was already built" end raise Familia::RelatedFieldFrozenError, "Cannot reconfigure #{scope}. configure_related_field must run before first use." end merged = definition.opts.merge(opts) (merged, definition.klass) registry[name] = definition.with(opts: merged) end end |