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
@@ -1,255 +1,21 @@
|
|
1
|
-
# lib/familia/horreum/
|
1
|
+
# lib/familia/horreum/subclass/management.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::
|
34
|
-
include Familia::Horreum::RelatedFieldsManagement
|
35
|
-
|
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
|
-
# Converts the class name into a string that can be used to look up
|
241
|
-
# configuration values. This is particularly useful when mapping
|
242
|
-
# familia models with specific database numbers in the configuration.
|
243
|
-
#
|
244
|
-
# @example V2::Session.config_name => 'session'
|
245
|
-
#
|
246
|
-
# @return [String] The underscored class name as a string
|
247
|
-
def config_name
|
248
|
-
name.split('::').last
|
249
|
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
250
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
251
|
-
.downcase
|
252
|
-
end
|
17
|
+
module ManagementMethods
|
18
|
+
include Familia::Horreum::RelatedFieldsManagement # Provides DataType query methods
|
253
19
|
|
254
20
|
# Creates and persists a new instance of the class.
|
255
21
|
#
|
@@ -289,9 +55,7 @@ module Familia
|
|
289
55
|
#
|
290
56
|
def create(*, **)
|
291
57
|
fobj = new(*, **)
|
292
|
-
|
293
|
-
|
294
|
-
fobj.save
|
58
|
+
fobj.save_if_not_exists
|
295
59
|
fobj
|
296
60
|
end
|
297
61
|
|
@@ -406,8 +170,8 @@ module Familia
|
|
406
170
|
# User.exists?(123) # Returns true if user:123:object exists in Redis
|
407
171
|
#
|
408
172
|
def exists?(identifier, suffix = nil)
|
173
|
+
raise NoIdentifier, "Empty identifier" if identifier.to_s.empty?
|
409
174
|
suffix ||= self.suffix
|
410
|
-
return false if identifier.to_s.empty?
|
411
175
|
|
412
176
|
objkey = dbkey identifier, suffix
|
413
177
|
|
@@ -468,21 +232,34 @@ module Familia
|
|
468
232
|
# distinction b/c passing in an explicitly nil is how DataType objects
|
469
233
|
# at the class level are created without the global default 'object'
|
470
234
|
# suffix. See DataType#dbkey "parent_class?" for more details.
|
235
|
+
#
|
471
236
|
def dbkey(identifier, suffix = self.suffix)
|
472
|
-
|
473
|
-
|
237
|
+
if identifier.to_s.empty?
|
238
|
+
raise NoIdentifier, "#{self} requires non-empty identifier, got: #{identifier.inspect}"
|
239
|
+
end
|
474
240
|
|
475
241
|
identifier &&= identifier.to_s
|
476
242
|
Familia.dbkey(prefix, identifier, suffix)
|
477
243
|
end
|
478
244
|
|
479
|
-
def
|
480
|
-
|
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) }
|
249
|
+
end
|
250
|
+
|
251
|
+
def any?(filter = '*')
|
252
|
+
matching_keys_count(filter) > 0
|
481
253
|
end
|
482
254
|
|
483
|
-
|
484
|
-
|
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
|
+
#
|
259
|
+
def matching_keys_count(filter = '*')
|
260
|
+
dbclient.keys(dbkey(filter)).compact.size
|
485
261
|
end
|
262
|
+
alias size matching_keys_count # For backwards compatibility
|
486
263
|
end
|
487
264
|
end
|
488
265
|
end
|
data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb}
RENAMED
@@ -15,7 +15,7 @@ module Familia
|
|
15
15
|
#
|
16
16
|
# Usage:
|
17
17
|
# Include this module in classes that need DataType management
|
18
|
-
# Call
|
18
|
+
# Call setup_related_fields_accessors to initialize the feature
|
19
19
|
#
|
20
20
|
module RelatedFieldsManagement
|
21
21
|
# A practical flag to indicate that a Horreum member has relations,
|
@@ -23,17 +23,22 @@ module Familia
|
|
23
23
|
@has_relations = nil
|
24
24
|
|
25
25
|
def self.included(base)
|
26
|
-
base.extend(
|
27
|
-
base.
|
26
|
+
base.extend(RelatedFieldsAccessors)
|
27
|
+
base.setup_related_fields_accessors
|
28
28
|
end
|
29
29
|
|
30
|
-
module
|
30
|
+
module RelatedFieldsAccessors
|
31
31
|
# Sets up all DataType related methods
|
32
|
-
# This method
|
32
|
+
# This method generates the following for each registered DataType:
|
33
33
|
#
|
34
|
-
|
34
|
+
# Instance methods: set(), list(), hashkey(), sorted_set(), etc.
|
35
|
+
# Query methods: set?(), list?(), hashkey?(), sorted_set?(), etc.
|
36
|
+
# Collection methods: sets(), lists(), hashkeys(), sorted_sets(), etc.
|
37
|
+
# Class methods: class_set(), class_list(), etc.
|
38
|
+
#
|
39
|
+
def setup_related_fields_accessors
|
35
40
|
Familia::DataType.registered_types.each_pair do |kind, klass|
|
36
|
-
Familia.
|
41
|
+
Familia.trace :registered_types, kind, klass, caller(1..1) if Familia.debug?
|
37
42
|
|
38
43
|
# Dynamically define instance-level relation methods
|
39
44
|
#
|
@@ -86,11 +91,11 @@ module Familia
|
|
86
91
|
end
|
87
92
|
end
|
88
93
|
end
|
89
|
-
# End of
|
94
|
+
# End of RelatedFieldsAccessors module
|
90
95
|
|
91
96
|
# Creates an instance-level relation
|
92
97
|
def attach_instance_related_field(name, klass, opts)
|
93
|
-
Familia.
|
98
|
+
Familia.trace :attach_instance, "#{name} #{klass}", opts, caller(1..1) if Familia.debug?
|
94
99
|
raise ArgumentError, "Name is blank (#{klass})" if name.to_s.empty?
|
95
100
|
|
96
101
|
name = name.to_s.to_sym
|
@@ -115,7 +120,7 @@ module Familia
|
|
115
120
|
|
116
121
|
# Creates a class-level relation
|
117
122
|
def attach_class_related_field(name, klass, opts)
|
118
|
-
Familia.
|
123
|
+
Familia.trace :attach_class_related_field, "#{name} #{klass}", opts, caller(1..1) if Familia.debug?
|
119
124
|
raise ArgumentError, 'Name is blank (klass)' if name.to_s.empty?
|
120
125
|
|
121
126
|
name = name.to_s.to_sym
|
data/lib/familia/horreum.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
# lib/familia/horreum.rb
|
2
2
|
|
3
|
+
require_relative 'horreum/subclass/definition'
|
4
|
+
require_relative 'horreum/subclass/management'
|
5
|
+
require_relative 'horreum/shared/settings'
|
6
|
+
require_relative 'horreum/core'
|
7
|
+
|
3
8
|
module Familia
|
4
9
|
#
|
5
10
|
# Horreum: A module for managing Redis-based object storage and relationships
|
@@ -23,6 +28,8 @@ module Familia
|
|
23
28
|
#
|
24
29
|
class Horreum
|
25
30
|
include Familia::Base
|
31
|
+
include Familia::Horreum::Core
|
32
|
+
include Familia::Horreum::Settings
|
26
33
|
|
27
34
|
# Singleton Class Context
|
28
35
|
#
|
@@ -62,12 +69,14 @@ module Familia
|
|
62
69
|
# Extends ClassMethods to subclasses and tracks Familia members
|
63
70
|
def inherited(member)
|
64
71
|
Familia.trace :HORREUM, nil, "Welcome #{member} to the family", caller(1..1) if Familia.debug?
|
65
|
-
member.extend(ClassMethods)
|
66
|
-
member.extend(Connection)
|
67
|
-
member.extend(Features)
|
68
72
|
|
69
|
-
#
|
70
|
-
#
|
73
|
+
# Class-level functionality extensions:
|
74
|
+
member.extend(Familia::Horreum::DefinitionMethods) # field(), identifier_field(), dbkey()
|
75
|
+
member.extend(Familia::Horreum::ManagementMethods) # create(), find(), destroy!()
|
76
|
+
member.extend(Familia::Horreum::Connection) # dbclient, connection management
|
77
|
+
member.extend(Familia::Features) # feature() method for optional modules
|
78
|
+
|
79
|
+
# Track all classes that inherit from Horreum
|
71
80
|
Familia.members << member
|
72
81
|
super
|
73
82
|
end
|
@@ -83,7 +92,7 @@ module Familia
|
|
83
92
|
# Session.new({sessid: "abc123", custid: "user456"}) # legacy hash (robust)
|
84
93
|
#
|
85
94
|
def initialize(*args, **kwargs)
|
86
|
-
Familia.
|
95
|
+
Familia.trace :INITIALIZE, dbclient, "Initializing #{self.class}", caller(1..1) if Familia.debug?
|
87
96
|
initialize_relatives
|
88
97
|
|
89
98
|
# No longer auto-create a key field - the identifier method will
|
@@ -122,7 +131,7 @@ module Familia
|
|
122
131
|
elsif args.any?
|
123
132
|
initialize_with_positional_args(*args)
|
124
133
|
else
|
125
|
-
|
134
|
+
Familia.trace :INITIALIZE, dbclient, "#{self.class} initialized with no arguments", caller(1..1) if Familia.debug?
|
126
135
|
# Default values are intentionally NOT set here to:
|
127
136
|
# - Maintain Database memory efficiency (only store non-nil values)
|
128
137
|
# - Avoid conflicts with nil-skipping serialization logic
|
@@ -133,7 +142,11 @@ module Familia
|
|
133
142
|
# Implementing classes can define an init method to do any
|
134
143
|
# additional initialization. Notice that this is called
|
135
144
|
# after the fields are set.
|
136
|
-
init
|
145
|
+
init
|
146
|
+
end
|
147
|
+
|
148
|
+
def init(*args, **kwargs)
|
149
|
+
# Default no-op
|
137
150
|
end
|
138
151
|
|
139
152
|
# Sets up related Database objects for the instance
|
@@ -150,10 +163,10 @@ module Familia
|
|
150
163
|
# familia_object.dbkey == v1:bone:INDEXVALUE:object
|
151
164
|
# familia_object.related_object.dbkey == v1:bone:INDEXVALUE:name
|
152
165
|
#
|
153
|
-
self.class.related_fields.each_pair do |name,
|
154
|
-
klass =
|
155
|
-
opts =
|
156
|
-
Familia.
|
166
|
+
self.class.related_fields.each_pair do |name, data_type_definition|
|
167
|
+
klass = data_type_definition.klass
|
168
|
+
opts = data_type_definition.opts
|
169
|
+
Familia.trace :INITIALIZE_RELATIVES, dbclient, "#{name} => #{klass} #{opts.keys}", caller(1..1) if Familia.debug?
|
157
170
|
|
158
171
|
# As a subclass of Familia::Horreum, we add ourselves as the parent
|
159
172
|
# automatically. This is what determines the dbkey for DataType
|
@@ -215,7 +228,9 @@ module Familia
|
|
215
228
|
# we use symbols. So we check for both.
|
216
229
|
value = fields[field.to_sym] || fields[field.to_s]
|
217
230
|
if value
|
218
|
-
|
231
|
+
# Use the mapped method name, not the field name
|
232
|
+
method_name = self.class.field_method_map[field] || field
|
233
|
+
send(:"#{method_name}=", value)
|
219
234
|
field.to_sym
|
220
235
|
end
|
221
236
|
end
|
@@ -283,9 +298,8 @@ module Familia
|
|
283
298
|
# # => #<Redis client v5.4.1 for redis://localhost:6379/0>
|
284
299
|
#
|
285
300
|
def dbclient
|
286
|
-
|
301
|
+
Fiber[:familia_transaction] || @dbclient || self.class.dbclient
|
287
302
|
# conn.select(self.class.logical_database)
|
288
|
-
conn
|
289
303
|
end
|
290
304
|
|
291
305
|
def generate_id
|
@@ -299,14 +313,8 @@ module Familia
|
|
299
313
|
# This allows passing Familia objects directly where strings are expected
|
300
314
|
# without requiring explicit .identifier calls
|
301
315
|
return super if identifier.to_s.empty?
|
316
|
+
|
302
317
|
identifier.to_s
|
303
318
|
end
|
304
319
|
end
|
305
320
|
end
|
306
|
-
|
307
|
-
require_relative 'horreum/class_methods'
|
308
|
-
require_relative 'horreum/commands'
|
309
|
-
require_relative 'horreum/connection'
|
310
|
-
require_relative 'horreum/serialization'
|
311
|
-
require_relative 'horreum/settings'
|
312
|
-
require_relative 'horreum/utils'
|
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
|
|
data/lib/familia/settings.rb
CHANGED
@@ -1,16 +1,18 @@
|
|
1
1
|
# lib/familia/settings.rb
|
2
2
|
|
3
3
|
module Familia
|
4
|
-
|
5
4
|
@delim = ':'
|
6
5
|
@prefix = nil
|
7
6
|
@suffix = :object
|
8
7
|
@default_expiration = 0 # see update_expiration. Zero is skip. nil is an exception.
|
9
8
|
@logical_database = nil
|
9
|
+
@encryption_keys = nil
|
10
|
+
@current_key_version = nil
|
11
|
+
@encryption_personalization = 'FamilialMatters'
|
10
12
|
|
11
13
|
module Settings
|
12
|
-
|
13
|
-
|
14
|
+
attr_writer :delim, :suffix, :default_expiration, :logical_database, :prefix, :encryption_keys,
|
15
|
+
:current_key_version, :encryption_personalization
|
14
16
|
|
15
17
|
def delim(val = nil)
|
16
18
|
@delim = val if val
|
@@ -44,5 +46,39 @@ module Familia
|
|
44
46
|
suffix
|
45
47
|
end
|
46
48
|
|
49
|
+
def encryption_keys(val = nil)
|
50
|
+
@encryption_keys = val if val
|
51
|
+
@encryption_keys
|
52
|
+
end
|
53
|
+
|
54
|
+
def current_key_version(val = nil)
|
55
|
+
@current_key_version = val if val
|
56
|
+
@current_key_version
|
57
|
+
end
|
58
|
+
|
59
|
+
# Personalization string for BLAKE2b key derivation in XChaCha20Poly1305.
|
60
|
+
# This provides cryptographic domain separation, ensuring derived keys are
|
61
|
+
# unique per application even with identical master keys and contexts.
|
62
|
+
# Must be 16 bytes or less (automatically padded with null bytes).
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# Familia.configure do |config|
|
66
|
+
# config.encryption_personalization = 'MyApp1.0'
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# @param val [String, nil] The personalization string, or nil to get current value
|
70
|
+
# @return [String] Current personalization string
|
71
|
+
def encryption_personalization(val = nil)
|
72
|
+
if val
|
73
|
+
raise ArgumentError, 'Personalization string cannot exceed 16 bytes' if val.bytesize > 16
|
74
|
+
|
75
|
+
@encryption_personalization = val
|
76
|
+
end
|
77
|
+
@encryption_personalization
|
78
|
+
end
|
79
|
+
|
80
|
+
def config
|
81
|
+
self
|
82
|
+
end
|
47
83
|
end
|
48
84
|
end
|
data/lib/familia/utils.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# lib/familia/utils.rb
|
2
2
|
|
3
3
|
module Familia
|
4
|
+
|
5
|
+
# Family-related utility methods
|
6
|
+
#
|
4
7
|
module Utils
|
5
8
|
|
6
9
|
# Joins array elements with Familia delimiter
|
@@ -145,5 +148,47 @@ module Familia
|
|
145
148
|
end
|
146
149
|
end
|
147
150
|
|
151
|
+
# Converts an absolute file path to a path relative to the current working
|
152
|
+
# directory. This simplifies logging and error reporting by showing
|
153
|
+
# only the relevant parts of file paths instead of lengthy absolute paths.
|
154
|
+
#
|
155
|
+
# @param filepath [String, Pathname] The file path to convert
|
156
|
+
# @return [Pathname, String, nil] A relative path from current directory,
|
157
|
+
# basename if path goes outside current directory, or nil if filepath is nil
|
158
|
+
#
|
159
|
+
# @example Using current directory as base
|
160
|
+
# Utils.pretty_path("/home/dev/project/lib/config.rb") # => "lib/config.rb"
|
161
|
+
#
|
162
|
+
# @example Path outside current directory
|
163
|
+
# Utils.pretty_path("/etc/hosts") # => "hosts"
|
164
|
+
#
|
165
|
+
# @example Nil input
|
166
|
+
# Utils.pretty_path(nil) # => nil
|
167
|
+
#
|
168
|
+
# @see Pathname#relative_path_from Ruby standard library documentation
|
169
|
+
def pretty_path(filepath)
|
170
|
+
return nil if filepath.nil?
|
171
|
+
|
172
|
+
basepath = Dir.pwd
|
173
|
+
relative_path = Pathname.new(filepath).relative_path_from(basepath)
|
174
|
+
if relative_path.to_s.start_with?('..')
|
175
|
+
File.basename(filepath)
|
176
|
+
else
|
177
|
+
relative_path
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Formats a stack trace with pretty file paths for improved readability
|
182
|
+
#
|
183
|
+
# @param limit [Integer] Maximum number of stack frames to include (default: 3)
|
184
|
+
# @return [String] Formatted stack trace with relative paths joined by newlines
|
185
|
+
#
|
186
|
+
# @example
|
187
|
+
# Utils.pretty_stack(limit: 10)
|
188
|
+
# # => "lib/models/user.rb:25:in `save'\n lib/controllers/app.rb:45:in `create'"
|
189
|
+
def pretty_stack(skip: 1, limit: 5)
|
190
|
+
caller(skip..(skip + limit + 1)).first(limit).map { |frame| pretty_path(frame) }.join("\n")
|
191
|
+
end
|
192
|
+
|
148
193
|
end
|
149
194
|
end
|