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.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +3 -3
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +18 -3
  7. data/README.md +36 -157
  8. data/TEST_COVERAGE.md +40 -0
  9. data/docs/overview.md +359 -0
  10. data/docs/wiki/API-Reference.md +270 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
  12. data/docs/wiki/Home.md +49 -0
  13. data/docs/wiki/Implementation-Guide.md +183 -0
  14. data/docs/wiki/Security-Model.md +143 -0
  15. data/lib/familia/base.rb +18 -27
  16. data/lib/familia/connection.rb +6 -5
  17. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  18. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  19. data/lib/familia/{datatype → data_type}/types/hashkey.rb +2 -2
  20. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  21. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  22. data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
  23. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  24. data/lib/familia/{datatype.rb → data_type.rb} +10 -12
  25. data/lib/familia/encryption/manager.rb +102 -0
  26. data/lib/familia/encryption/provider.rb +49 -0
  27. data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
  28. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  29. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -0
  30. data/lib/familia/encryption/registry.rb +50 -0
  31. data/lib/familia/encryption.rb +178 -0
  32. data/lib/familia/encryption_request_cache.rb +68 -0
  33. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +153 -0
  34. data/lib/familia/features/encrypted_fields.rb +28 -0
  35. data/lib/familia/features/expiration.rb +107 -77
  36. data/lib/familia/features/quantization.rb +5 -9
  37. data/lib/familia/features/relatable_objects.rb +2 -4
  38. data/lib/familia/features/safe_dump.rb +14 -17
  39. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  40. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  41. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  42. data/lib/familia/features/transient_fields.rb +47 -0
  43. data/lib/familia/features.rb +40 -24
  44. data/lib/familia/field_type.rb +270 -0
  45. data/lib/familia/horreum/connection.rb +8 -11
  46. data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
  47. data/lib/familia/horreum/definition_methods.rb +453 -0
  48. data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
  49. data/lib/familia/horreum/serialization.rb +46 -18
  50. data/lib/familia/horreum/settings.rb +10 -2
  51. data/lib/familia/horreum/utils.rb +9 -10
  52. data/lib/familia/horreum.rb +18 -10
  53. data/lib/familia/logging.rb +14 -14
  54. data/lib/familia/settings.rb +39 -3
  55. data/lib/familia/utils.rb +45 -0
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -1
  58. data/try/core/base_enhancements_try.rb +115 -0
  59. data/try/core/connection_try.rb +0 -1
  60. data/try/core/errors_try.rb +0 -1
  61. data/try/core/familia_extended_try.rb +3 -4
  62. data/try/core/familia_try.rb +0 -1
  63. data/try/core/pools_try.rb +2 -2
  64. data/try/core/secure_identifier_try.rb +0 -1
  65. data/try/core/settings_try.rb +0 -1
  66. data/try/core/utils_try.rb +0 -1
  67. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  68. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  69. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  70. data/try/{datatypes → data_types}/list_try.rb +1 -2
  71. data/try/{datatypes → data_types}/set_try.rb +1 -2
  72. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  73. data/try/{datatypes → data_types}/string_try.rb +1 -2
  74. data/try/debugging/README.md +32 -0
  75. data/try/debugging/cache_behavior_tracer.rb +91 -0
  76. data/try/debugging/encryption_method_tracer.rb +138 -0
  77. data/try/debugging/provider_diagnostics.rb +110 -0
  78. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  79. data/try/edge_cases/json_serialization_try.rb +0 -1
  80. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  81. data/try/encryption/config_persistence_try.rb +192 -0
  82. data/try/encryption/encryption_core_try.rb +328 -0
  83. data/try/encryption/instance_variable_scope_try.rb +31 -0
  84. data/try/encryption/module_loading_try.rb +28 -0
  85. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  86. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  87. data/try/encryption/roundtrip_validation_try.rb +28 -0
  88. data/try/encryption/secure_memory_handling_try.rb +125 -0
  89. data/try/features/encrypted_fields_core_try.rb +117 -0
  90. data/try/features/encrypted_fields_integration_try.rb +220 -0
  91. data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
  92. data/try/features/encrypted_fields_security_try.rb +370 -0
  93. data/try/features/encryption_fields/aad_protection_try.rb +53 -0
  94. data/try/features/encryption_fields/context_isolation_try.rb +120 -0
  95. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  96. data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
  97. data/try/features/encryption_fields/fresh_key_try.rb +163 -0
  98. data/try/features/encryption_fields/key_rotation_try.rb +117 -0
  99. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  100. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  101. data/try/features/encryption_fields/nonce_uniqueness_try.rb +54 -0
  102. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  103. data/try/features/expiration_try.rb +0 -1
  104. data/try/features/feature_dependencies_try.rb +159 -0
  105. data/try/features/quantization_try.rb +0 -1
  106. data/try/features/real_feature_integration_try.rb +148 -0
  107. data/try/features/relatable_objects_try.rb +0 -1
  108. data/try/features/safe_dump_advanced_try.rb +0 -1
  109. data/try/features/safe_dump_try.rb +0 -1
  110. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  111. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  112. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  113. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  114. data/try/features/transient_fields_core_try.rb +181 -0
  115. data/try/features/transient_fields_integration_try.rb +260 -0
  116. data/try/helpers/test_helpers.rb +42 -0
  117. data/try/horreum/base_try.rb +157 -3
  118. data/try/horreum/class_methods_try.rb +27 -36
  119. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  120. data/try/horreum/field_categories_try.rb +118 -0
  121. data/try/horreum/field_definition_try.rb +96 -0
  122. data/try/horreum/initialization_try.rb +0 -1
  123. data/try/horreum/relations_try.rb +0 -1
  124. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  125. data/try/horreum/serialization_try.rb +2 -3
  126. data/try/memory/memory_basic_test.rb +73 -0
  127. data/try/memory/memory_detailed_test.rb +121 -0
  128. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  129. data/try/memory/memory_search_for_string.rb +83 -0
  130. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  131. data/try/models/customer_safe_dump_try.rb +0 -1
  132. data/try/models/customer_try.rb +0 -1
  133. data/try/models/datatype_base_try.rb +1 -2
  134. data/try/models/familia_object_try.rb +0 -1
  135. metadata +85 -18
