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_keys, #encryption_personalization, #schema_path, #schema_validator, #schemas, #transaction_mode

Instance Method Summary collapse

Methods included from RelatedFieldsManagement

#attach_class_related_field, #attach_instance_related_field

Methods included from Settings

#configure, #default_suffix, #pipelined_mode, #pipelined_mode=

Instance Method Details

#add_feature_options(feature_name, **options) ⇒ Hash

Note:

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.

Examples:

Per-class storage behavior

class ModelA < Familia::Horreum
  # This stores options in ModelA's @feature_options
  add_feature_options(:my_feature, key: 'value_a')
end

class ModelB < Familia::Horreum
  # This stores options in ModelB's @feature_options (separate from ModelA)
  add_feature_options(:my_feature, key: 'value_b')
end

Parameters:

  • feature_name (Symbol)

    The feature name

  • options (Hash)

    The options to add/merge

Returns:

  • (Hash)

    The updated options for the feature



360
361
362
363
364
365
366
367
368
369
370
# File 'lib/familia/horreum/definition.rb', line 360

def add_feature_options(feature_name, **options)
  @feature_options ||= {}
  @feature_options[feature_name.to_sym] ||= {}

  # Only set defaults for options that don't already exist
  options.each do |key, value|
    @feature_options[feature_name.to_sym][key] ||= value
  end

  @feature_options[feature_name.to_sym]
end


229
230
231
232
# File 'lib/familia/horreum/definition.rb', line 229

def class_related_fields
  @class_related_fields ||= {}
  @class_related_fields
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.

Examples:

Getting options for a specific feature

class MyModel < Familia::Horreum
  feature :object_identifier, generator: :uuid_v4
end

MyModel.feature_options(:object_identifier) #=> {generator: :uuid_v4}
MyModel.feature_options                     #=> {object_identifier: {generator: :uuid_v4}}

Per-class isolation

class UserModel < Familia::Horreum
  feature :object_identifier, generator: :uuid_v4
end

class SessionModel < Familia::Horreum
  feature :object_identifier, generator: :hex
end

UserModel.feature_options(:object_identifier)    #=> {generator: :uuid_v4}
SessionModel.feature_options(:object_identifier) #=> {generator: :hex}

Parameters:

  • feature_name (Symbol, String, nil) (defaults to: nil)

    the name of the feature to get options for. If nil, returns the entire feature options hash for this class.

Returns:

  • (Hash)

    the feature options hash, either for a specific feature or all features



325
326
327
328
329
330
# File 'lib/familia/horreum/definition.rb', line 325

def feature_options(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.

Parameters:

  • name (Symbol, String)

    the name of the field to define. If a method with the same name already exists, an error is raised.

  • as (Symbol, String, false, nil) (defaults to: name)

    as the name to use for the accessor method (defaults to name). If false or nil, no accessor methods are created.

  • fast_method (Symbol, false, nil) (defaults to: :"#{name}!")

    the name to use for the fast writer method (defaults to :"#{name}!"). If false or nil, no fast writer method is created.

  • on_conflict (Symbol) (defaults to: :raise)

    conflict resolution strategy when method already exists:

    • :raise - raise error if method exists (default)
    • :skip - skip definition if method exists
    • :warn - warn but proceed (may overwrite)
    • :ignore - proceed silently (may overwrite)


178
179
180
181
# File 'lib/familia/horreum/definition.rb', line 178

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.

Examples:

Manual field grouping

class User < Familia::Horreum
  field_group :personal_info do
    field :name
    field :email
  end
end

User.personal_info  # => [:name, :email]

Initialize empty group

class User < Familia::Horreum
  field_group :placeholder
end

User.placeholder  # => []

Parameters:

  • name (Symbol, String)

    the name of the field group

Yields:

  • optional block for defining fields within the group

Returns:

  • (Array<Symbol>)

    the array of field names in the group

Raises:



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/familia/horreum/definition.rb', line 83

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_groupsArray<Symbol>

Returns the list of all field group names defined for the class.

Examples:

class User < Familia::Horreum
  field_group :personal_info do
    field :name
  end
  field_group :metadata do
    field :created_at
  end
end

User.field_groups  # => [
  :personal_info => [...],
  :metadata => [..]
]

Returns:

  • (Array<Symbol>)

    array of field group names



128
129
130
131
132
133
# File 'lib/familia/horreum/definition.rb', line 128

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_mapObject

Returns a hash mapping field names to method names for backward compatibility



252
253
254
# File 'lib/familia/horreum/definition.rb', line 252

def field_method_map
  field_types.transform_values(&:method_name)
end

#field_typesObject

Storage for field type instances



244
245
246
247
248
249
# File 'lib/familia/horreum/definition.rb', line 244

def field_types
  @field_types_mutex ||= Familia::ThreadSafety::InstrumentedMutex.new('field_types')
  @field_types || @field_types_mutex.synchronize do
    @field_types ||= {}
  end
end

#fieldsObject

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].



222
223
224
225
226
227
# File 'lib/familia/horreum/definition.rb', line 222

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.

Parameters:

  • val (Object) (defaults to: nil)

    the field name or method to set as the identifier field (optional).

Returns:

  • (Object)

    the current identifier field.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/familia/horreum/definition.rb', line 144

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



214
215
216
217
218
# File 'lib/familia/horreum/definition.rb', line 214

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_fieldsObject

Get fields for serialization (excludes transients)



257
258
259
260
261
# File 'lib/familia/horreum/definition.rb', line 257

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.

Parameters:

  • a (String, Symbol, nil)

    the prefix to set (optional).

Returns:

  • (String, Symbol)

    the current prefix.



203
204
205
206
207
208
209
210
211
212
# File 'lib/familia/horreum/definition.rb', line 203

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.

Parameters:

  • field_type (FieldType)

    The field type to register



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/familia/horreum/definition.rb', line 278

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


234
235
236
237
# File 'lib/familia/horreum/definition.rb', line 234

def related_fields
  @related_fields ||= {}
  @related_fields
end

#relations?Boolean

Returns:

  • (Boolean)


239
240
241
# File 'lib/familia/horreum/definition.rb', line 239

def relations?
  @has_related_fields ||= false
end

#suffix(val = nil, &blk) ⇒ String, Symbol

Sets or retrieves the suffix for generating Valkey/Redis keys.

Parameters:

  • a (String, Symbol, nil)

    the suffix to set (optional).

  • blk (Proc)

    a block that returns the suffix (optional).

Returns:

  • (String, Symbol)

    the current suffix or Familia.default_suffix if none is set.



189
190
191
192
# File 'lib/familia/horreum/definition.rb', line 189

def suffix(val = nil, &blk)
  @suffix = val || blk if val || !blk.nil?
  @suffix || Familia.default_suffix
end

#transient_fieldsObject

Get fields that are not persisted to the database (transients)



264
265
266
267
268
# File 'lib/familia/horreum/definition.rb', line 264

def transient_fields
  fields.select do |field|
    field_types[field]&.transient?
  end
end