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.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +11 -8
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +19 -3
  7. data/README.md +36 -157
  8. data/docs/overview.md +359 -0
  9. data/docs/wiki/API-Reference.md +347 -0
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +600 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +106 -0
  17. data/docs/wiki/Implementation-Guide.md +276 -0
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/RelatableObjects-Guide.md +563 -0
  20. data/docs/wiki/Security-Model.md +183 -0
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/lib/familia/base.rb +18 -27
  23. data/lib/familia/connection.rb +6 -5
  24. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  25. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  26. data/lib/familia/data_type/types/counter.rb +38 -0
  27. data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
  28. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  31. data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
  32. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  33. data/lib/familia/{datatype.rb → data_type.rb} +12 -14
  34. data/lib/familia/encryption/encrypted_data.rb +137 -0
  35. data/lib/familia/encryption/manager.rb +119 -0
  36. data/lib/familia/encryption/provider.rb +49 -0
  37. data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
  38. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  39. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
  40. data/lib/familia/encryption/registry.rb +50 -0
  41. data/lib/familia/encryption.rb +178 -0
  42. data/lib/familia/encryption_request_cache.rb +68 -0
  43. data/lib/familia/errors.rb +17 -3
  44. data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
  45. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
  46. data/lib/familia/features/encrypted_fields.rb +28 -0
  47. data/lib/familia/features/expiration.rb +107 -77
  48. data/lib/familia/features/quantization.rb +5 -9
  49. data/lib/familia/features/relatable_objects.rb +2 -4
  50. data/lib/familia/features/safe_dump.rb +14 -17
  51. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  52. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  53. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  54. data/lib/familia/features/transient_fields.rb +47 -0
  55. data/lib/familia/features.rb +40 -24
  56. data/lib/familia/field_type.rb +273 -0
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
  58. data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
  63. data/lib/familia/horreum/subclass/definition.rb +469 -0
  64. data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +30 -22
  67. data/lib/familia/logging.rb +14 -14
  68. data/lib/familia/settings.rb +39 -3
  69. data/lib/familia/utils.rb +45 -0
  70. data/lib/familia/version.rb +1 -1
  71. data/lib/familia.rb +3 -2
  72. data/try/core/base_enhancements_try.rb +115 -0
  73. data/try/core/connection_try.rb +0 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -5
  77. data/try/core/familia_extended_try.rb +3 -4
  78. data/try/core/familia_try.rb +1 -2
  79. data/try/core/persistence_operations_try.rb +297 -0
  80. data/try/core/pools_try.rb +2 -2
  81. data/try/core/secure_identifier_try.rb +0 -1
  82. data/try/core/settings_try.rb +0 -1
  83. data/try/core/utils_try.rb +0 -1
  84. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  85. data/try/data_types/counter_try.rb +93 -0
  86. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  87. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  88. data/try/{datatypes → data_types}/list_try.rb +1 -2
  89. data/try/data_types/lock_try.rb +133 -0
  90. data/try/{datatypes → data_types}/set_try.rb +1 -2
  91. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  92. data/try/{datatypes → data_types}/string_try.rb +1 -2
  93. data/try/debugging/README.md +32 -0
  94. data/try/debugging/cache_behavior_tracer.rb +91 -0
  95. data/try/debugging/debug_aad_process.rb +82 -0
  96. data/try/debugging/debug_concealed_internal.rb +59 -0
  97. data/try/debugging/debug_concealed_reveal.rb +61 -0
  98. data/try/debugging/debug_context_aad.rb +68 -0
  99. data/try/debugging/debug_context_simple.rb +80 -0
  100. data/try/debugging/debug_cross_context.rb +62 -0
  101. data/try/debugging/debug_database_load.rb +64 -0
  102. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  103. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  104. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  105. data/try/debugging/debug_field_decrypt.rb +74 -0
  106. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  107. data/try/debugging/debug_load_path.rb +66 -0
  108. data/try/debugging/debug_method_definition.rb +46 -0
  109. data/try/debugging/debug_method_resolution.rb +41 -0
  110. data/try/debugging/debug_minimal.rb +24 -0
  111. data/try/debugging/debug_provider.rb +68 -0
  112. data/try/debugging/debug_secure_behavior.rb +73 -0
  113. data/try/debugging/debug_string_class.rb +46 -0
  114. data/try/debugging/debug_test.rb +46 -0
  115. data/try/debugging/debug_test_design.rb +80 -0
  116. data/try/debugging/encryption_method_tracer.rb +138 -0
  117. data/try/debugging/provider_diagnostics.rb +110 -0
  118. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  119. data/try/edge_cases/json_serialization_try.rb +0 -1
  120. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  121. data/try/encryption/config_persistence_try.rb +192 -0
  122. data/try/encryption/encryption_core_try.rb +328 -0
  123. data/try/encryption/instance_variable_scope_try.rb +31 -0
  124. data/try/encryption/module_loading_try.rb +28 -0
  125. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  126. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  127. data/try/encryption/roundtrip_validation_try.rb +28 -0
  128. data/try/encryption/secure_memory_handling_try.rb +125 -0
  129. data/try/features/encrypted_fields_core_try.rb +125 -0
  130. data/try/features/encrypted_fields_integration_try.rb +216 -0
  131. data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
  132. data/try/features/encrypted_fields_security_try.rb +377 -0
  133. data/try/features/encryption_fields/aad_protection_try.rb +138 -0
  134. data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
  135. data/try/features/encryption_fields/context_isolation_try.rb +141 -0
  136. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  137. data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
  138. data/try/features/encryption_fields/fresh_key_try.rb +168 -0
  139. data/try/features/encryption_fields/key_rotation_try.rb +123 -0
  140. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  141. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  142. data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
  143. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  144. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  145. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  146. data/try/features/expiration_try.rb +0 -1
  147. data/try/features/feature_dependencies_try.rb +159 -0
  148. data/try/features/quantization_try.rb +0 -1
  149. data/try/features/real_feature_integration_try.rb +148 -0
  150. data/try/features/relatable_objects_try.rb +0 -1
  151. data/try/features/safe_dump_advanced_try.rb +0 -1
  152. data/try/features/safe_dump_try.rb +0 -1
  153. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  154. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  155. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  156. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  157. data/try/features/transient_fields_core_try.rb +181 -0
  158. data/try/features/transient_fields_integration_try.rb +260 -0
  159. data/try/helpers/test_helpers.rb +67 -0
  160. data/try/horreum/base_try.rb +157 -3
  161. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  162. data/try/horreum/field_categories_try.rb +118 -0
  163. data/try/horreum/field_definition_try.rb +96 -0
  164. data/try/horreum/initialization_try.rb +1 -2
  165. data/try/horreum/relations_try.rb +1 -2
  166. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  167. data/try/horreum/serialization_try.rb +41 -7
  168. data/try/memory/memory_basic_test.rb +73 -0
  169. data/try/memory/memory_detailed_test.rb +121 -0
  170. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  171. data/try/memory/memory_search_for_string.rb +83 -0
  172. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  173. data/try/models/customer_safe_dump_try.rb +1 -2
  174. data/try/models/customer_try.rb +1 -2
  175. data/try/models/datatype_base_try.rb +1 -2
  176. data/try/models/familia_object_try.rb +0 -1
  177. metadata +131 -23
  178. data/lib/familia/horreum/serialization.rb +0 -445
