familia 2.0.0.pre4 → 2.0.0.pre6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop_todo.yml +17 -17
- data/CLAUDE.md +11 -8
- data/Gemfile +5 -1
- data/Gemfile.lock +19 -3
- data/README.md +36 -157
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +347 -0
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +600 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +106 -0
- data/docs/wiki/Implementation-Guide.md +276 -0
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/RelatableObjects-Guide.md +563 -0
- data/docs/wiki/Security-Model.md +183 -0
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/lib/familia/base.rb +18 -27
- data/lib/familia/connection.rb +6 -5
- data/lib/familia/{datatype → data_type}/commands.rb +2 -5
- data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +12 -14
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +119 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
- data/lib/familia/encryption/registry.rb +50 -0
- data/lib/familia/encryption.rb +178 -0
- data/lib/familia/encryption_request_cache.rb +68 -0
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
- data/lib/familia/features/encrypted_fields.rb +28 -0
- data/lib/familia/features/expiration.rb +107 -77
- data/lib/familia/features/quantization.rb +5 -9
- data/lib/familia/features/relatable_objects.rb +2 -4
- data/lib/familia/features/safe_dump.rb +14 -17
- data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
- data/lib/familia/features/transient_fields.rb +47 -0
- data/lib/familia/features.rb +40 -24
- data/lib/familia/field_type.rb +273 -0
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
- data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
- data/lib/familia/horreum/subclass/definition.rb +469 -0
- data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +30 -22
- data/lib/familia/logging.rb +14 -14
- data/lib/familia/settings.rb +39 -3
- data/lib/familia/utils.rb +45 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +3 -2
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -5
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +1 -2
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/core/pools_try.rb +2 -2
- data/try/core/secure_identifier_try.rb +0 -1
- data/try/core/settings_try.rb +0 -1
- data/try/core/utils_try.rb +0 -1
- data/try/{datatypes → data_types}/boolean_try.rb +1 -2
- data/try/data_types/counter_try.rb +93 -0
- data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
- data/try/{datatypes → data_types}/hash_try.rb +1 -2
- data/try/{datatypes → data_types}/list_try.rb +1 -2
- data/try/data_types/lock_try.rb +133 -0
- data/try/{datatypes → data_types}/set_try.rb +1 -2
- data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
- data/try/{datatypes → data_types}/string_try.rb +1 -2
- data/try/debugging/README.md +32 -0
- data/try/debugging/cache_behavior_tracer.rb +91 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/debugging/encryption_method_tracer.rb +138 -0
- data/try/debugging/provider_diagnostics.rb +110 -0
- data/try/edge_cases/hash_symbolization_try.rb +0 -1
- data/try/edge_cases/json_serialization_try.rb +0 -1
- data/try/edge_cases/reserved_keywords_try.rb +42 -11
- data/try/encryption/config_persistence_try.rb +192 -0
- data/try/encryption/encryption_core_try.rb +328 -0
- data/try/encryption/instance_variable_scope_try.rb +31 -0
- data/try/encryption/module_loading_try.rb +28 -0
- data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
- data/try/encryption/roundtrip_validation_try.rb +28 -0
- data/try/encryption/secure_memory_handling_try.rb +125 -0
- data/try/features/encrypted_fields_core_try.rb +125 -0
- data/try/features/encrypted_fields_integration_try.rb +216 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
- data/try/features/encrypted_fields_security_try.rb +377 -0
- data/try/features/encryption_fields/aad_protection_try.rb +138 -0
- data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
- data/try/features/encryption_fields/context_isolation_try.rb +141 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
- data/try/features/encryption_fields/fresh_key_try.rb +168 -0
- data/try/features/encryption_fields/key_rotation_try.rb +123 -0
- data/try/features/encryption_fields/memory_security_try.rb +37 -0
- data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -0
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/expiration_try.rb +0 -1
- data/try/features/feature_dependencies_try.rb +159 -0
- data/try/features/quantization_try.rb +0 -1
- data/try/features/real_feature_integration_try.rb +148 -0
- data/try/features/relatable_objects_try.rb +0 -1
- data/try/features/safe_dump_advanced_try.rb +0 -1
- data/try/features/safe_dump_try.rb +0 -1
- data/try/features/transient_fields/redacted_string_try.rb +248 -0
- data/try/features/transient_fields/refresh_reset_try.rb +164 -0
- data/try/features/transient_fields/simple_refresh_test.rb +50 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
- data/try/features/transient_fields_core_try.rb +181 -0
- data/try/features/transient_fields_integration_try.rb +260 -0
- data/try/helpers/test_helpers.rb +67 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
- data/try/horreum/field_categories_try.rb +118 -0
- data/try/horreum/field_definition_try.rb +96 -0
- data/try/horreum/initialization_try.rb +1 -2
- data/try/horreum/relations_try.rb +1 -2
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +41 -7
- data/try/memory/memory_basic_test.rb +73 -0
- data/try/memory/memory_detailed_test.rb +121 -0
- data/try/memory/memory_docker_ruby_dump.sh +80 -0
- data/try/memory/memory_search_for_string.rb +83 -0
- data/try/memory/test_actual_redactedstring_protection.rb +38 -0
- data/try/models/customer_safe_dump_try.rb +1 -2
- data/try/models/customer_try.rb +1 -2
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +131 -23
- data/lib/familia/horreum/serialization.rb +0 -445
@@ -0,0 +1,469 @@
|
|
1
|
+
# lib/familia/horreum/subclass/definition.rb
|
2
|
+
|
3
|
+
require_relative 'related_fields_management'
|
4
|
+
require_relative '../shared/settings'
|
5
|
+
|
6
|
+
module Familia
|
7
|
+
VALID_STRATEGIES = %i[raise skip ignore warn overwrite].freeze
|
8
|
+
|
9
|
+
# Familia::Horreum
|
10
|
+
#
|
11
|
+
class Horreum
|
12
|
+
# Class-level instance variables
|
13
|
+
# These are set up as nil initially and populated later
|
14
|
+
#
|
15
|
+
# Connection and database settings
|
16
|
+
@dbclient = nil
|
17
|
+
@logical_database = nil
|
18
|
+
@uri = nil
|
19
|
+
|
20
|
+
# Database Key generation settings
|
21
|
+
@prefix = nil
|
22
|
+
@identifier_field = nil
|
23
|
+
@suffix = nil
|
24
|
+
|
25
|
+
# Fields and relationships
|
26
|
+
@fields = nil
|
27
|
+
@class_related_fields = nil
|
28
|
+
@related_fields = nil
|
29
|
+
@default_expiration = nil
|
30
|
+
|
31
|
+
# Serialization settings
|
32
|
+
@dump_method = nil
|
33
|
+
@load_method = nil
|
34
|
+
|
35
|
+
# DefinitionMethods: Provides class-level functionality for Horreum subclasses
|
36
|
+
#
|
37
|
+
# This module is extended into classes that include Familia::Horreum,
|
38
|
+
# providing methods for Database operations and object management.
|
39
|
+
#
|
40
|
+
# Key features:
|
41
|
+
# * Includes RelatedFieldsManagement for DataType field handling
|
42
|
+
# * Defines methods for managing fields, identifiers, and dbkeys
|
43
|
+
# * Provides utility methods for working with Database objects
|
44
|
+
#
|
45
|
+
module DefinitionMethods
|
46
|
+
include Familia::Settings
|
47
|
+
include Familia::Horreum::RelatedFieldsManagement # Provides DataType field methods
|
48
|
+
|
49
|
+
# Sets or retrieves the unique identifier field for the class.
|
50
|
+
#
|
51
|
+
# This method defines or returns the field or method that contains the unique
|
52
|
+
# identifier used to generate the dbkey for the object. If a value is provided,
|
53
|
+
# it sets the identifier field; otherwise, it returns the current identifier field.
|
54
|
+
#
|
55
|
+
# @param [Object] val the field name or method to set as the identifier field (optional).
|
56
|
+
# @return [Object] the current identifier field.
|
57
|
+
#
|
58
|
+
def identifier_field(val = nil)
|
59
|
+
if val
|
60
|
+
# Validate identifier field definition at class definition time
|
61
|
+
case val
|
62
|
+
when Symbol, String, Proc
|
63
|
+
@identifier_field = val
|
64
|
+
else
|
65
|
+
raise Problem, <<~ERROR
|
66
|
+
Invalid identifier field definition: #{val.inspect}.
|
67
|
+
Use a field name (Symbol/String) or Proc.
|
68
|
+
ERROR
|
69
|
+
end
|
70
|
+
end
|
71
|
+
@identifier_field
|
72
|
+
end
|
73
|
+
|
74
|
+
# Defines a field for the class and creates accessor methods.
|
75
|
+
#
|
76
|
+
# This method defines a new field for the class, creating getter and setter
|
77
|
+
# instance methods similar to `attr_accessor`. It also generates a fast
|
78
|
+
# writer method for immediate persistence to Redis.
|
79
|
+
#
|
80
|
+
# @param name [Symbol, String] the name of the field to define. If a method
|
81
|
+
# with the same name already exists, an error is raised.
|
82
|
+
# @param as [Symbol, String, false, nil] as the name to use for the accessor method (defaults to name).
|
83
|
+
# If false or nil, no accessor methods are created.
|
84
|
+
# @param fast_method [Symbol, false, nil] the name to use for the fast writer method (defaults to :"#{name}!").
|
85
|
+
# If false or nil, no fast writer method is created.
|
86
|
+
# @param on_conflict [Symbol] conflict resolution strategy when method already exists:
|
87
|
+
# - :raise - raise error if method exists (default)
|
88
|
+
# - :skip - skip definition if method exists
|
89
|
+
# - :warn - warn but proceed (may overwrite)
|
90
|
+
# - :ignore - proceed silently (may overwrite)
|
91
|
+
# @param category [Symbol, nil] field category for special handling:
|
92
|
+
# - nil - regular field (default)
|
93
|
+
# - :encrypted - field contains encrypted data
|
94
|
+
# - :transient - field is not persisted
|
95
|
+
# - Others, depending on features available
|
96
|
+
#
|
97
|
+
def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, category: nil)
|
98
|
+
# Use field type system for consistency
|
99
|
+
require_relative '../../field_type'
|
100
|
+
|
101
|
+
# Create appropriate field type based on category
|
102
|
+
field_type = if category == :transient
|
103
|
+
require_relative '../../features/transient_fields/transient_field_type'
|
104
|
+
TransientFieldType.new(name, as: as, fast_method: false, on_conflict: on_conflict)
|
105
|
+
else
|
106
|
+
# For regular fields and other categories, create custom field type with category override
|
107
|
+
custom_field_type = Class.new(FieldType) do
|
108
|
+
define_method :category do
|
109
|
+
category || :field
|
110
|
+
end
|
111
|
+
end
|
112
|
+
custom_field_type.new(name, as: as, fast_method: fast_method, on_conflict: on_conflict)
|
113
|
+
end
|
114
|
+
|
115
|
+
register_field_type(field_type)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Sets or retrieves the suffix for generating Redis keys.
|
119
|
+
#
|
120
|
+
# @param a [String, Symbol, nil] the suffix to set (optional).
|
121
|
+
# @param blk [Proc] a block that returns the suffix (optional).
|
122
|
+
# @return [String, Symbol] the current suffix or Familia.default_suffix if none is set.
|
123
|
+
#
|
124
|
+
def suffix(a = nil, &blk)
|
125
|
+
@suffix = a || blk if a || !blk.nil?
|
126
|
+
@suffix || Familia.default_suffix
|
127
|
+
end
|
128
|
+
|
129
|
+
# Sets or retrieves the prefix for generating Redis keys.
|
130
|
+
#
|
131
|
+
# @param a [String, Symbol, nil] the prefix to set (optional).
|
132
|
+
# @return [String, Symbol] the current prefix.
|
133
|
+
#
|
134
|
+
# The exception is only raised when both @prefix is nil/falsy AND name is nil,
|
135
|
+
# which typically occurs with anonymous classes that haven't had their prefix
|
136
|
+
# explicitly set.
|
137
|
+
#
|
138
|
+
def prefix(a = nil)
|
139
|
+
@prefix = a if a
|
140
|
+
@prefix || begin
|
141
|
+
if name.nil?
|
142
|
+
raise Problem, 'Cannot generate prefix for anonymous class. ' \
|
143
|
+
'Use `prefix` method to set explicitly.'
|
144
|
+
end
|
145
|
+
name.downcase.gsub('::', Familia.delim).to_sym
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def logical_database(v = nil)
|
150
|
+
Familia.trace :DB, Familia.dbclient, "#{@logical_database} #{v.nil?}", caller(0..2) if Familia.debug?
|
151
|
+
@logical_database = v unless v.nil?
|
152
|
+
@logical_database || parent&.logical_database
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns the list of field names defined for the class in the order
|
156
|
+
# that they were defined. i.e. `field :a; field :b; fields => [:a, :b]`.
|
157
|
+
def fields
|
158
|
+
@fields ||= []
|
159
|
+
@fields
|
160
|
+
end
|
161
|
+
|
162
|
+
def class_related_fields
|
163
|
+
@class_related_fields ||= {}
|
164
|
+
@class_related_fields
|
165
|
+
end
|
166
|
+
|
167
|
+
def related_fields
|
168
|
+
@related_fields ||= {}
|
169
|
+
@related_fields
|
170
|
+
end
|
171
|
+
|
172
|
+
def has_relations?
|
173
|
+
@has_relations ||= false
|
174
|
+
end
|
175
|
+
|
176
|
+
# Converts the class name into a string that can be used to look up
|
177
|
+
# configuration values. This is particularly useful when mapping
|
178
|
+
# familia models with specific database numbers in the configuration.
|
179
|
+
#
|
180
|
+
# @example V2::Session.config_name => 'session'
|
181
|
+
#
|
182
|
+
# @return [String] The underscored class name as a string
|
183
|
+
def config_name
|
184
|
+
name.split('::').last
|
185
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
186
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
187
|
+
.downcase
|
188
|
+
end
|
189
|
+
|
190
|
+
def dump_method
|
191
|
+
@dump_method || :to_json # Familia.dump_method
|
192
|
+
end
|
193
|
+
|
194
|
+
def load_method
|
195
|
+
@load_method || :from_json # Familia.load_method
|
196
|
+
end
|
197
|
+
|
198
|
+
# Storage for field type instances
|
199
|
+
def field_types
|
200
|
+
@field_types ||= {}
|
201
|
+
end
|
202
|
+
|
203
|
+
# Returns a hash mapping field names to method names for backward compatibility
|
204
|
+
def field_method_map
|
205
|
+
field_types.transform_values(&:method_name)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Get fields for serialization (excludes transients)
|
209
|
+
def persistent_fields
|
210
|
+
fields.select do |field|
|
211
|
+
field_types[field]&.persistent?
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Get fields that are not persisted to the database (transients)
|
216
|
+
def transient_fields
|
217
|
+
fields.select do |field|
|
218
|
+
field_types[field]&.transient?
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# Register a field type instance with this class
|
223
|
+
#
|
224
|
+
# This method installs the field type's methods and registers it
|
225
|
+
# for later reference. It maintains backward compatibility by
|
226
|
+
# creating FieldDefinition objects.
|
227
|
+
#
|
228
|
+
# @param field_type [FieldType] The field type to register
|
229
|
+
#
|
230
|
+
def register_field_type(field_type)
|
231
|
+
fields << field_type.name
|
232
|
+
field_type.install(self)
|
233
|
+
# Complete the registration after installation. If we do this beforehand
|
234
|
+
# we can run into issues where it looks like it's already installed.
|
235
|
+
field_types[field_type.name] = field_type
|
236
|
+
end
|
237
|
+
|
238
|
+
# Create and register a transient field type
|
239
|
+
#
|
240
|
+
# @param name [Symbol] The field name
|
241
|
+
# @param options [Hash] Field options
|
242
|
+
#
|
243
|
+
def transient_field(name, **)
|
244
|
+
require_relative '../../features/transient_fields/transient_field_type'
|
245
|
+
field_type = TransientFieldType.new(name, **, fast_method: false)
|
246
|
+
register_field_type(field_type)
|
247
|
+
end
|
248
|
+
|
249
|
+
private
|
250
|
+
|
251
|
+
# Hook to detect silent overwrites and handle conflicts
|
252
|
+
def method_added(method_name)
|
253
|
+
super
|
254
|
+
|
255
|
+
# Find the field type that generated this method
|
256
|
+
field_type = field_types.values.find { |ft| ft.generated_methods.include?(method_name) }
|
257
|
+
return unless field_type
|
258
|
+
|
259
|
+
case field_type.on_conflict
|
260
|
+
when :warn
|
261
|
+
warn <<~WARNING
|
262
|
+
|
263
|
+
WARNING: Method >>> #{method_name} <<< was redefined after field definition.
|
264
|
+
Field functionality may be broken. Consider using a different name
|
265
|
+
with field(:#{field_type.name}, as: :other_name)
|
266
|
+
|
267
|
+
Called from:
|
268
|
+
#{Familia.pretty_stack(limit: 3)}
|
269
|
+
|
270
|
+
WARNING
|
271
|
+
when :raise
|
272
|
+
raise ArgumentError, "Method >>> #{method_name} <<< already defined for #{self}"
|
273
|
+
when :skip, :ignore
|
274
|
+
# Do nothing, skip silently
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def define_attr_accessor_methods(field_name, method_name, on_conflict)
|
279
|
+
handle_method_conflict(method_name, on_conflict) do
|
280
|
+
# Equivalent to `attr_reader :field_name`
|
281
|
+
define_method method_name do
|
282
|
+
instance_variable_get(:"@#{field_name}")
|
283
|
+
end
|
284
|
+
# Equivalent to `attr_writer :field_name=`
|
285
|
+
define_method :"#{method_name}=" do |value|
|
286
|
+
instance_variable_set(:"@#{field_name}", value)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Fast attribute accessor method for immediate DB persistence.
|
292
|
+
#
|
293
|
+
# @param field_name [Symbol, String] the name of the horreum model attribute
|
294
|
+
# @param method_name [Symbol, String] the name of the regular accessor method
|
295
|
+
# @param fast_method_name [Symbol, String] the name of the fast method (must end with '!')
|
296
|
+
# @param on_conflict [Symbol] conflict resolution strategy for method name conflicts
|
297
|
+
#
|
298
|
+
# @return [void]
|
299
|
+
#
|
300
|
+
# @raise [ArgumentError] if fast_method_name doesn't end with '!'
|
301
|
+
#
|
302
|
+
# @note Generated method behavior:
|
303
|
+
# - Without args: Retrieves current value from Redis
|
304
|
+
# - With value: Sets and immediately persists to Redis
|
305
|
+
# - Returns boolean indicating success for writes
|
306
|
+
# - Bypasses object-level caching and expiration updates
|
307
|
+
#
|
308
|
+
# @example
|
309
|
+
# # Creates a method like: username!(value = nil)
|
310
|
+
# define_fast_writer_method(:username, :username, :username!, :raise)
|
311
|
+
#
|
312
|
+
def define_fast_writer_method(field_name, method_name, fast_method_name, on_conflict)
|
313
|
+
raise ArgumentError, 'Must end with !' unless fast_method_name.to_s.end_with?('!')
|
314
|
+
|
315
|
+
handle_method_conflict(fast_method_name, on_conflict) do
|
316
|
+
# Fast attribute accessor method for the '#{field_name}' attribute.
|
317
|
+
# This method provides immediate read and write access to the attribute
|
318
|
+
# in Redis.
|
319
|
+
#
|
320
|
+
# When called without arguments, it retrieves the current value of the
|
321
|
+
# attribute from Redis.
|
322
|
+
# When called with an argument, it immediately persists the new value to
|
323
|
+
# Redis.
|
324
|
+
#
|
325
|
+
# @overload #{method_name}
|
326
|
+
# Retrieves the current value of the attribute from Redis.
|
327
|
+
# @return [Object] the current value of the attribute.
|
328
|
+
#
|
329
|
+
# @overload #{method_name}(value)
|
330
|
+
# Sets and immediately persists the new value of the attribute to
|
331
|
+
# Redis.
|
332
|
+
# @param value [Object] the new value to set for the attribute.
|
333
|
+
# @return [Object] the newly set value.
|
334
|
+
#
|
335
|
+
# @raise [ArgumentError] if more than one argument is provided.
|
336
|
+
# @raise [RuntimeError] if an exception occurs during the execution of
|
337
|
+
# the method.
|
338
|
+
#
|
339
|
+
# @note This method bypasses any object-level caching and interacts
|
340
|
+
# directly with Redis. It does not trigger updates to other attributes
|
341
|
+
# or the object's expiration time.
|
342
|
+
#
|
343
|
+
# @example
|
344
|
+
#
|
345
|
+
# def field_name!(*args)
|
346
|
+
# # Method implementation
|
347
|
+
# end
|
348
|
+
#
|
349
|
+
define_method fast_method_name do |*args|
|
350
|
+
# Check if the correct number of arguments is provided (exactly one).
|
351
|
+
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0 or 1)" if args.size > 1
|
352
|
+
|
353
|
+
val = args.first
|
354
|
+
|
355
|
+
# If no value is provided to this fast attribute method, make a call
|
356
|
+
# to the db to return the current stored value of the hash field.
|
357
|
+
return hget field_name if val.nil?
|
358
|
+
|
359
|
+
begin
|
360
|
+
# Trace the operation if debugging is enabled.
|
361
|
+
Familia.trace :FAST_WRITER, dbclient, "#{field_name}: #{val.inspect}", caller(1..1) if Familia.debug?
|
362
|
+
|
363
|
+
# Convert the provided value to a format suitable for Database storage.
|
364
|
+
prepared = serialize_value(val)
|
365
|
+
Familia.ld "[.define_fast_writer_method] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
|
366
|
+
|
367
|
+
# Use the existing accessor method to set the attribute value.
|
368
|
+
send :"#{method_name}=", val
|
369
|
+
|
370
|
+
# Persist the value to Database immediately using the hset command.
|
371
|
+
ret = hset field_name, prepared
|
372
|
+
ret.zero? || ret.positive?
|
373
|
+
rescue Familia::Problem => e
|
374
|
+
# Raise a custom error message if an exception occurs during the execution of the method.
|
375
|
+
raise "#{fast_method_name} method failed: #{e.message}", e.backtrace
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
# Handles method name conflicts during dynamic method definition.
|
382
|
+
#
|
383
|
+
# @param method_name [Symbol, String] the method to define
|
384
|
+
# @param strategy [Symbol] conflict resolution strategy:
|
385
|
+
# - :raise - raise error if method exists (default)
|
386
|
+
# - :skip - skip definition if method exists
|
387
|
+
# - :warn - warn but proceed (may overwrite)
|
388
|
+
# - :overwrite - explicitly remove existing method first
|
389
|
+
#
|
390
|
+
# @yield the method definition to execute
|
391
|
+
#
|
392
|
+
# @example
|
393
|
+
# handle_method_conflict(:my_method, :skip) do
|
394
|
+
# attr_accessor :my_method
|
395
|
+
# end
|
396
|
+
#
|
397
|
+
# @raise [ArgumentError] if strategy invalid or method exists with :raise
|
398
|
+
#
|
399
|
+
# @private
|
400
|
+
def handle_method_conflict(method_name, strategy, &)
|
401
|
+
validate_strategy!(strategy)
|
402
|
+
|
403
|
+
if method_exists?(method_name)
|
404
|
+
handle_existing_method(method_name, strategy, &)
|
405
|
+
else
|
406
|
+
yield
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
def validate_strategy!(strategy)
|
411
|
+
return if VALID_STRATEGIES.include?(strategy)
|
412
|
+
|
413
|
+
raise ArgumentError, "Invalid conflict strategy: #{strategy}. " \
|
414
|
+
"Valid strategies: #{VALID_STRATEGIES.join(', ')}"
|
415
|
+
end
|
416
|
+
|
417
|
+
def method_exists?(method_name)
|
418
|
+
method_defined?(method_name)
|
419
|
+
end
|
420
|
+
|
421
|
+
def handle_existing_method(method_name, strategy)
|
422
|
+
case strategy
|
423
|
+
when :raise
|
424
|
+
raise_method_exists_error(method_name)
|
425
|
+
when :skip
|
426
|
+
# Do nothing - skip the definition
|
427
|
+
when :warn
|
428
|
+
warn_method_exists(method_name)
|
429
|
+
yield
|
430
|
+
when :overwrite
|
431
|
+
remove_method(method_name)
|
432
|
+
yield
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
def raise_method_exists_error(method_name)
|
437
|
+
location = format_method_location(method_name)
|
438
|
+
raise ArgumentError, "Method >>> #{method_name} <<< already defined for #{self}#{location}"
|
439
|
+
end
|
440
|
+
|
441
|
+
def warn_method_exists(method_name)
|
442
|
+
location = format_method_location(method_name)
|
443
|
+
caller_info = Familia.pretty_stack(skip: 5, limit: 3)
|
444
|
+
|
445
|
+
warn <<~WARNING
|
446
|
+
|
447
|
+
WARNING: Method '#{method_name}' is already defined.
|
448
|
+
|
449
|
+
Class: #{self}#{location}
|
450
|
+
|
451
|
+
Called from:
|
452
|
+
#{caller_info}
|
453
|
+
|
454
|
+
WARNING
|
455
|
+
end
|
456
|
+
|
457
|
+
def format_method_location(method_name)
|
458
|
+
method_obj = instance_method(method_name)
|
459
|
+
source_location = method_obj.source_location
|
460
|
+
|
461
|
+
return '' unless source_location
|
462
|
+
|
463
|
+
path = Familia.pretty_path(source_location[0])
|
464
|
+
line = source_location[1]
|
465
|
+
" (defined at #{path}:#{line})"
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|