familia 2.0.0.pre3 → 2.0.0.pre5
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 +3 -3
- data/Gemfile +5 -1
- data/Gemfile.lock +18 -3
- data/README.md +36 -157
- data/TEST_COVERAGE.md +40 -0
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +270 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
- data/docs/wiki/Home.md +49 -0
- data/docs/wiki/Implementation-Guide.md +183 -0
- data/docs/wiki/Security-Model.md +143 -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/{datatype → data_type}/types/hashkey.rb +2 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +10 -12
- data/lib/familia/encryption/manager.rb +102 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -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/features/encrypted_fields/encrypted_field_type.rb +153 -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 +270 -0
- data/lib/familia/horreum/connection.rb +8 -11
- data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
- data/lib/familia/horreum/definition_methods.rb +453 -0
- data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
- data/lib/familia/horreum/serialization.rb +46 -18
- data/lib/familia/horreum/settings.rb +10 -2
- data/lib/familia/horreum/utils.rb +9 -10
- data/lib/familia/horreum.rb +18 -10
- 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 +2 -1
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -1
- data/try/core/errors_try.rb +0 -1
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +0 -1
- 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/{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/{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/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 +117 -0
- data/try/features/encrypted_fields_integration_try.rb +220 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
- data/try/features/encrypted_fields_security_try.rb +370 -0
- data/try/features/encryption_fields/aad_protection_try.rb +53 -0
- data/try/features/encryption_fields/context_isolation_try.rb +120 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
- data/try/features/encryption_fields/fresh_key_try.rb +163 -0
- data/try/features/encryption_fields/key_rotation_try.rb +117 -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 +54 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -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 +42 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/class_methods_try.rb +27 -36
- 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 +0 -1
- data/try/horreum/relations_try.rb +0 -1
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +2 -3
- 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 +0 -1
- data/try/models/customer_try.rb +0 -1
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +85 -18
@@ -1,242 +1,22 @@
|
|
1
|
-
# lib/familia/horreum/
|
1
|
+
# lib/familia/horreum/management_methods.rb
|
2
2
|
|
3
3
|
require_relative 'related_fields_management'
|
4
4
|
|
5
5
|
module Familia
|
6
6
|
class Horreum
|
7
|
-
#
|
8
|
-
#
|
9
|
-
@dbclient = nil # TODO
|
10
|
-
@identifier_field = nil
|
11
|
-
@default_expiration = nil
|
12
|
-
@logical_database = nil
|
13
|
-
@uri = nil
|
14
|
-
@suffix = nil
|
15
|
-
@prefix = nil
|
16
|
-
@fields = nil # []
|
17
|
-
@class_related_fields = nil # {}
|
18
|
-
@related_fields = nil # {}
|
19
|
-
@dump_method = nil
|
20
|
-
@load_method = nil
|
21
|
-
|
22
|
-
# ClassMethods: Provides class-level functionality for Horreum
|
7
|
+
# ManagementMethods: Provides class-level functionality for Horreum
|
8
|
+
# records.
|
23
9
|
#
|
24
10
|
# This module is extended into classes that include Familia::Horreum,
|
25
11
|
# providing methods for Database operations and object management.
|
26
12
|
#
|
27
|
-
# Key features:
|
13
|
+
# # Key features:
|
28
14
|
# * Includes RelatedFieldsManagement for DataType field handling
|
29
|
-
# * Defines methods for managing fields, identifiers, and dbkeys
|
30
15
|
# * Provides utility methods for working with Database objects
|
31
16
|
#
|
32
|
-
module
|
33
|
-
include Familia::Settings
|
17
|
+
module ManagementMethods
|
34
18
|
include Familia::Horreum::RelatedFieldsManagement
|
35
19
|
|
36
|
-
# Sets or retrieves the unique identifier field for the class.
|
37
|
-
#
|
38
|
-
# This method defines or returns the field or method that contains the unique
|
39
|
-
# identifier used to generate the dbkey for the object. If a value is provided,
|
40
|
-
# it sets the identifier field; otherwise, it returns the current identifier field.
|
41
|
-
#
|
42
|
-
# @param [Object] val the field name or method to set as the identifier field (optional).
|
43
|
-
# @return [Object] the current identifier field.
|
44
|
-
#
|
45
|
-
def identifier_field(val = nil)
|
46
|
-
if val
|
47
|
-
# Validate identifier field definition at class definition time
|
48
|
-
case val
|
49
|
-
when Symbol, String, Proc
|
50
|
-
@identifier_field = val
|
51
|
-
else
|
52
|
-
raise Problem, <<~ERROR
|
53
|
-
Invalid identifier field definition: #{val.inspect}.
|
54
|
-
Use a field name (Symbol/String) or Proc.
|
55
|
-
ERROR
|
56
|
-
end
|
57
|
-
end
|
58
|
-
@identifier_field
|
59
|
-
end
|
60
|
-
|
61
|
-
# Defines a field for the class and creates accessor methods.
|
62
|
-
#
|
63
|
-
# This method defines a new field for the class, creating getter and setter
|
64
|
-
# instance methods similar to `attr_accessor`. It also generates a fast
|
65
|
-
# writer method for immediate persistence to Redis.
|
66
|
-
#
|
67
|
-
# @param [Symbol, String] name the name of the field to define.
|
68
|
-
#
|
69
|
-
def field(name)
|
70
|
-
fields << name
|
71
|
-
attr_accessor name
|
72
|
-
|
73
|
-
# Every field gets a fast attribute method for immediately persisting
|
74
|
-
fast_attribute! name
|
75
|
-
end
|
76
|
-
|
77
|
-
# Defines a fast attribute method with a bang (!) suffix for a given
|
78
|
-
# attribute name. Fast attribute methods are used to immediately read or
|
79
|
-
# write attribute values from/to Redis. Calling a fast attribute method
|
80
|
-
# has no effect on any of the object's other attributes and does not
|
81
|
-
# trigger a call to update the object's expiration time.
|
82
|
-
#
|
83
|
-
# The dynamically defined method performs the following:
|
84
|
-
# - Acts as both a reader and a writer method.
|
85
|
-
# - When called without arguments, retrieves the current value from Redis.
|
86
|
-
# - When called with an argument, persists the value to Database immediately.
|
87
|
-
# - Checks if the correct number of arguments is provided (zero or one).
|
88
|
-
# - Converts the provided value to a format suitable for Database storage.
|
89
|
-
# - Uses the existing accessor method to set the attribute value when
|
90
|
-
# writing.
|
91
|
-
# - Persists the value to Database immediately using the hset command when
|
92
|
-
# writing.
|
93
|
-
# - Includes custom error handling to raise an ArgumentError if the wrong
|
94
|
-
# number of arguments is given.
|
95
|
-
# - Raises a custom error message if an exception occurs during the
|
96
|
-
# execution of the method.
|
97
|
-
#
|
98
|
-
# @param [Symbol, String] name the name of the attribute for which the
|
99
|
-
# fast method is defined.
|
100
|
-
# @return [Object] the current value of the attribute when called without
|
101
|
-
# arguments.
|
102
|
-
# @raise [ArgumentError] if more than one argument is provided.
|
103
|
-
# @raise [RuntimeError] if an exception occurs during the execution of the
|
104
|
-
# method.
|
105
|
-
#
|
106
|
-
def fast_attribute!(name = nil)
|
107
|
-
# Fast attribute accessor method for the '#{name}' attribute.
|
108
|
-
# This method provides immediate read and write access to the attribute
|
109
|
-
# in Redis.
|
110
|
-
#
|
111
|
-
# When called without arguments, it retrieves the current value of the
|
112
|
-
# attribute from Redis.
|
113
|
-
# When called with an argument, it immediately persists the new value to
|
114
|
-
# Redis.
|
115
|
-
#
|
116
|
-
# @overload #{name}!
|
117
|
-
# Retrieves the current value of the attribute from Redis.
|
118
|
-
# @return [Object] the current value of the attribute.
|
119
|
-
#
|
120
|
-
# @overload #{name}!(value)
|
121
|
-
# Sets and immediately persists the new value of the attribute to
|
122
|
-
# Redis.
|
123
|
-
# @param value [Object] the new value to set for the attribute.
|
124
|
-
# @return [Object] the newly set value.
|
125
|
-
#
|
126
|
-
# @raise [ArgumentError] if more than one argument is provided.
|
127
|
-
# @raise [RuntimeError] if an exception occurs during the execution of
|
128
|
-
# the method.
|
129
|
-
#
|
130
|
-
# @note This method bypasses any object-level caching and interacts
|
131
|
-
# directly with Redis. It does not trigger updates to other attributes
|
132
|
-
# or the object's expiration time.
|
133
|
-
#
|
134
|
-
# @example
|
135
|
-
#
|
136
|
-
# def #{name}!(*args)
|
137
|
-
# # Method implementation
|
138
|
-
# end
|
139
|
-
#
|
140
|
-
define_method :"#{name}!" do |*args|
|
141
|
-
# Check if the correct number of arguments is provided (exactly one).
|
142
|
-
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0 or 1)" if args.size > 1
|
143
|
-
|
144
|
-
val = args.first
|
145
|
-
|
146
|
-
# If no value is provided to this fast attribute method, make a call
|
147
|
-
# to the db to return the current stored value of the hash field.
|
148
|
-
return hget name if val.nil?
|
149
|
-
|
150
|
-
begin
|
151
|
-
# Trace the operation if debugging is enabled.
|
152
|
-
Familia.trace :FAST_WRITER, dbclient, "#{name}: #{val.inspect}", caller(1..1) if Familia.debug?
|
153
|
-
|
154
|
-
# Convert the provided value to a format suitable for Database storage.
|
155
|
-
prepared = serialize_value(val)
|
156
|
-
Familia.ld "[.fast_attribute!] #{name} val: #{val.class} prepared: #{prepared.class}"
|
157
|
-
|
158
|
-
# Use the existing accessor method to set the attribute value.
|
159
|
-
send :"#{name}=", val
|
160
|
-
|
161
|
-
# Persist the value to Database immediately using the hset command.
|
162
|
-
hset name, prepared
|
163
|
-
rescue Familia::Problem => e
|
164
|
-
# Raise a custom error message if an exception occurs during the execution of the method.
|
165
|
-
raise "#{name}! method failed: #{e.message}", e.backtrace
|
166
|
-
end
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
# Returns the list of field names defined for the class in the order
|
171
|
-
# that they were defined. i.e. `field :a; field :b; fields => [:a, :b]`.
|
172
|
-
def fields
|
173
|
-
@fields ||= []
|
174
|
-
@fields
|
175
|
-
end
|
176
|
-
|
177
|
-
def class_related_fields
|
178
|
-
@class_related_fields ||= {}
|
179
|
-
@class_related_fields
|
180
|
-
end
|
181
|
-
|
182
|
-
def related_fields
|
183
|
-
@related_fields ||= {}
|
184
|
-
@related_fields
|
185
|
-
end
|
186
|
-
|
187
|
-
def has_relations?
|
188
|
-
@has_relations ||= false
|
189
|
-
end
|
190
|
-
|
191
|
-
def logical_database(v = nil)
|
192
|
-
Familia.trace :DB, Familia.dbclient, "#{@logical_database} #{v}", caller(1..1) if Familia.debug?
|
193
|
-
@logical_database = v unless v.nil?
|
194
|
-
@logical_database || parent&.logical_database
|
195
|
-
end
|
196
|
-
|
197
|
-
def all(suffix = nil)
|
198
|
-
suffix ||= self.suffix
|
199
|
-
# objects that could not be parsed will be nil
|
200
|
-
keys(suffix).filter_map { |k| find_by_key(k) }
|
201
|
-
end
|
202
|
-
|
203
|
-
def any?(filter = '*')
|
204
|
-
matching_keys_count(filter) > 0
|
205
|
-
end
|
206
|
-
|
207
|
-
# Returns the number of dbkeys matching the given filter pattern
|
208
|
-
# @param filter [String] dbkey pattern to match (default: '*')
|
209
|
-
# @return [Integer] Number of matching keys
|
210
|
-
def matching_keys_count(filter = '*')
|
211
|
-
dbclient.keys(dbkey(filter)).compact.size
|
212
|
-
end
|
213
|
-
alias size matching_keys_count # For backwards compatibility
|
214
|
-
|
215
|
-
def suffix(a = nil, &blk)
|
216
|
-
@suffix = a || blk if a || !blk.nil?
|
217
|
-
@suffix || Familia.default_suffix
|
218
|
-
end
|
219
|
-
|
220
|
-
# Sets or retrieves the prefix for generating Redis keys.
|
221
|
-
#
|
222
|
-
# @param a [String, Symbol, nil] the prefix to set (optional).
|
223
|
-
# @return [String, Symbol] the current prefix.
|
224
|
-
#
|
225
|
-
# The exception is only raised when both @prefix is nil/falsy AND name is nil,
|
226
|
-
# which typically occurs with anonymous classes that haven't had their prefix
|
227
|
-
# explicitly set.
|
228
|
-
#
|
229
|
-
def prefix(a = nil)
|
230
|
-
@prefix = a if a
|
231
|
-
@prefix || begin
|
232
|
-
if name.nil?
|
233
|
-
raise Problem, 'Cannot generate prefix for anonymous class. ' \
|
234
|
-
'Use `prefix` method to set explicitly.'
|
235
|
-
end
|
236
|
-
name.downcase.gsub('::', Familia.delim).to_sym
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
20
|
# Creates and persists a new instance of the class.
|
241
21
|
#
|
242
22
|
# @param *args [Array] Variable number of positional arguments to be passed
|
@@ -462,13 +242,23 @@ module Familia
|
|
462
242
|
Familia.dbkey(prefix, identifier, suffix)
|
463
243
|
end
|
464
244
|
|
465
|
-
def
|
466
|
-
|
245
|
+
def all(suffix = nil)
|
246
|
+
suffix ||= self.suffix
|
247
|
+
# objects that could not be parsed will be nil
|
248
|
+
keys(suffix).filter_map { |k| find_by_key(k) }
|
467
249
|
end
|
468
250
|
|
469
|
-
def
|
470
|
-
|
251
|
+
def any?(filter = '*')
|
252
|
+
matching_keys_count(filter) > 0
|
471
253
|
end
|
254
|
+
|
255
|
+
# Returns the number of dbkeys matching the given filter pattern
|
256
|
+
# @param filter [String] dbkey pattern to match (default: '*')
|
257
|
+
# @return [Integer] Number of matching keys
|
258
|
+
def matching_keys_count(filter = '*')
|
259
|
+
dbclient.keys(dbkey(filter)).compact.size
|
260
|
+
end
|
261
|
+
alias size matching_keys_count # For backwards compatibility
|
472
262
|
end
|
473
263
|
end
|
474
264
|
end
|
@@ -1,8 +1,6 @@
|
|
1
1
|
# lib/familia/horreum/serialization.rb
|
2
2
|
#
|
3
3
|
module Familia
|
4
|
-
|
5
|
-
|
6
4
|
# Familia::Horreum
|
7
5
|
#
|
8
6
|
class Horreum
|
@@ -26,7 +24,7 @@ module Familia
|
|
26
24
|
#
|
27
25
|
# May your Database returns be ever valid, and your data ever flowing!
|
28
26
|
#
|
29
|
-
@valid_command_return_values = [
|
27
|
+
@valid_command_return_values = ['OK', true, 1, 0, nil].freeze
|
30
28
|
|
31
29
|
class << self
|
32
30
|
attr_reader :valid_command_return_values
|
@@ -59,7 +57,6 @@ module Familia
|
|
59
57
|
# the Ruby roses, but watch out for the Database thorns!)
|
60
58
|
#
|
61
59
|
module Serialization
|
62
|
-
|
63
60
|
# Save our precious data to Redis, with a sprinkle of timestamp magic!
|
64
61
|
#
|
65
62
|
# This method is like a conscientious historian, not only recording your
|
@@ -76,7 +73,7 @@ module Familia
|
|
76
73
|
# @note This method will leave breadcrumbs (traces) if you're in debug mode.
|
77
74
|
# It's like Hansel and Gretel, but for data operations!
|
78
75
|
#
|
79
|
-
def save
|
76
|
+
def save(update_expiration: true)
|
80
77
|
Familia.trace :SAVE, dbclient, uri, caller(1..1) if Familia.debug?
|
81
78
|
|
82
79
|
# No longer need to sync computed identifier with a cache field
|
@@ -198,11 +195,11 @@ module Familia
|
|
198
195
|
# @note The expiration update is only performed for classes that have
|
199
196
|
# the expiration feature enabled. For others, it's a no-op.
|
200
197
|
#
|
201
|
-
def commit_fields
|
198
|
+
def commit_fields(update_expiration: true)
|
202
199
|
prepared_value = to_h
|
203
200
|
Familia.ld "[commit_fields] Begin #{self.class} #{dbkey} #{prepared_value} (exp: #{update_expiration})"
|
204
201
|
|
205
|
-
result =
|
202
|
+
result = hmset(prepared_value)
|
206
203
|
|
207
204
|
# Only classes that have the expiration ferature enabled will
|
208
205
|
# actually set an expiration time on their keys. Otherwise
|
@@ -258,7 +255,7 @@ module Familia
|
|
258
255
|
# # => All fields are now nil, like a spell gone slightly too well.
|
259
256
|
#
|
260
257
|
def clear_fields!
|
261
|
-
self.class.
|
258
|
+
self.class.field_method_map.each_value { |method_name| send("#{method_name}=", nil) }
|
262
259
|
end
|
263
260
|
|
264
261
|
# The Great Database Refresh-o-matic 3000
|
@@ -284,8 +281,15 @@ module Familia
|
|
284
281
|
def refresh!
|
285
282
|
Familia.trace :REFRESH, dbclient, uri, caller(1..1) if Familia.debug?
|
286
283
|
raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
|
284
|
+
|
287
285
|
fields = hgetall
|
288
286
|
Familia.ld "[refresh!] #{self.class} #{dbkey} fields:#{fields.keys}"
|
287
|
+
|
288
|
+
# Reset transient fields to nil for semantic clarity and ORM consistency
|
289
|
+
# Transient fields have no authoritative source, so they should return to
|
290
|
+
# their uninitialized state during refresh operations
|
291
|
+
reset_transient_fields!
|
292
|
+
|
289
293
|
optimistic_refresh(**fields)
|
290
294
|
end
|
291
295
|
|
@@ -326,14 +330,15 @@ module Familia
|
|
326
330
|
# @note Watch in awe as each field is lovingly prepared for its Database adventure!
|
327
331
|
#
|
328
332
|
def to_h
|
329
|
-
self.class.
|
330
|
-
|
333
|
+
self.class.persistent_fields.each_with_object({}) do |field, hsh|
|
334
|
+
field_type = self.class.field_types[field]
|
335
|
+
method_name = field_type.method_name
|
336
|
+
val = send(method_name)
|
331
337
|
prepared = serialize_value(val)
|
332
338
|
Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
|
333
339
|
|
334
340
|
# Only include non-nil values in the hash for Redis
|
335
341
|
hsh[field] = prepared unless prepared.nil?
|
336
|
-
hsh
|
337
342
|
end
|
338
343
|
end
|
339
344
|
|
@@ -353,10 +358,12 @@ module Familia
|
|
353
358
|
# before joining the parade.
|
354
359
|
#
|
355
360
|
def to_a
|
356
|
-
self.class.
|
357
|
-
|
361
|
+
self.class.persistent_fields.collect do |field|
|
362
|
+
field_type = self.class.field_types[field]
|
363
|
+
method_name = field_type.method_name
|
364
|
+
val = send(method_name)
|
358
365
|
prepared = serialize_value(val)
|
359
|
-
Familia.ld " [to_a] field: #{field} val: #{val.class} prepared: #{prepared.class}"
|
366
|
+
Familia.ld " [to_a] field: #{field} method: #{method_name} val: #{val.class} prepared: #{prepared.class}"
|
360
367
|
prepared
|
361
368
|
end
|
362
369
|
end
|
@@ -406,9 +413,7 @@ module Familia
|
|
406
413
|
end
|
407
414
|
|
408
415
|
# If both the distinguisher and dump_method return nil, log an error
|
409
|
-
if prepared.nil?
|
410
|
-
Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}"
|
411
|
-
end
|
416
|
+
Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}" if prepared.nil?
|
412
417
|
|
413
418
|
prepared
|
414
419
|
end
|
@@ -423,7 +428,7 @@ module Familia
|
|
423
428
|
# @return [Object] The deserialized value (Hash, Array, or original string)
|
424
429
|
#
|
425
430
|
def deserialize_value(val, symbolize: true)
|
426
|
-
return val if val.nil? || val ==
|
431
|
+
return val if val.nil? || val == ''
|
427
432
|
|
428
433
|
# Try to parse as JSON first for complex types
|
429
434
|
begin
|
@@ -438,6 +443,29 @@ module Familia
|
|
438
443
|
val
|
439
444
|
end
|
440
445
|
|
446
|
+
private
|
447
|
+
|
448
|
+
# Reset all transient fields to nil
|
449
|
+
#
|
450
|
+
# This method ensures that transient fields return to their uninitialized
|
451
|
+
# state during refresh operations. This provides semantic clarity (refresh
|
452
|
+
# means "reload from authoritative source"), ORM consistency with other
|
453
|
+
# frameworks, and prevents stale transient data accumulation.
|
454
|
+
#
|
455
|
+
# @return [void]
|
456
|
+
#
|
457
|
+
def reset_transient_fields!
|
458
|
+
return unless self.class.respond_to?(:transient_fields)
|
459
|
+
|
460
|
+
self.class.transient_fields.each do |field_name|
|
461
|
+
field_type = self.class.field_types[field_name]
|
462
|
+
next unless field_type&.method_name
|
463
|
+
|
464
|
+
# Set the transient field back to nil
|
465
|
+
send("#{field_type.method_name}=", nil)
|
466
|
+
Familia.ld "[reset_transient_fields!] Reset #{field_name} to nil"
|
467
|
+
end
|
468
|
+
end
|
441
469
|
end
|
442
470
|
|
443
471
|
include Serialization # these become Horreum instance methods
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# lib/familia/horreum/settings.rb
|
2
|
-
|
2
|
+
|
3
3
|
module Familia
|
4
4
|
# InstanceMethods - Module containing instance-level methods for Familia
|
5
5
|
#
|
@@ -7,7 +7,6 @@ module Familia
|
|
7
7
|
# instance-level functionality for Database operations and object management.
|
8
8
|
#
|
9
9
|
class Horreum
|
10
|
-
|
11
10
|
# Settings - Module containing settings for Familia::Horreum (InstanceMethods)
|
12
11
|
#
|
13
12
|
module Settings
|
@@ -26,6 +25,15 @@ module Familia
|
|
26
25
|
@logical_database || self.class.logical_database
|
27
26
|
end
|
28
27
|
|
28
|
+
# Retrieves the prefix for the current instance by delegating to its class.
|
29
|
+
#
|
30
|
+
# @return [String] The prefix associated with the class of the current instance.
|
31
|
+
# @example
|
32
|
+
# instance.prefix
|
33
|
+
def prefix
|
34
|
+
self.class.prefix
|
35
|
+
end
|
36
|
+
|
29
37
|
def suffix
|
30
38
|
@suffix || self.class.suffix
|
31
39
|
end
|
@@ -7,17 +7,16 @@ module Familia
|
|
7
7
|
# instance-level functionality for Database operations and object management.
|
8
8
|
#
|
9
9
|
class Horreum
|
10
|
-
|
11
10
|
# Utils - Module containing utility methods for Familia::Horreum (InstanceMethods)
|
12
11
|
#
|
13
12
|
module Utils
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
13
|
+
# def uri
|
14
|
+
# base_uri = self.class.uri || Familia.uri
|
15
|
+
# u = base_uri.dup # make a copy to modify safely
|
16
|
+
# u.logical_database = logical_database if logical_database
|
17
|
+
# u.key = dbkey
|
18
|
+
# u
|
19
|
+
# end
|
21
20
|
|
22
21
|
# +suffix+ is the value to be used at the end of the db key
|
23
22
|
# (e.g. `customer:customer_id:scores` would have `scores` as the suffix
|
@@ -27,8 +26,9 @@ module Familia
|
|
27
26
|
# Whether this is a Horreum or DataType object, the value is taken
|
28
27
|
# from the `identifier` method).
|
29
28
|
#
|
30
|
-
def dbkey(suffix = nil,
|
29
|
+
def dbkey(suffix = nil, _ignored = nil)
|
31
30
|
raise Familia::NoIdentifier, "No identifier for #{self.class}" if identifier.to_s.empty?
|
31
|
+
|
32
32
|
suffix ||= self.suffix # use the instance method to get the default suffix
|
33
33
|
self.class.dbkey identifier, suffix
|
34
34
|
end
|
@@ -36,7 +36,6 @@ module Familia
|
|
36
36
|
def join(*args)
|
37
37
|
Familia.join(args.map { |field| send(field) })
|
38
38
|
end
|
39
|
-
|
40
39
|
end
|
41
40
|
|
42
41
|
include Utils # these become Horreum instance methods
|
data/lib/familia/horreum.rb
CHANGED
@@ -62,7 +62,8 @@ module Familia
|
|
62
62
|
# Extends ClassMethods to subclasses and tracks Familia members
|
63
63
|
def inherited(member)
|
64
64
|
Familia.trace :HORREUM, nil, "Welcome #{member} to the family", caller(1..1) if Familia.debug?
|
65
|
-
member.extend(
|
65
|
+
member.extend(DefinitionMethods)
|
66
|
+
member.extend(ManagementMethods)
|
66
67
|
member.extend(Connection)
|
67
68
|
member.extend(Features)
|
68
69
|
|
@@ -133,7 +134,11 @@ module Familia
|
|
133
134
|
# Implementing classes can define an init method to do any
|
134
135
|
# additional initialization. Notice that this is called
|
135
136
|
# after the fields are set.
|
136
|
-
init
|
137
|
+
init
|
138
|
+
end
|
139
|
+
|
140
|
+
def init(*args, **kwargs)
|
141
|
+
# Default no-op
|
137
142
|
end
|
138
143
|
|
139
144
|
# Sets up related Database objects for the instance
|
@@ -150,9 +155,9 @@ module Familia
|
|
150
155
|
# familia_object.dbkey == v1:bone:INDEXVALUE:object
|
151
156
|
# familia_object.related_object.dbkey == v1:bone:INDEXVALUE:name
|
152
157
|
#
|
153
|
-
self.class.related_fields.each_pair do |name,
|
154
|
-
klass =
|
155
|
-
opts =
|
158
|
+
self.class.related_fields.each_pair do |name, data_type_definition|
|
159
|
+
klass = data_type_definition.klass
|
160
|
+
opts = data_type_definition.opts
|
156
161
|
Familia.ld "[#{self.class}] initialize_relatives #{name} => #{klass} #{opts.keys}"
|
157
162
|
|
158
163
|
# As a subclass of Familia::Horreum, we add ourselves as the parent
|
@@ -215,7 +220,9 @@ module Familia
|
|
215
220
|
# we use symbols. So we check for both.
|
216
221
|
value = fields[field.to_sym] || fields[field.to_s]
|
217
222
|
if value
|
218
|
-
|
223
|
+
# Use the mapped method name, not the field name
|
224
|
+
method_name = self.class.field_method_map[field] || field
|
225
|
+
send(:"#{method_name}=", value)
|
219
226
|
field.to_sym
|
220
227
|
end
|
221
228
|
end
|
@@ -283,9 +290,8 @@ module Familia
|
|
283
290
|
# # => #<Redis client v5.4.1 for redis://localhost:6379/0>
|
284
291
|
#
|
285
292
|
def dbclient
|
286
|
-
|
293
|
+
Fiber[:familia_transaction] || @dbclient || self.class.dbclient
|
287
294
|
# conn.select(self.class.logical_database)
|
288
|
-
conn
|
289
295
|
end
|
290
296
|
|
291
297
|
def generate_id
|
@@ -299,13 +305,15 @@ module Familia
|
|
299
305
|
# This allows passing Familia objects directly where strings are expected
|
300
306
|
# without requiring explicit .identifier calls
|
301
307
|
return super if identifier.to_s.empty?
|
308
|
+
|
302
309
|
identifier.to_s
|
303
310
|
end
|
304
311
|
end
|
305
312
|
end
|
306
313
|
|
307
|
-
require_relative 'horreum/
|
308
|
-
require_relative 'horreum/
|
314
|
+
require_relative 'horreum/definition_methods'
|
315
|
+
require_relative 'horreum/management_methods'
|
316
|
+
require_relative 'horreum/database_commands'
|
309
317
|
require_relative 'horreum/connection'
|
310
318
|
require_relative 'horreum/serialization'
|
311
319
|
require_relative 'horreum/settings'
|
data/lib/familia/logging.rb
CHANGED
@@ -6,14 +6,14 @@ require 'logger'
|
|
6
6
|
module Familia
|
7
7
|
@logger = Logger.new($stdout)
|
8
8
|
@logger.progname = name
|
9
|
-
@logger.formatter = proc do |severity, datetime,
|
10
|
-
severity_letter = severity[0]
|
9
|
+
@logger.formatter = proc do |severity, datetime, _progname, msg|
|
10
|
+
severity_letter = severity[0] # Get the first letter of the severity
|
11
11
|
pid = Process.pid
|
12
12
|
thread_id = Thread.current.object_id
|
13
|
-
full_path, line = caller
|
13
|
+
full_path, line = caller(5..5).first.split(':')[0..1]
|
14
14
|
parent_path = Pathname.new(full_path).ascend.find { |p| p.basename.to_s == 'familia' }
|
15
15
|
relative_path = full_path.sub(parent_path.to_s, 'familia')
|
16
|
-
utc_datetime = datetime.utc.strftime(
|
16
|
+
utc_datetime = datetime.utc.strftime('%m-%d %H:%M:%S.%6N')
|
17
17
|
|
18
18
|
# Get the severity letter from the thread local variable or use
|
19
19
|
# the default. The thread local variable is set in the trace
|
@@ -115,6 +115,7 @@ module Familia
|
|
115
115
|
|
116
116
|
def ld(*msg)
|
117
117
|
return unless Familia.debug?
|
118
|
+
|
118
119
|
@logger.debug(*msg)
|
119
120
|
end
|
120
121
|
|
@@ -152,15 +153,15 @@ module Familia
|
|
152
153
|
# and multi blocks. In some contexts it's nil where the
|
153
154
|
# database connection isn't relevant.
|
154
155
|
instance_id = if dbclient
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
156
|
+
case dbclient
|
157
|
+
when Redis
|
158
|
+
dbclient.id.respond_to?(:to_s) ? dbclient.id.to_s : dbclient.class.name
|
159
|
+
when Redis::Future
|
160
|
+
'Redis::Future'
|
161
|
+
else
|
162
|
+
dbclient.class.name
|
163
|
+
end
|
164
|
+
end
|
164
165
|
|
165
166
|
codeline = if context
|
166
167
|
context = [context].flatten
|
@@ -170,7 +171,6 @@ module Familia
|
|
170
171
|
|
171
172
|
@logger.trace format('[%s] %s -> %s <- at %s', label, instance_id, ident, codeline)
|
172
173
|
end
|
173
|
-
|
174
174
|
end
|
175
175
|
end
|
176
176
|
|