@@ -0,0 +1,47 @@
1
+ # lib/familia/features/transient_fields.rb
2
+
3
+ require_relative 'transient_fields/redacted_string'
4
+
5
+ module Familia
6
+ module Features
7
+ # Familia::Features::TransientFields
8
+ #
9
+ # Provides secure transient fields that wrap sensitive values in RedactedString
10
+ # objects. These fields are excluded from serialization operations and provide
11
+ # automatic memory wiping for security.
12
+ #
13
+ module TransientFields
14
+ def self.included(base)
15
+ Familia.trace :included, base, self, caller(1..1) if Familia.debug?
16
+ base.extend ClassMethods
17
+ end
18
+
19
+ # ClassMethods
20
+ #
21
+ module ClassMethods
22
+ # Define a transient field that automatically wraps values in RedactedString
23
+ #
24
+ # @param name [Symbol] The field name
25
+ # @param as [Symbol] The method name (defaults to field name)
26
+ # @param kwargs [Hash] Additional field options
27
+ #
28
+ # @example Define a transient API key field
29
+ # class Service < Familia::Horreum
30
+ # feature :transient_fields
31
+ # transient_field :api_key
32
+ # end
33
+ #
34
+ def transient_field(name, as: name, **kwargs)
35
+ # Use the field type system - much cleaner than alias_method approach!
36
+ # We can now remove the transient_field method from this feature entirely
37
+ # since it's built into DefinitionMethods using TransientFieldType
38
+ require_relative 'transient_fields/transient_field_type'
39
+ field_type = TransientFieldType.new(name, as: as, **kwargs.merge(fast_method: false))
40
+ register_field_type(field_type)
41
+ end
42
+ end
43
+
44
+ Familia::Base.add_feature self, :transient_fields, depends_on: nil
45
+ end
46
+ end
47
+ end
@@ -2,45 +2,61 @@
2
2
 
