Class: Familia::FieldType
- Inherits:
-
Object
- Object
- Familia::FieldType
- Defined in:
- lib/familia/field_type.rb
Overview
Base class for all field types in Familia
Field types encapsulate the behavior for different kinds of fields, including how their getter/setter methods are defined and how values are serialized/deserialized.
Direct Known Subclasses
EncryptedFieldType, Familia::Features::ExternalIdentifier::ExternalIdentifierFieldType, Familia::Features::ObjectIdentifier::ObjectIdentifierFieldType, TransientFieldType
Instance Attribute Summary collapse
-
#fast_method_name ⇒ Object
readonly
Returns the value of attribute fast_method_name.
-
#loggable ⇒ Object
readonly
Returns the value of attribute loggable.
-
#method_name ⇒ Object
readonly
Returns the value of attribute method_name.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#on_conflict ⇒ Object
readonly
Returns the value of attribute on_conflict.
-
#options ⇒ Object
readonly
Returns the value of attribute options.
Instance Method Summary collapse
-
#category ⇒ Symbol
The category for this field type (used for filtering).
-
#define_fast_writer(klass) ⇒ Object
Define the fast writer method on the target class.
-
#define_getter(klass) ⇒ Object
Define the getter method on the target class.
-
#define_setter(klass) ⇒ Object
Define the setter method on the target class.
-
#deserialize(value, _record = nil) ⇒ Object
Deserialize a value from database storage.
-
#generated_methods ⇒ Array<Symbol>
Returns all method names generated for this field (used for conflict detection).
-
#initialize(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, loggable: true, **options) ⇒ FieldType
constructor
Initialize a new field type.
-
#inspect ⇒ String
(also: #to_s)
Enhanced inspection output for debugging.
-
#install(klass) ⇒ Object
Install this field type on a class.
-
#persistent? ⇒ Boolean
Whether this field should be persisted to the database.
-
#serialize(value, _record = nil) ⇒ Object
Serialize a value for database storage.
- #transient? ⇒ Boolean
Constructor Details
#initialize(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, loggable: true, **options) ⇒ FieldType
Initialize a new field type
49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/familia/field_type.rb', line 49 def initialize(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, loggable: true, **) @name = name.to_sym @method_name = as == false ? nil : as.to_sym @fast_method_name = fast_method == false ? nil : fast_method&.to_sym # Validate fast method name format if @fast_method_name && !@fast_method_name.to_s.end_with?('!') raise ArgumentError, "Fast method name must end with '!' (got: #{@fast_method_name})" end @on_conflict = on_conflict @loggable = loggable @options = end |
Instance Attribute Details
#fast_method_name ⇒ Object (readonly)
Returns the value of attribute fast_method_name.
32 33 34 |
# File 'lib/familia/field_type.rb', line 32 def fast_method_name @fast_method_name end |
#loggable ⇒ Object (readonly)
Returns the value of attribute loggable.
32 33 34 |
# File 'lib/familia/field_type.rb', line 32 def loggable @loggable end |
#method_name ⇒ Object (readonly)
Returns the value of attribute method_name.
32 33 34 |
# File 'lib/familia/field_type.rb', line 32 def method_name @method_name end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
32 33 34 |
# File 'lib/familia/field_type.rb', line 32 def name @name end |
#on_conflict ⇒ Object (readonly)
Returns the value of attribute on_conflict.
32 33 34 |
# File 'lib/familia/field_type.rb', line 32 def on_conflict @on_conflict end |
#options ⇒ Object (readonly)
Returns the value of attribute options.
32 33 34 |
# File 'lib/familia/field_type.rb', line 32 def @options end |
Instance Method Details
#category ⇒ Symbol
The category for this field type (used for filtering)
230 231 232 |
# File 'lib/familia/field_type.rb', line 230 def category :field end |
#define_fast_writer(klass) ⇒ Object
Define the fast writer method on the target class
Fast methods provide direct database access for immediate persistence. Subclasses can override this to customize fast method behavior.
Fields backing a class-level index are maintained fail-closed (#308): outside any transaction/pipeline the unique claim runs before the hash write (a conflict raises Familia::RecordExistsError and the hash is untouched); inside a caller's MULTI/pipeline no claim is possible, so the writer raises Familia::IndexedFieldFastWriteError rather than write the hash and leave the index stale. Should the hash write itself fail after the index was updated, the writer compensates best-effort before re-raising (see #compensate_failed_indexed_fast_write) -- the two commands are not one MULTI. Non-indexed fields are unaffected.
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 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 |
# File 'lib/familia/field_type.rb', line 150 def define_fast_writer(klass) return unless @fast_method_name&.to_s&.end_with?('!') field_name = @name fast_method_name = @fast_method_name field_type = self # Lives in the closure (not an ivar) because field types are frozen # after installation. One entry per runtime class, never pruned: safe # in practice because Horreum subclasses are a small static set for # the life of the process. It would only leak under unbounded dynamic # class creation (e.g. anonymous classes churned in a long-lived # process), where each discarded class leaves an entry that also pins # the class itself. index_rel_cache = {}.compare_by_identity handle_method_conflict(klass, fast_method_name) do klass.define_method fast_method_name do |*args| raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0 or 1)" if args.size > 1 val = args.first # If no value provided, return current stored value # Handle Redis::Future objects during transactions return hget(field_name) if val.nil? || val.is_a?(Redis::Future) index_rels = field_type.send(:class_index_relationships, index_rel_cache, self.class) field_type.send(:guard_indexed_fast_write!, index_rels) # Trace the operation if debugging is enabled Familia.trace :FAST_WRITER, nil, "#{field_name}: #{val.inspect}" if Familia.debug? # Convert value for database storage prepared = serialize_value(val) Familia.debug "[FieldType#define_fast_writer] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}" # Runs the setter, then claims and updates any class-level index # entries before the hash write below (ADR-0002 fail-closed # ordering). A claim conflict restores the in-memory state and # propagates typed, so the hash write never runs. For indexed # fields the rollback snapshot is returned (nil otherwise) so a # failed hash write below can be compensated. snapshot = field_type.send(:apply_fast_write_value, self, val, index_rels) begin # Persist to database immediately, compensating the index # entries above if the write fails (the two are separate # commands, not one MULTI -- see #persist_fast_write_value). ret = field_type.send(:persist_fast_write_value, self, prepared, val, index_rels, snapshot) # Touch instances timeline so the object is visible # to list-based enumeration (instances.to_a, count, etc.) touch_instances! if respond_to?(:touch_instances!) clear_dirty!(field_name) if respond_to?(:clear_dirty!) Familia.success?(ret) rescue Familia::Problem => e raise "#{fast_method_name} method failed: #{e.}", e.backtrace end end end end |
#define_getter(klass) ⇒ Object
Define the getter method on the target class
Subclasses can override this to customize getter behavior. The default implementation creates a simple attr_reader equivalent.
96 97 98 99 100 101 102 103 104 105 |
# File 'lib/familia/field_type.rb', line 96 def define_getter(klass) field_name = @name method_name = @method_name handle_method_conflict(klass, method_name) do klass.define_method method_name do instance_variable_get(:"@#{field_name}") end end end |
#define_setter(klass) ⇒ Object
This setter only updates the in-memory instance variable. Call +save+, +commit_fields+, or use the fast_writer (+field_name!+) to persist to Redis.
Define the setter method on the target class
Subclasses can override this to customize setter behavior. The default implementation creates a simple attr_writer equivalent.
118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/familia/field_type.rb', line 118 def define_setter(klass) field_name = @name method_name = @method_name handle_method_conflict(klass, :"#{method_name}=") do klass.define_method :"#{method_name}=" do |value| old_value = instance_variable_get(:"@#{field_name}") instance_variable_set(:"@#{field_name}", value) # Track the change for dirty-tracking (only for Horreum instances) mark_dirty!(field_name, old_value) if respond_to?(:mark_dirty!) end end end |
#deserialize(value, _record = nil) ⇒ Object
Deserialize a value from database storage
Subclasses can override this to customize deserialization. The default implementation passes values through unchanged.
256 257 258 |
# File 'lib/familia/field_type.rb', line 256 def deserialize(value, _record = nil) value end |
#generated_methods ⇒ Array<Symbol>
Returns all method names generated for this field (used for conflict detection)
264 265 266 |
# File 'lib/familia/field_type.rb', line 264 def generated_methods [@method_name, @fast_method_name].compact end |
#inspect ⇒ String Also known as: to_s
Enhanced inspection output for debugging
272 273 274 275 276 277 278 279 280 281 |
# File 'lib/familia/field_type.rb', line 272 def inspect attributes = [ "name=#{@name}", "method_name=#{@method_name}", "fast_method_name=#{@fast_method_name}", "on_conflict=#{@on_conflict}", "category=#{category}", ] "#<#{self.class.name} #{attributes.join(' ')}>" end |
#install(klass) ⇒ Object
Install this field type on a class
This method defines all necessary methods on the target class and registers the field type for later reference.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/familia/field_type.rb', line 71 def install(klass) if @method_name # For skip strategy, check for any method conflicts first if @on_conflict == :skip has_getter_conflict = klass.method_defined?(@method_name) || klass.private_method_defined?(@method_name) has_setter_conflict = klass.method_defined?(:"#{@method_name}=") || klass.private_method_defined?(:"#{@method_name}=") # If either getter or setter conflicts, skip the whole field return if has_getter_conflict || has_setter_conflict end define_getter(klass) define_setter(klass) end define_fast_writer(klass) if @fast_method_name end |
#persistent? ⇒ Boolean
Whether this field should be persisted to the database
218 219 220 |
# File 'lib/familia/field_type.rb', line 218 def persistent? true end |
#serialize(value, _record = nil) ⇒ Object
Serialize a value for database storage
Subclasses can override this to customize serialization. The default implementation passes values through unchanged.
243 244 245 |
# File 'lib/familia/field_type.rb', line 243 def serialize(value, _record = nil) value end |
#transient? ⇒ Boolean
222 223 224 |
# File 'lib/familia/field_type.rb', line 222 def transient? !persistent? end |