Module: Familia::Horreum::DefinitionMethods
- Includes:
- RelatedFieldsManagement, Settings
- Defined in:
- lib/familia/horreum/definition.rb
Overview
DefinitionMethods - Class-level DSL methods for defining Horreum model structure
This module is extended into classes that include Familia::Horreum, providing class methods for defining model structure and configuration (e.g., Customer.field :name, Customer.identifier_field :custid).
Key features:
- Defines DSL methods for field definitions (field, identifier_field)
- Includes RelatedFieldsManagement for DataType field DSL (list, set, zset, etc.)
- Provides class-level configuration (prefix, suffix, logical_database)
- Manages field metadata and inheritance
Instance Attribute Summary
Attributes included from Settings
#current_key_version, #default_expiration, #delim, #encryption_hkdf_salt, #encryption_hkdf_salt_history, #encryption_keys, #encryption_personalization, #encryption_personalization_history, #raise_on_unsaved_parent_write, #schema_path, #schema_validator, #schemas, #strict_write_order, #transaction_mode
Class Method Summary collapse
-
.extended(base) ⇒ Object
Per-class locks and cache must exist before any thread can race on them, so they are created when the module is extended (Horreum's +inherited+ hook), not lazily on first use.
Instance Method Summary collapse
-
#add_feature_options(feature_name, **options) ⇒ Hash
Add feature options for a specific feature.
-
#class_related_field_build_lock ⇒ Object
Serializes the DataType constructions that must happen exactly once: class-level collections (materialize_class_related_field) and instance-level fields built after the instance's initial snapshot (Horreum#materialize_related_field, for a field declared late).
-
#class_related_field_cache ⇒ Object
Built class-level DataTypes, keyed by field name.
- #class_related_fields ⇒ Object
-
#class_related_fields_snapshot ⇒ Object
Class-level counterpart to +related_fields_snapshot+.
-
#dirty_write_warnings(mode = nil) ⇒ Symbol
Sets or retrieves how collection writes on this class's DataTypes react when the parent instance has unsaved scalar field changes.
- #dirty_write_warnings=(mode) ⇒ Object
-
#feature_options(feature_name = nil) ⇒ Hash
Retrieves feature options for the current class.
-
#field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise) ⇒ Object
Defines a field for the class and creates accessor methods.
-
#field_group(name) { ... } ⇒ Array<Symbol>
Defines a field group to organize related fields.
-
#field_groups ⇒ Array<Symbol>
Returns the list of all field group names defined for the class.
-
#field_method_map ⇒ Object
Returns a hash mapping field names to method names for backward compatibility.
-
#field_types ⇒ Object
Storage for field type instances.
-
#fields ⇒ Object
Returns the list of field names defined for the class in the order that they were defined.
-
#identifier_field(val = nil) ⇒ Object
Sets or retrieves the unique identifier field for the class.
- #logical_database(num = nil) ⇒ Object
-
#persistent_fields ⇒ Object
Get fields for serialization (excludes transients).
-
#prefix(val = nil) ⇒ String, Symbol
Sets or retrieves the prefix for generating Valkey/Redis keys.
-
#register_field_type(field_type) ⇒ Object
Register a field type instance with this class.
- #related_fields ⇒ Object
-
#related_fields_mutex ⇒ Object
Guards the related-field registries: declaration and re-declaration (attach_*_related_field), reconfiguration (configure_related_field) and the freeze that closes the window (initialize_relatives and materialize_class_related_field).
-
#related_fields_snapshot ⇒ Object
Returns a copy of the instance-level related-field registry, taken under related_fields_mutex.
- #relations? ⇒ Boolean
-
#suffix(val = nil, &blk) ⇒ String, Symbol
Sets or retrieves the suffix for generating Valkey/Redis keys.
-
#transient_fields ⇒ Object
Get fields that are not persisted to the database (transients).
Methods included from RelatedFieldsManagement
#attach_class_related_field, #attach_instance_related_field, #configure_related_field
Methods included from Settings
#configure, #default_suffix, #pipelined_mode, #pipelined_mode=
Class Method Details
.extended(base) ⇒ Object
Per-class locks and cache must exist before any thread can race on them, so they are created when the module is extended (Horreum's +inherited+ hook), not lazily on first use.
338 339 340 341 342 343 |
# File 'lib/familia/horreum/definition.rb', line 338 def self.extended(base) base.instance_variable_set(:@related_fields_mutex, Familia::ThreadSafety::InstrumentedMutex.new('related_fields')) base.instance_variable_set(:@class_related_field_build_lock, ::Monitor.new) base.instance_variable_set(:@class_related_field_cache, {}) end |
Instance Method Details
#add_feature_options(feature_name, **options) ⇒ Hash
This method only sets defaults for options that don't already exist, using the ||= operator to prevent overwrites.
Add feature options for a specific feature
This method provides a clean way for features to set their default options without worrying about initialization state. Similar to register_field_type for field types.
Feature options are stored at the class level using instance variables, ensuring complete isolation between different Familia::Horreum subclasses. Each class maintains its own @feature_options hash.
491 492 493 494 495 496 497 498 499 500 501 |
# File 'lib/familia/horreum/definition.rb', line 491 def (feature_name, **) @feature_options ||= {} @feature_options[feature_name.to_sym] ||= {} # Only set defaults for options that don't already exist .each do |key, value| @feature_options[feature_name.to_sym][key] ||= value end @feature_options[feature_name.to_sym] end |
#class_related_field_build_lock ⇒ Object
Serializes the DataType constructions that must happen exactly once: class-level collections (materialize_class_related_field) and instance-level fields built after the instance's initial snapshot (Horreum#materialize_related_field, for a field declared late). A reentrant ::Monitor rather than a Mutex: a custom type's +init+ may read a sibling collection of the same class from inside the build, which re-enters this lock on the same thread. One per class, not per field or instance: a per-name table would need its own guarded creation, and serializing these one-time builds is cheap. The ordinary per-instance build in initialize_relatives does not take it.
Lock order is build lock, then related_fields_mutex. Nothing takes them the other way round: the registry paths (attach_*, configure_related_field, the freeze in initialize_relatives) hold the mutex alone and never construct under it.
Not ::Monitor's namesake Familia::ThreadSafety::Monitor, which is the contention reporter.
331 332 333 |
# File 'lib/familia/horreum/definition.rb', line 331 def @class_related_field_build_lock end |
#class_related_field_cache ⇒ Object
Built class-level DataTypes, keyed by field name. A dedicated Hash
rather than @
292 293 294 |
# File 'lib/familia/horreum/definition.rb', line 292 def @class_related_field_cache end |
#class_related_fields ⇒ Object
282 283 284 285 |
# File 'lib/familia/horreum/definition.rb', line 282 def @class_related_fields ||= {} @class_related_fields end |
#class_related_fields_snapshot ⇒ Object
Class-level counterpart to +related_fields_snapshot+. Same mutex, same writer (attach_class_related_field), same hazard when iterated live.
366 367 368 |
# File 'lib/familia/horreum/definition.rb', line 366 def .synchronize { .dup } end |
#dirty_write_warnings(mode = nil) ⇒ Symbol
Sets or retrieves how collection writes on this class's DataTypes react when the parent instance has unsaved scalar field changes.
Mirrors +Familia.strict_write_order+ but is scoped to a single Horreum subclass, so seed scripts, bulk importers, and known-safe call sites can opt down without touching the global setting.
Resolution order (highest precedence first):
- Active +atomic_write+ block -- suppresses everything.
- Class-level +:off+ -- suppresses both warnings AND raises, overriding +strict_write_order+ and +raise_on_unsaved_parent_write+. "Off means off."
- Raise gates: +Familia.strict_write_order = true+, class +:strict+, or a new/unsaved parent when +raise_on_unsaved_parent_write+ is true.
- Otherwise warn per the resolved mode: this class-level setting (inherited through the subclass chain), else +Familia.dirty_write_warnings+ (:once when unset).
- :strict - raise Familia::Problem (overrides global strict_write_order=false)
- :warn - warn on every collection write (legacy behavior)
- :once - warn once per distinct dirty-field signature per window [default]
- :off - suppress entirely for this class
255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/familia/horreum/definition.rb', line 255 def dirty_write_warnings(mode = nil) unless mode.nil? valid = %i[strict warn once off] unless valid.include?(mode) raise ArgumentError, "dirty_write_warnings must be one of #{valid.inspect}, got #{mode.inspect}" end return @dirty_write_warnings = mode end @dirty_write_warnings || (superclass.respond_to?(:dirty_write_warnings) ? superclass.dirty_write_warnings : nil) || Familia.dirty_write_warnings end |
#dirty_write_warnings=(mode) ⇒ Object
269 270 271 |
# File 'lib/familia/horreum/definition.rb', line 269 def dirty_write_warnings=(mode) dirty_write_warnings(mode) end |
#feature_options(feature_name = nil) ⇒ Hash
Retrieves feature options for the current class.
Feature options are stored per-class in instance variables, ensuring complete isolation between different Familia::Horreum subclasses. Each class maintains its own @feature_options hash that does not interfere with other classes' configurations.
456 457 458 459 460 461 |
# File 'lib/familia/horreum/definition.rb', line 456 def (feature_name = nil) @feature_options ||= {} return @feature_options if feature_name.nil? @feature_options[feature_name.to_sym] || {} end |
#field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise) ⇒ Object
Defines a field for the class and creates accessor methods.
This method defines a new field for the class, creating getter and setter
instance methods similar to attr_accessor. It also generates a fast
writer method for immediate persistence to the database.
183 184 185 186 |
# File 'lib/familia/horreum/definition.rb', line 183 def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise) field_type = FieldType.new(name, as: as, fast_method: fast_method, on_conflict: on_conflict) register_field_type(field_type) end |
#field_group(name) { ... } ⇒ Array<Symbol>
Defines a field group to organize related fields.
Field groups provide a way to categorize and query fields by purpose or feature. When a block is provided, fields defined within the block are automatically added to the group. Without a block, an empty group is initialized.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/familia/horreum/definition.rb', line 88 def field_group(name, &block) # Prevent nested field groups if @current_field_group raise Familia::Problem, "Cannot define field group :#{name} while :#{@current_field_group} is being defined. " \ "Nested field groups are not supported." end # Initialize group field_groups[name.to_sym] ||= [] if block_given? @current_field_group = name.to_sym begin instance_eval(&block) ensure @current_field_group = nil end else Familia.debug "[field_group] Created field group :#{name} but no block given" end field_groups[name.to_sym] end |
#field_groups ⇒ Array<Symbol>
Returns the list of all field group names defined for the class.
133 134 135 136 137 138 |
# File 'lib/familia/horreum/definition.rb', line 133 def field_groups @field_groups_mutex ||= Familia::ThreadSafety::InstrumentedMutex.new('field_groups') @field_groups || @field_groups_mutex.synchronize do @field_groups ||= {} end end |
#field_method_map ⇒ Object
Returns a hash mapping field names to method names for backward compatibility
383 384 385 |
# File 'lib/familia/horreum/definition.rb', line 383 def field_method_map field_types.transform_values(&:method_name) end |
#field_types ⇒ Object
Storage for field type instances
375 376 377 378 379 380 |
# File 'lib/familia/horreum/definition.rb', line 375 def field_types @field_types_mutex ||= Familia::ThreadSafety::InstrumentedMutex.new('field_types') @field_types || @field_types_mutex.synchronize do @field_types ||= {} end end |
#fields ⇒ Object
Returns the list of field names defined for the class in the order
that they were defined. i.e. field :a; field :b; fields => [:a, :b].
275 276 277 278 279 280 |
# File 'lib/familia/horreum/definition.rb', line 275 def fields @fields_mutex ||= Familia::ThreadSafety::InstrumentedMutex.new('fields') @fields || @fields_mutex.synchronize do @fields ||= [] end end |
#identifier_field(val = nil) ⇒ Object
Sets or retrieves the unique identifier field for the class.
This method defines or returns the field or method that contains the unique identifier used to generate the dbkey for the object. If a value is provided, it sets the identifier field; otherwise, it returns the current identifier field.
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/familia/horreum/definition.rb', line 149 def identifier_field(val = nil) if val # Validate identifier field definition at class definition time case val when Symbol, String, Proc @identifier_field = val else raise Problem, <<~ERROR Invalid identifier field definition: #{val.inspect}. Use a field name (Symbol/String) or Proc. ERROR end end @identifier_field end |
#logical_database(num = nil) ⇒ Object
219 220 221 222 223 |
# File 'lib/familia/horreum/definition.rb', line 219 def logical_database(num = nil) Familia.trace :LOGICAL_DATABASE_DEF, "instvar:#{@logical_database}", num if Familia.debug? @logical_database = num unless num.nil? @logical_database || parent&.logical_database end |
#persistent_fields ⇒ Object
Get fields for serialization (excludes transients)
388 389 390 391 392 |
# File 'lib/familia/horreum/definition.rb', line 388 def persistent_fields fields.select do |field| field_types[field]&.persistent? end end |
#prefix(val = nil) ⇒ String, Symbol
Sets or retrieves the prefix for generating Valkey/Redis keys.
The exception is only raised when both @prefix is nil/falsy AND name is nil, which typically occurs with anonymous classes that haven't had their prefix explicitly set.
208 209 210 211 212 213 214 215 216 217 |
# File 'lib/familia/horreum/definition.rb', line 208 def prefix(val = nil) @prefix = val if val @prefix || begin if name.nil? raise Problem, 'Cannot generate prefix for anonymous class. ' \ 'Use `prefix` method to set explicitly.' end config_name.to_sym end end |
#register_field_type(field_type) ⇒ Object
Register a field type instance with this class
This method installs the field type's methods and registers it for later reference. It maintains backward compatibility by creating FieldDefinition objects.
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 |
# File 'lib/familia/horreum/definition.rb', line 409 def register_field_type(field_type) fields << field_type.name field_type.install(self) # Complete the registration after installation. If we do this beforehand # we can run into issues where it looks like it's already installed. field_types[field_type.name] = field_type # Add to current field group if one is active if @current_field_group @field_groups[@current_field_group] << field_type.name end # Freeze the field_type to ensure immutability (maintains Data class heritage) field_type.freeze end |
#related_fields ⇒ Object
345 346 347 348 |
# File 'lib/familia/horreum/definition.rb', line 345 def @related_fields ||= {} @related_fields end |
#related_fields_mutex ⇒ Object
Guards the related-field registries: declaration and re-declaration
(attach_*_related_field), reconfiguration (configure_related_field)
and the freeze that closes the window (initialize_relatives and
materialize_class_related_field). One per class, like fields_mutex,
but created eagerly in +extended+ rather than with ||=: two threads
taking the lock for the first time would otherwise each allocate a
mutex and exclude nothing.
Backed by a non-reentrant Mutex. The lifecycle only holds it around registry reads, replacements and the opts freeze; DataType construction (which runs user-overridable setters and +init+) happens outside it so a custom type may touch other collections.
308 309 310 |
# File 'lib/familia/horreum/definition.rb', line 308 def @related_fields_mutex end |
#related_fields_snapshot ⇒ Object
Returns a copy of the instance-level related-field registry, taken under related_fields_mutex.
attach_instance_related_field adds keys under that mutex; a cascade loop that iterates the live Hash instead would, if a new field were declared mid-iteration (application autoloading finishing a participates_in while an early request saves or expires a record), make MRI raise "can't add a new key into hash during iteration" in the declaring thread. Iterating this copy closes that window and releases the lock before the Redis calls in the cascade run.
360 361 362 |
# File 'lib/familia/horreum/definition.rb', line 360 def .synchronize { .dup } end |
#relations? ⇒ Boolean
370 371 372 |
# File 'lib/familia/horreum/definition.rb', line 370 def relations? @has_related_fields ||= false end |
#suffix(val = nil, &blk) ⇒ String, Symbol
Sets or retrieves the suffix for generating Valkey/Redis keys.
194 195 196 197 |
# File 'lib/familia/horreum/definition.rb', line 194 def suffix(val = nil, &blk) @suffix = val || blk if val || !blk.nil? @suffix || Familia.default_suffix end |
#transient_fields ⇒ Object
Get fields that are not persisted to the database (transients)
395 396 397 398 399 |
# File 'lib/familia/horreum/definition.rb', line 395 def transient_fields fields.select do |field| field_types[field]&.transient? end end |