3
3
  module Familia
4
4
 
5
+ FeatureDefinition = Data.define(:name, :depends_on)
6
+
7
+ # Familia::Features
8
+ #
5
9
  module Features
6
10
 
7
11
  @features_enabled = nil
8
12
  attr_reader :features_enabled
9
13
 
10
- def feature(val = nil)
14
+ def feature(feature_name = nil)
11
15
  @features_enabled ||= []
12
16
 
13
- # If there's a value provied check that it's a valid feature
14
- if val
15
- val = val.to_sym
16
- raise Familia::Problem, "Unsupported feature: #{val}" unless Familia::Base.features.key?(val)
17
+ return features_enabled if feature_name.nil?
17
18
 
18
- # If the feature is already enabled, do nothing but log about it
19
- if @features_enabled.member?(val)
20
- Familia.warn "[Familia::Settings] feature already enabled: #{val}"
21
- return
22
- end
19
+ # If there's a value provied check that it's a valid feature
20
+ feature_name = feature_name.to_sym
21
+ unless Familia::Base.features_available.key?(feature_name)
22
+ raise Familia::Problem, "Unsupported feature: #{feature_name}"
23
+ end
23
24
 
24
- Familia.trace :FEATURE, nil, "#{self} includes #{val.inspect}", caller(1..1) if Familia.debug?
25
+ # If the feature is already available, do nothing but log about it
26
+ if features_enabled.member?(feature_name)
27
+ Familia.warn "[#{self.class}] feature already available: #{feature_name}"
28
+ return
29
+ end
25
30
 
26
- klass = Familia::Base.features[val]
31
+ if Familia.debug?
32
+ Familia.trace :FEATURE, nil, "#{self} includes #{feature_name.inspect}", caller(1..1)
33
+ end
27
34
 
28
- # Extend the Familia::Base subclass (e.g. Customer) with the feature module
29
- include klass
35
+ # Add it to the list available features_enabled for Familia::Base classes.
36
+ features_enabled << feature_name
30
37
 
31
- # NOTE: We may also want to extend Familia::DataType here so that we can
32
- # call safe_dump on relations fields (e.g. list, set, zset, hashkey). Or
33
- # maybe that only makes sense for hashk/object relations.
34
- #
35
- # We'd need to avoid it getting included multiple times (i.e. once for each
36
- # Familia::Horreum subclass that includes the feature).
38
+ klass = Familia::Base.features_available[feature_name]
37
39
 