@@ -0,0 +1,270 @@
1
+ # lib/familia/field_type.rb
2
+
3
+ module Familia
4
+ # Base class for all field types in Familia
5
+ #
6
+ # Field types encapsulate the behavior for different kinds of fields,
7
+ # including how their getter/setter methods are defined and how values
8
+ # are serialized/deserialized.
9
+ #
10
+ # @example Creating a custom field type
11
+ # class TimestampFieldType < Familia::FieldType
12
+ # def define_setter(klass)
13
+ # field_name = @name
14
+ # klass.define_method :"#{@method_name}=" do |value|
15
+ # timestamp = value.is_a?(Time) ? value.to_i : value
16
+ # instance_variable_set(:"@#{field_name}", timestamp)
17
+ # end
18
+ # end
19
+ #
20
+ # def define_getter(klass)
21
+ # field_name = @name
22
+ # klass.define_method @method_name do
23
+ # timestamp = instance_variable_get(:"@#{field_name}")
24
+ # timestamp ? Time.at(timestamp) : nil
25
+ # end
26
+ # end
27
+ # end
28
+ #
29
+ class FieldType
30
+ attr_reader :name, :options, :method_name, :fast_method_name, :on_conflict
31
+
32
+ # Initialize a new field type
33
+ #
34
+ # @param name [Symbol] The field name
35
+ # @param as [Symbol, String, false] The method name (defaults to field name)
36
+ # If false, no accessor methods are created
37
+ # @param fast_method [Symbol, String, false] The fast method name
38
+ # (defaults to "#{name}!"). If false, no fast method is created
39
+ # @param on_conflict [Symbol] Conflict resolution strategy when method
40
+ # already exists (:raise, :skip, :warn, :overwrite)
41
+ # @param options [Hash] Additional options for the field type
42
+ #
43
+ def initialize(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, **options)
44
+ @name = name.to_sym
45
+ @method_name = as == false ? nil : as.to_sym
46
+ @fast_method_name = fast_method == false ? nil : fast_method&.to_sym
47
+
48
+ # Validate fast method name format
49
+ if @fast_method_name && !@fast_method_name.to_s.end_with?('!')
50
+ raise ArgumentError, "Fast method name must end with '!' (got: #{@fast_method_name})"
51
+ end
52
+
53
+ @on_conflict = on_conflict
54
+ @options = options
55
+ end
56
+
57
+ # Install this field type on a class
58
+ #
59
+ # This method defines all necessary methods on the target class
60
+ # and registers the field type for later reference.
61
+ #
62
+ # @param klass [Class] The class to install this field type on
63
+ #
64
+ def install(klass)
65
+ if @method_name
66
+ # For skip strategy, check for any method conflicts first
67
+ if @on_conflict == :skip
68
+ has_getter_conflict = klass.method_defined?(@method_name) || klass.private_method_defined?(@method_name)
69
+ has_setter_conflict = klass.method_defined?(:"#{@method_name}=") || klass.private_method_defined?(:"#{@method_name}=")
70
+
71
+ # If either getter or setter conflicts, skip the whole field
72
+ return if has_getter_conflict || has_setter_conflict
73
+ end
74
+
75
+ define_getter(klass)
76
+ define_setter(klass)
77
+ end
78
+
79
+ define_fast_writer(klass) if @fast_method_name
80
+ end
81
+
82
+ # Define the getter method on the target class
83
+ #
84
+ # Subclasses can override this to customize getter behavior.
85
+ # The default implementation creates a simple attr_reader equivalent.
86
+ #
87
+ # @param klass [Class] The class to define the method on
88
+ #
89
+ def define_getter(klass)
90
+ field_name = @name
91
+ method_name = @method_name
92
+
93
+ handle_method_conflict(klass, method_name) do
94
+ klass.define_method method_name do
95
+ instance_variable_get(:"@#{field_name}")
96
+ end
97
+ end
98
+ end
99
+
100
+ # Define the setter method on the target class
101
+ #
102
+ # Subclasses can override this to customize setter behavior.
103
+ # The default implementation creates a simple attr_writer equivalent.
104
+ #
105
+ # @param klass [Class] The class to define the method on
106
+ #
107
+ def define_setter(klass)
108
+ field_name = @name
109
+ method_name = @method_name
110
+
111
+ handle_method_conflict(klass, :"#{method_name}=") do
112
+ klass.define_method :"#{method_name}=" do |value|
113
+ instance_variable_set(:"@#{field_name}", value)
114
+ end
115
+ end
116
+ end
117
+
118
+ # Define the fast writer method on the target class
119
+ #
120
+ # Fast methods provide direct database access for immediate persistence.
121
+ # Subclasses can override this to customize fast method behavior.
122
+ #
123
+ # @param klass [Class] The class to define the method on
124
+ #
125
+ def define_fast_writer(klass)
126
+ return unless @fast_method_name&.to_s&.end_with?('!')
127
+
128
+ field_name = @name
129
+ method_name = @method_name
130
+ fast_method_name = @fast_method_name
131
+
132
+ handle_method_conflict(klass, fast_method_name) do
133
+ klass.define_method fast_method_name do |*args|
134
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0 or 1)" if args.size > 1
135
+
136
+ val = args.first
137
+
138
+ # If no value provided, return current stored value
139
+ return hget(field_name) if val.nil?
140
+
141
+ begin
142
+ # Trace the operation if debugging is enabled
143
+ Familia.trace :FAST_WRITER, dbclient, "#{field_name}: #{val.inspect}", caller(1..1) if Familia.debug?
144
+
145
+ # Convert value for database storage
146
+ prepared = serialize_value(val)
147
+ Familia.ld "[FieldType#define_fast_writer] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
148
+
149
+ # Use the setter method to update instance variable
150
+ send(:"#{method_name}=", val) if method_name
151
+
152
+ # Persist to database immediately
153
+ ret = hset(field_name, prepared)
154
+ ret.zero? || ret.positive?
155
+ rescue Familia::Problem => e
156
+ raise "#{fast_method_name} method failed: #{e.message}", e.backtrace
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ # Whether this field should be persisted to the database
163
+ #
164
+ # @return [Boolean] true if field should be persisted
165
+ #
166
+ def persistent?
167
+ true
168
+ end
169
+
170
+ def transient?
171
+ !persistent?
172
+ end
173
+
174
+ # The category for this field type (used for filtering)
175
+ #
176
+ # @return [Symbol] the field category
177
+ #
178
+ def category
179
+ :field
180
+ end
181
+
182
+ # Serialize a value for database storage
183
+ #
184
+ # Subclasses can override this to customize serialization.
185
+ # The default implementation passes values through unchanged.
186
+ #
187
+ # @param value [Object] The value to serialize
188
+ # @param record [Object] The record instance (for context)
189
+ # @return [Object] The serialized value
190
+ #
191
+ def serialize(value, _record = nil)
192
+ value
193
+ end
194
+
195
+ # Deserialize a value from database storage
196
+ #
197
+ # Subclasses can override this to customize deserialization.
198
+ # The default implementation passes values through unchanged.
199
+ #
200
+ # @param value [Object] The value to deserialize
201
+ # @param record [Object] The record instance (for context)
202
+ # @return [Object] The deserialized value
203
+ #
204
+ def deserialize(value, _record = nil)
205
+ value
206
+ end
207
+
208
+ # Returns all method names generated for this field (used for conflict detection)
209
+ #
210
+ # @return [Array<Symbol>] Array of method names this field type generates
211
+ #
212
+ def generated_methods
213
+ [@method_name, @fast_method_name].compact
214
+ end
215
+
216
+ # Enhanced inspection output for debugging
217
+ #
218
+ # @return [String] Human-readable representation
219
+ #
220
+ def inspect
221
+ attributes = [
222
+ "name=#{@name}",
223
+ "method_name=#{@method_name}",
224
+ "fast_method_name=#{@fast_method_name}",
225
+ "on_conflict=#{@on_conflict}",
226
+ "category=#{category}"
227
+ ]
228
+ "#<#{self.class.name} #{attributes.join(' ')}>"
229
+ end
230
+ alias to_s inspect
231
+
232
+ private
233
+
234
+ # Handle method name conflicts during definition
235
+ #
236
+ # @param klass [Class] The target class
237
+ # @param method_name [Symbol] The method name to define
238
+ # @yield Block that defines the method
239
+ #
240
+ def handle_method_conflict(klass, method_name)
241
+ case @on_conflict
242
+ when :skip
243
+ return if klass.method_defined?(method_name) || klass.private_method_defined?(method_name)
244
+ when :warn
245
+ if klass.method_defined?(method_name) || klass.private_method_defined?(method_name)
246
+ warn <<~WARNING
247
+
248
+ WARNING: Method >>> #{method_name} <<< already exists on #{klass}.
249
+ Field functionality may be broken. Consider using a different name
250
+ with field(:#{@name}, as: :other_name)
251
+
252
+ Called from:
253
+ #{Familia.pretty_stack(limit: 3)}
254
+
255
+ WARNING
256
+ end
257
+ when :raise
258
+ if klass.method_defined?(method_name) || klass.private_method_defined?(method_name)
259
+ raise ArgumentError, "Method >>> #{method_name} <<< already defined for #{klass}"
260
+ end
261
+ when :overwrite
262
+ # Proceed silently - allow overwrite
263
+ else
264
+ raise ArgumentError, "Unknown conflict resolution strategy: #{@on_conflict}"
265
+ end
266
+
267
+ yield
268
+ end
269
+ end
270
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Familia
4
4
  class Horreum
5
-
6
5
  # Familia::Horreum::Connection
7
6
  #
8
7
  module Connection
@@ -47,36 +46,34 @@ module Familia
47
46
  #
48
47
  # @note This method works with the global Familia.transaction context when available
49
48
  #
50
- def transaction
49
+ def transaction(&)
51
50
  # If we're already in a Familia.transaction context, just yield the multi connection
52
51
  if Fiber[:familia_transaction]
53
52
  yield(Fiber[:familia_transaction])
54
53
  else
55
54
  # Otherwise, create a local transaction
56
- block_result = dbclient.multi do |conn|
57
- yield(conn)
58
- end
55
+ block_result = dbclient.multi(&)
59
56
  end
60
57
  block_result
61
58
  end
62
59
  alias multi transaction
63
60
 
64
- def pipeline
61
+ def pipeline(&)
65
62
  # If we're already in a Familia.pipeline context, just yield the pipeline connection
66
63
  if Fiber[:familia_pipeline]
67
64
  yield(Fiber[:familia_pipeline])
68
65
  else
69
66
  # Otherwise, create a local transaction
70
- block_result = dbclient.pipeline do |conn|
71
- yield(conn)
72
- end
67
+ block_result = dbclient.pipeline(&)
73
68
  end
74
69
  block_result
75
70
  end
76
-
77
71
  end
78
72
 
79
- # Include Connection module for instance methods after it's loaded
73
+ # include for instance methods after it's loaded. Note that Horreum::Utils
74
+ # are also included and at one time also has a uri method. This connection
75
+ # module is also extended for the class level methods. It will require some
76
+ # disambiguation at some point.
80
77
  include Familia::Horreum::Connection
81
78
  end
82
79
  end
@@ -1,4 +1,4 @@
1
- # lib/familia/horreum/commands.rb
1
+ # lib/familia/horreum/database_commands.rb
2
2
 
3
3
  module Familia
4
4
  # InstanceMethods - Module containing instance-level methods for Familia
@@ -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
  # Methods that call Database commands (InstanceMethods)
12
11
  #
13
12
  # NOTE: There is no hgetall for Horreum. This is because Horreum
@@ -16,8 +15,7 @@ module Familia
16
15
  # emphasize this, instead of "refreshing" the object with hgetall,
17
16
  # just load the object again.
18
17
  #
19
- module Commands
20
-
18
+ module DatabaseCommands
21
19
  def move(logical_database)
22
20
  dbclient.move dbkey, logical_database
23
21
  end
@@ -41,6 +39,7 @@ module Familia
41
39
  def exists?(check_size: true)
42
40
  key_exists = self.class.dbclient.exists?(dbkey)
43
41
  return key_exists unless check_size
42
+
44
43
  key_exists && !size.zero?
45
44
  end
46
45
 
@@ -83,21 +82,11 @@ module Familia
83
82
  end
84
83
  alias remove remove_field # deprecated
85
84
 
86
- def datatype
85
+ def data_type
87
86
  Familia.trace :DATATYPE, dbclient, uri, caller(1..1) if Familia.debug?
88
87
  dbclient.type dbkey(suffix)
89
88
  end
90
89
 
91
-
92
- # Retrieves the prefix for the current instance by delegating to its class.
93
- #
94
- # @return [String] The prefix associated with the class of the current instance.
95
- # @example
96
- # instance.prefix
97
- def prefix
98
- self.class.prefix
99
- end
100
-
101
90
  # For parity with DataType#hgetall
102
91
  def hgetall
103
92
  Familia.trace :HGETALL, dbclient, uri, caller(1..1) if Familia.debug?
@@ -117,8 +106,8 @@ module Familia
117
106
  dbclient.hset dbkey, field, value
118
107
  end
119
108
 
120
- def hmset(hsh={})
121
- hsh ||= self.to_h
109
+ def hmset(hsh = {})
110
+ hsh ||= to_h
122
111
  Familia.trace :HMSET, dbclient, hsh, caller(1..1) if Familia.debug?
123
112
  dbclient.hmset dbkey(suffix), hsh
124
113
  end
@@ -175,9 +164,8 @@ module Familia
175
164
  ret.positive?
176
165
  end
177
166
  alias clear delete!
178
-
179
167
  end
180
168
 
181
- include Commands # these become Familia::Horreum instance methods
169
+ include DatabaseCommands # these become Familia::Horreum instance methods
182
170
  end
183
171
  end