38
- # Now that the feature is loaded successfully, add it to the list
39
- # enabled features for Familia::Base classes.
40
- @features_enabled << val
40
+ # Validate dependencies
41
+ feature_def = Familia::Base.feature_definitions[feature_name]
42
+ if feature_def&.depends_on&.any?
43
+ missing = feature_def.depends_on - features_enabled
44
+ raise Familia::Problem, "#{feature_name} requires: #{missing.join(', ')}" if missing.any?
41
45
  end
42
46
 
43
- features_enabled
47
+ # Extend the Familia::Base subclass (e.g. Customer) with the feature module
48
+ include klass
49
+
50
+ # NOTE: Do we want to extend Familia::DataType here? That would make it
51
+ # possible to call safe_dump on relations fields (e.g. list, zset, hashkey).
52
+ #
53
+ # The challenge is that DataType classes (List, Set, etc.) are shared across
54
+ # all Horreum models. If Customer extends DataType with safe_dump, then
55
+ # Session's lists would also have it. Not ideal. If that's all we wanted
56
+ # then we can do that by looping through every DataType class here.
57
+ #
58
+ # We'd need to extend the DataType instances for each Horreum subclass. That
59
+ # avoids it getting included multiple times per DataType
44
60
  end
45
61
 
46
62
  end
@@ -0,0 +1,273 @@
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, :loggable
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 loggable [Boolean] Whether this field should be included in
42
+ # serialization and logging operations (default: true)
43
+ # @param options [Hash] Additional options for the field type
44
+ #
45
+ def initialize(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, loggable: true, **options)
46
+ @name = name.to_sym
47
+ @method_name = as == false ? nil : as.to_sym
48
+ @fast_method_name = fast_method == false ? nil : fast_method&.to_sym
49
+
50
+ # Validate fast method name format
51
+ if @fast_method_name && !@fast_method_name.to_s.end_with?('!')
52
+ raise ArgumentError, "Fast method name must end with '!' (got: #{@fast_method_name})"
53
+ end
54
+
55
+ @on_conflict = on_conflict
56
+ @loggable = loggable
57
+ @options = options
58
+ end
59
+
60
+ # Install this field type on a class
61
+ #
62
+ # This method defines all necessary methods on the target class
63
+ # and registers the field type for later reference.
64
+ #
65
+ # @param klass [Class] The class to install this field type on
66
+ #
67
+ def install(klass)
68
+ if @method_name
69
+ # For skip strategy, check for any method conflicts first
70
+ if @on_conflict == :skip
71
+ has_getter_conflict = klass.method_defined?(@method_name) || klass.private_method_defined?(@method_name)
72
+ has_setter_conflict = klass.method_defined?(:"#{@method_name}=") || klass.private_method_defined?(:"#{@method_name}=")
73
+
74
+ # If either getter or setter conflicts, skip the whole field
75
+ return if has_getter_conflict || has_setter_conflict
76
+ end
77
+
78
+ define_getter(klass)
79
+ define_setter(klass)
80
+ end
81
+
82
+ define_fast_writer(klass) if @fast_method_name
83
+ end
84
+
85
+ # Define the getter method on the target class
86
+ #
87
+ # Subclasses can override this to customize getter behavior.
88
+ # The default implementation creates a simple attr_reader equivalent.
89
+ #
90
+ # @param klass [Class] The class to define the method on
91
+ #
92
+ def define_getter(klass)
93
+ field_name = @name
94
+ method_name = @method_name
95
+
96
+ handle_method_conflict(klass, method_name) do
97
+ klass.define_method method_name do
98
+ instance_variable_get(:"@#{field_name}")
99
+ end
100
+ end
101
+ end
102
+
103
+ # Define the setter method on the target class
104
+ #
105
+ # Subclasses can override this to customize setter behavior.
106
+ # The default implementation creates a simple attr_writer equivalent.
107
+ #
108
+ # @param klass [Class] The class to define the method on
109
+ #
110
+ def define_setter(klass)
111
+ field_name = @name
112
+ method_name = @method_name
113
+
114
+ handle_method_conflict(klass, :"#{method_name}=") do
115
+ klass.define_method :"#{method_name}=" do |value|
116
+ instance_variable_set(:"@#{field_name}", value)
117
+ end
118
+ end
119
+ end
120
+
121
+ # Define the fast writer method on the target class
122
+ #
123
+ # Fast methods provide direct database access for immediate persistence.
124
+ # Subclasses can override this to customize fast method behavior.
125
+ #
126
+ # @param klass [Class] The class to define the method on
127
+ #
128
+ def define_fast_writer(klass)
129
+ return unless @fast_method_name&.to_s&.end_with?('!')
130
+
131
+ field_name = @name
132
+ method_name = @method_name
133
+ fast_method_name = @fast_method_name
134
+
135
+ handle_method_conflict(klass, fast_method_name) do
136
+ klass.define_method fast_method_name do |*args|
137
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0 or 1)" if args.size > 1
138
+
139
+ val = args.first
140
+
141
+ # If no value provided, return current stored value
142
+ return hget(field_name) if val.nil?
143
+
144
+ begin
145
+ # Trace the operation if debugging is enabled
146
+ Familia.trace :FAST_WRITER, dbclient, "#{field_name}: #{val.inspect}", caller(1..1) if Familia.debug?
147
+
148
+ # Convert value for database storage
149
+ prepared = serialize_value(val)
150
+ Familia.ld "[FieldType#define_fast_writer] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
151
+
152
+ # Use the setter method to update instance variable
153
+ send(:"#{method_name}=", val) if method_name
154
+
155
+ # Persist to database immediately
156
+ ret = hset(field_name, prepared)
157
+ ret.zero? || ret.positive?
158
+ rescue Familia::Problem => e
159
+ raise "#{fast_method_name} method failed: #{e.message}", e.backtrace
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ # Whether this field should be persisted to the database
166
+ #
167
+ # @return [Boolean] true if field should be persisted
168
+ #
169
+ def persistent?
170
+ true
171
+ end
172
+
173
+ def transient?
174
+ !persistent?
175
+ end
176
+
177
+ # The category for this field type (used for filtering)
178
+ #
179
+ # @return [Symbol] the field category
180
+ #
181
+ def category
182
+ :field
183
+ end
184
+
185
+ # Serialize a value for database storage
186
+ #
187
+ # Subclasses can override this to customize serialization.
188
+ # The default implementation passes values through unchanged.
189
+ #
190
+ # @param value [Object] The value to serialize
191
+ # @param record [Object] The record instance (for context)
192
+ # @return [Object] The serialized value
193
+ #
194
+ def serialize(value, _record = nil)
195
+ value
196
+ end
197
+
198
+ # Deserialize a value from database storage
199
+ #
200
+ # Subclasses can override this to customize deserialization.
201
+ # The default implementation passes values through unchanged.
202
+ #
203
+ # @param value [Object] The value to deserialize
204
+ # @param record [Object] The record instance (for context)
205
+ # @return [Object] The deserialized value
206
+ #
207
+ def deserialize(value, _record = nil)
208
+ value
209
+ end
210
+
211
+ # Returns all method names generated for this field (used for conflict detection)
212
+ #
213
+ # @return [Array<Symbol>] Array of method names this field type generates
214
+ #
215
+ def generated_methods
216
+ [@method_name, @fast_method_name].compact
217
+ end
218
+
219
+ # Enhanced inspection output for debugging
220
+ #
221
+ # @return [String] Human-readable representation
222
+ #
223
+ def inspect
224
+ attributes = [
225
+ "name=#{@name}",
226
+ "method_name=#{@method_name}",
227
+ "fast_method_name=#{@fast_method_name}",
228
+ "on_conflict=#{@on_conflict}",
229
+ "category=#{category}"
230
+ ]
231
+ "#<#{self.class.name} #{attributes.join(' ')}>"
232
+ end
233
+ alias to_s inspect
234
+
235
+ private
236
+
237
+ # Handle method name conflicts during definition
238
+ #
239
+ # @param klass [Class] The target class
240
+ # @param method_name [Symbol] The method name to define
241
+ # @yield Block that defines the method
242
+ #
243
+ def handle_method_conflict(klass, method_name)
244
+ case @on_conflict
245
+ when :skip
246
+ return if klass.method_defined?(method_name) || klass.private_method_defined?(method_name)
247
+ when :warn
248
+ if klass.method_defined?(method_name) || klass.private_method_defined?(method_name)
249
+ warn <<~WARNING
250
+
251
+ WARNING: Method >>> #{method_name} <<< already exists on #{klass}.
252
+ Field functionality may be broken. Consider using a different name
253
+ with field(:#{@name}, as: :other_name)
254
+
255
+ Called from:
256
+ #{Familia.pretty_stack(limit: 3)}
257
+
258
+ WARNING
259
+ end
260
+ when :raise
261
+ if klass.method_defined?(method_name) || klass.private_method_defined?(method_name)
262
+ raise ArgumentError, "Method >>> #{method_name} <<< already defined for #{klass}"
263
+ end
264
+ when :overwrite
265
+ # Proceed silently - allow overwrite
266
+ else
267
+ raise ArgumentError, "Unknown conflict resolution strategy: #{@on_conflict}"
268
+ end
269
+
270
+ yield
271
+ end
272
+ end
273
+ end
@@ -2,9 +2,8 @@
2
2
 
3
3
  module Familia
4
4
  class Horreum
5
-
6
- # Familia::Horreum::Connection
7
- #
5
+ # Connection: Valkey connection management for Horreum instances
6
+ # Provides both instance and class-level connection methods
8
7
  module Connection
9
8
  attr_reader :uri
10
9
 
@@ -47,36 +46,28 @@ 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
-
79
- # Include Connection module for instance methods after it's loaded
80
- include Familia::Horreum::Connection
81
72
  end
82
73
  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
@@ -39,8 +37,9 @@ module Familia
39
37
  # @note The default behavior maintains backward compatibility by treating empty hashes
40
38
  # as non-existent. Use `check_size: false` for pure key existence checking.
41
39
  def exists?(check_size: true)
42
- key_exists = self.class.dbclient.exists?(dbkey)
40
+ key_exists = self.class.exists?(identifier)
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,21 @@ module Familia
117
106
  dbclient.hset dbkey, field, value
118
107
  end
119
108
 
120
- def hmset(hsh={})
121
- hsh ||= self.to_h
109
+ # Sets field in the hash stored at key to value, only if field does not yet exist.
110
+ # If key does not exist, a new key holding a hash is created. If field already exists,
111
+ # this operation has no effect.
112
+ #
113
+ # @param field [String] The field to set in the hash
114
+ # @param value [String] The value to set for the field
115
+ # @return [Integer] 1 if the field is a new field in the hash and the value was set,
116
+ # 0 if the field already exists in the hash and no operation was performed
117
+ def hsetnx(field, value)
118
+ Familia.trace :HSETNX, dbclient, field, caller(1..1) if Familia.debug?
119
+ dbclient.hsetnx dbkey, field, value
120
+ end
121
+
122
+ def hmset(hsh = {})
123
+ hsh ||= to_h
122
124
  Familia.trace :HMSET, dbclient, hsh, caller(1..1) if Familia.debug?
123
125
  dbclient.hmset dbkey(suffix), hsh
124
126
  end
@@ -175,9 +177,6 @@ module Familia
175
177
  ret.positive?
176
178
  end
177
179
  alias clear delete!
178
-
179
180
  end
180
-
181
- include Commands # these become Familia::Horreum instance methods
182
181
  end
183
182
  end