familia 2.0.0.pre17 → 2.0.0.pre18
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/CHANGELOG.rst +60 -0
- data/CLAUDE.md +9 -2
- data/Gemfile.lock +1 -1
- data/README.md +13 -0
- data/bin/irb +1 -1
- data/docs/guides/core-field-system.md +48 -26
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection.rb +1 -1
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
- data/lib/familia/data_type/serialization.rb +5 -5
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +41 -27
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/horreum/database_commands.rb +1 -1
- data/lib/familia/horreum/definition.rb +6 -37
- data/lib/familia/horreum/management.rb +17 -12
- data/lib/familia/horreum/persistence.rb +1 -1
- data/lib/familia/horreum/serialization.rb +91 -73
- data/lib/familia/horreum.rb +10 -6
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +271 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -14
- data/pr_agent.toml +31 -0
- data/pr_compliance_checklist.yaml +45 -0
- data/try/edge_cases/empty_identifiers_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +31 -31
- data/try/edge_cases/json_serialization_try.rb +2 -2
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
- data/try/edge_cases/race_conditions_try.rb +1 -1
- data/try/edge_cases/reserved_keywords_try.rb +1 -1
- data/try/edge_cases/string_coercion_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
- data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
- data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
- data/try/features/encrypted_fields/memory_security_try.rb +1 -1
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
- data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
- data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
- data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
- data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
- data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
- data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
- data/try/features/expiration/expiration_try.rb +1 -1
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +1 -1
- data/try/features/feature_improvements_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_try.rb +1 -1
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/real_feature_integration_try.rb +17 -14
- data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
- data/try/features/relationships/indexing_try.rb +6 -1
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +4 -4
- data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +1 -1
- data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
- data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
- data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
- data/try/features/relationships/relationships_performance_try.rb +1 -1
- data/try/features/relationships/relationships_performance_working_try.rb +1 -1
- data/try/features/relationships/relationships_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_try.rb +1 -1
- data/try/features/transient_fields/redacted_string_try.rb +1 -1
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +1 -1
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
- data/try/{core → integration/connection}/pools_try.rb +1 -1
- data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
- data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
- data/try/{core → integration}/create_method_try.rb +1 -1
- data/try/integration/cross_component_try.rb +1 -1
- data/try/{core → integration}/database_consistency_try.rb +11 -8
- data/try/{core → integration}/familia_extended_try.rb +1 -1
- data/try/{core → integration}/familia_members_methods_try.rb +1 -1
- data/try/{models → integration/models}/customer_safe_dump_try.rb +1 -1
- data/try/{models → integration/models}/customer_try.rb +1 -1
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +1 -1
- data/try/{core → integration}/persistence_operations_try.rb +1 -1
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +1 -1
- data/try/{core → integration}/secure_identifier_try.rb +1 -1
- data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
- data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
- data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
- data/try/{core → unit/core}/autoloader_try.rb +1 -1
- data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
- data/try/{core → unit/core}/connection_try.rb +1 -1
- data/try/{core → unit/core}/errors_try.rb +1 -1
- data/try/{core → unit/core}/extensions_try.rb +1 -1
- data/try/unit/core/familia_logger_try.rb +110 -0
- data/try/{core → unit/core}/familia_try.rb +1 -1
- data/try/{core → unit/core}/middleware_try.rb +41 -1
- data/try/{core → unit/core}/settings_try.rb +1 -1
- data/try/{core → unit/core}/time_utils_try.rb +1 -1
- data/try/{core → unit/core}/tools_try.rb +1 -1
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
- data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
- data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
- data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
- data/try/{data_types → unit/data_types}/list_try.rb +1 -1
- data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
- data/try/{data_types → unit/data_types}/string_try.rb +1 -1
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +1 -1
- data/try/{horreum → unit/horreum}/base_try.rb +3 -3
- data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
- data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
- data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
- data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
- data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
- data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
- data/try/{horreum → unit/horreum}/initialization_try.rb +2 -2
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
- metadata +134 -125
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/try/{debugging → support/debugging}/README.md +0 -0
- /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
- /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
- /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
- /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
- /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
- /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -7,21 +7,20 @@ module Familia
|
|
7
7
|
module Serialization
|
8
8
|
# Converts the object's persistent fields to a hash for external use.
|
9
9
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
10
|
+
# Returns actual Ruby values (String, Integer, Hash, etc.) for API consumption,
|
11
|
+
# NOT JSON-encoded strings. Excludes non-loggable fields like encrypted fields
|
12
|
+
# for security.
|
13
13
|
#
|
14
|
-
# @return [Hash] Hash with field names as keys and
|
15
|
-
# safe for external exposure
|
14
|
+
# @return [Hash] Hash with field names as string keys and Ruby values
|
16
15
|
#
|
17
16
|
# @example Converting an object to hash format for API response
|
18
17
|
# user = User.new(name: "John", email: "john@example.com", age: 30)
|
19
18
|
# user.to_h
|
20
|
-
# # => {"name"=>"John", "email"=>"john@example.com", "age"=>
|
21
|
-
# #
|
19
|
+
# # => {"name"=>"John", "email"=>"john@example.com", "age"=>30}
|
20
|
+
# # Note: Returns actual Ruby types, not JSON strings
|
22
21
|
#
|
23
|
-
# @note Only loggable fields are included
|
24
|
-
# @note
|
22
|
+
# @note Only loggable fields are included. Encrypted fields are excluded.
|
23
|
+
# @note Nil values are excluded from the returned hash (storage optimization)
|
25
24
|
#
|
26
25
|
def to_h
|
27
26
|
self.class.persistent_fields.each_with_object({}) do |field, hsh|
|
@@ -30,40 +29,45 @@ module Familia
|
|
30
29
|
# Security: Skip non-loggable fields (e.g., encrypted fields)
|
31
30
|
next unless field_type.loggable
|
32
31
|
|
33
|
-
|
34
|
-
val
|
35
|
-
prepared = serialize_value(val)
|
36
|
-
Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
|
32
|
+
val = send(field_type.method_name)
|
33
|
+
Familia.ld " [to_h] field: #{field} val: #{val.class}"
|
37
34
|
|
38
|
-
#
|
39
|
-
#
|
40
|
-
hsh[field.to_s] =
|
35
|
+
# Use string key for external API compatibility
|
36
|
+
# Return Ruby values, not JSON-encoded strings
|
37
|
+
hsh[field.to_s] = val
|
41
38
|
end
|
42
39
|
end
|
43
40
|
|
44
41
|
# Converts the object's persistent fields to a hash for database storage.
|
45
42
|
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
43
|
+
# Returns JSON-encoded strings for ALL persistent field values, ready for
|
44
|
+
# Redis storage. Unlike to_h, this includes encrypted fields and serializes
|
45
|
+
# values using serialize_value (JSON encoding).
|
46
|
+
#
|
47
|
+
# @return [Hash] Hash with field names as string keys and JSON-encoded values
|
49
48
|
#
|
50
|
-
# @
|
51
|
-
#
|
49
|
+
# @example Internal storage preparation
|
50
|
+
# user = User.new(name: "John", age: 30)
|
51
|
+
# user.to_h_for_storage
|
52
|
+
# # => {"name"=>"\"John\"", "age"=>"30"}
|
53
|
+
# # Note: Strings are JSON-encoded with quotes
|
52
54
|
#
|
53
|
-
# @note
|
54
|
-
# @note
|
55
|
+
# @note This is an internal method used by commit_fields and hmset
|
56
|
+
# @note Nil values are excluded to optimize Redis storage
|
55
57
|
#
|
56
58
|
def to_h_for_storage
|
57
59
|
self.class.persistent_fields.each_with_object({}) do |field, hsh|
|
58
60
|
field_type = self.class.field_types[field]
|
59
|
-
|
60
|
-
val = send(method_name)
|
61
|
+
|
62
|
+
val = send(field_type.method_name)
|
61
63
|
prepared = serialize_value(val)
|
62
|
-
Familia.ld " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
|
63
64
|
|
64
|
-
|
65
|
+
if Familia.debug?
|
66
|
+
Familia.ld " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
|
67
|
+
end
|
68
|
+
|
65
69
|
# Use string key for database compatibility
|
66
|
-
hsh[field.to_s] = prepared
|
70
|
+
hsh[field.to_s] = prepared
|
67
71
|
end
|
68
72
|
end
|
69
73
|
|
@@ -84,7 +88,7 @@ module Familia
|
|
84
88
|
# methods to maintain data consistency across operations.
|
85
89
|
#
|
86
90
|
def to_a
|
87
|
-
self.class.persistent_fields.
|
91
|
+
self.class.persistent_fields.map do |field|
|
88
92
|
field_type = self.class.field_types[field]
|
89
93
|
|
90
94
|
# Security: Skip non-loggable fields (e.g., encrypted fields)
|
@@ -92,80 +96,94 @@ module Familia
|
|
92
96
|
|
93
97
|
method_name = field_type.method_name
|
94
98
|
val = send(method_name)
|
95
|
-
|
96
|
-
|
97
|
-
|
99
|
+
Familia.ld " [to_a] field: #{field} method: #{method_name} val: #{val.class}"
|
100
|
+
|
101
|
+
# Return actual Ruby values, including nil to maintain array positions
|
102
|
+
val
|
98
103
|
end
|
99
104
|
end
|
100
105
|
|
101
106
|
# Serializes a Ruby object for Valkey storage.
|
102
107
|
#
|
103
|
-
# Converts Ruby
|
104
|
-
#
|
105
|
-
#
|
108
|
+
# Converts ALL Ruby values (including strings) to JSON-encoded strings for
|
109
|
+
# type-safe storage. This ensures round-trip type preservation: the type you
|
110
|
+
# save is the type you get back.
|
106
111
|
#
|
107
112
|
# The serialization process:
|
108
|
-
# 1.
|
109
|
-
# 2.
|
110
|
-
# 3. Logs warnings when serialization fails completely
|
113
|
+
# 1. ConcealedStrings (encrypted values) → extract encrypted_value
|
114
|
+
# 2. ALL other types → JSON serialization (String, Integer, Boolean, Float, nil, Hash, Array)
|
111
115
|
#
|
112
116
|
# @param val [Object] The Ruby object to serialize for Valkey storage
|
113
117
|
#
|
114
|
-
# @return [String
|
115
|
-
#
|
118
|
+
# @return [String] JSON-encoded string representation
|
119
|
+
#
|
120
|
+
# @example Type preservation through JSON encoding
|
121
|
+
# serialize_value("007") # => "\"007\"" (JSON string)
|
122
|
+
# serialize_value(123) # => "123" (JSON number)
|
123
|
+
# serialize_value(true) # => "true" (JSON boolean)
|
124
|
+
# serialize_value({a: 1}) # => "{\"a\":1}" (JSON object)
|
116
125
|
#
|
117
|
-
# @
|
118
|
-
#
|
119
|
-
# serialize_value(42) # => "42"
|
120
|
-
# serialize_value({name: "John"}) # => '{"name":"John"}'
|
121
|
-
# serialize_value([1, 2, 3]) # => "[1,2,3]"
|
126
|
+
# @note Strings are JSON-encoded to prevent type coercion bugs where
|
127
|
+
# string "123" would be indistinguishable from integer 123 in storage
|
122
128
|
#
|
123
129
|
# @note This method integrates with Familia's type system and supports
|
124
130
|
# custom serialization methods when available on the object
|
125
131
|
#
|
126
|
-
# @see Familia.
|
132
|
+
# @see Familia.identifier_extractor For extracting identifiers from Familia objects
|
127
133
|
#
|
128
134
|
def serialize_value(val)
|
129
135
|
# Security: Handle ConcealedString safely - extract encrypted data for storage
|
130
136
|
return val.encrypted_value if val.respond_to?(:encrypted_value)
|
131
137
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
# use JSON serialization for complex types that need it.
|
136
|
-
if prepared.nil? && (val.is_a?(Hash) || val.is_a?(Array))
|
137
|
-
prepared = val.respond_to?(dump_method) ? val.send(dump_method) : Familia::JsonSerializer.dump(val)
|
138
|
-
end
|
139
|
-
|
140
|
-
# If both the distinguisher and dump_method return nil, log an error
|
141
|
-
Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}" if prepared.nil?
|
142
|
-
|
143
|
-
prepared
|
138
|
+
# ALWAYS write valid JSON for type preservation
|
139
|
+
# This includes strings, which get JSON-encoded with wrapping quotes
|
140
|
+
Familia::JsonSerializer.dump(val)
|
144
141
|
end
|
145
142
|
|
146
|
-
# Converts a
|
143
|
+
# Converts a Redis string value back to its original Ruby type
|
144
|
+
#
|
145
|
+
# This method deserializes JSON strings back to their original Ruby types
|
146
|
+
# (Integer, Boolean, Float, nil, Hash, Array). Plain strings that cannot
|
147
|
+
# be parsed as JSON are returned as-is.
|
147
148
|
#
|
148
|
-
# This
|
149
|
-
#
|
149
|
+
# This pairs with serialize_value which JSON-encodes all non-string values.
|
150
|
+
# The contract ensures type preservation across Redis storage:
|
151
|
+
# - Strings stored as-is → returned as-is
|
152
|
+
# - All other types JSON-encoded → JSON-decoded back to original type
|
150
153
|
#
|
151
|
-
# @param val [String] The string value from
|
152
|
-
# @param symbolize [Boolean] Whether to symbolize hash keys (default:
|
153
|
-
# @
|
154
|
+
# @param val [String] The string value from Redis to deserialize
|
155
|
+
# @param symbolize [Boolean] Whether to symbolize hash keys (default: false)
|
156
|
+
# @param field_name [Symbol, nil] Optional field name for better error context
|
157
|
+
# @return [Object] The deserialized value with original Ruby type, or the original string if not JSON
|
154
158
|
#
|
155
|
-
def deserialize_value(val, symbolize:
|
156
|
-
return
|
159
|
+
def deserialize_value(val, symbolize: false, field_name: nil)
|
160
|
+
return nil if val.nil? || val == ''
|
157
161
|
|
158
|
-
# Try to parse as JSON first for complex types
|
159
162
|
begin
|
160
|
-
|
161
|
-
# Only return parsed value if it's a complex type (Hash/Array)
|
162
|
-
# Simple values should remain as strings
|
163
|
-
return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
|
163
|
+
Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
|
164
164
|
rescue Familia::SerializerError
|
165
|
-
|
165
|
+
log_deserialization_issue(val, field_name)
|
166
|
+
val
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def log_deserialization_issue(val, field_name)
|
173
|
+
context = field_name ? "#{self.class}##{field_name}" : self.class.to_s
|
174
|
+
dbkey_info = respond_to?(:dbkey) ? dbkey : 'no dbkey'
|
175
|
+
|
176
|
+
msg = if looks_like_json?(val)
|
177
|
+
"Corrupted JSON in #{context}: #{val.inspect} (#{dbkey_info})"
|
178
|
+
else
|
179
|
+
"Legacy plain string in #{context}: #{val.inspect} (#{dbkey_info})"
|
166
180
|
end
|
167
181
|
|
168
|
-
|
182
|
+
Familia.le(msg)
|
183
|
+
end
|
184
|
+
|
185
|
+
def looks_like_json?(val)
|
186
|
+
val.start_with?('{', '[', '"') || %w[true false null].include?(val)
|
169
187
|
end
|
170
188
|
end
|
171
189
|
end
|
data/lib/familia/horreum.rb
CHANGED
@@ -299,7 +299,9 @@ module Familia
|
|
299
299
|
|
300
300
|
def initialize_with_keyword_args_deserialize_value(**fields)
|
301
301
|
# Deserialize Database string values back to their original types
|
302
|
-
deserialized_fields = fields.
|
302
|
+
deserialized_fields = fields.each_with_object({}) do |(field_name, value), hsh|
|
303
|
+
hsh[field_name] = deserialize_value(value, field_name: field_name)
|
304
|
+
end
|
303
305
|
initialize_with_keyword_args(**deserialized_fields)
|
304
306
|
end
|
305
307
|
|
@@ -308,7 +310,7 @@ module Familia
|
|
308
310
|
#
|
309
311
|
# This method is part of horreum.rb rather than serialization.rb because it
|
310
312
|
# operates solely on the provided values and doesn't query Database or other
|
311
|
-
# external sources. That's why it's called "
|
313
|
+
# external sources. That's why it's called "naive" refresh: it assumes
|
312
314
|
# the provided values are correct and updates the object accordingly.
|
313
315
|
#
|
314
316
|
# @see #refresh!
|
@@ -316,8 +318,8 @@ module Familia
|
|
316
318
|
# @param fields [Hash] A hash of field names and their new values to update
|
317
319
|
# the object with.
|
318
320
|
# @return [Array] The list of field names that were updated.
|
319
|
-
def
|
320
|
-
Familia.ld "[
|
321
|
+
def naive_refresh(**fields)
|
322
|
+
Familia.ld "[naive_refresh] #{self.class} #{dbkey} #{fields.keys}"
|
321
323
|
initialize_with_keyword_args_deserialize_value(**fields)
|
322
324
|
end
|
323
325
|
|
@@ -401,8 +403,10 @@ module Familia
|
|
401
403
|
self.class.fields.filter_map do |field|
|
402
404
|
# Database will give us field names as strings back, but internally
|
403
405
|
# we use symbols. So we check for both.
|
404
|
-
|
405
|
-
|
406
|
+
# Use fetch with default to avoid || operator which skips false values
|
407
|
+
value = fields.fetch(field.to_sym) { fields[field.to_s] }
|
408
|
+
# Check for nil explicitly to allow false and 0 values
|
409
|
+
unless value.nil?
|
406
410
|
# Use the mapped method name, not the field name
|
407
411
|
method_name = self.class.field_method_map[field] || field
|
408
412
|
send(:"#{method_name}=", value)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# lib/familia/identifier_extractor.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
# IdentifierExtractor - Extracts identifiers from Familia objects for storage
|
5
|
+
#
|
6
|
+
# This module provides a focused mechanism for converting object references
|
7
|
+
# into Redis-storable strings. It handles two primary cases:
|
8
|
+
#
|
9
|
+
# 1. Class references: Customer → "Customer"
|
10
|
+
# 2. Familia::Base instances: customer_obj → customer_obj.identifier
|
11
|
+
#
|
12
|
+
# This is primarily used by DataType serialization when storing object
|
13
|
+
# references in Redis data structures (lists, sets, zsets). It extracts
|
14
|
+
# the identifier rather than serializing the entire object.
|
15
|
+
#
|
16
|
+
# @example With class_zset
|
17
|
+
# class Customer < Familia::Horreum
|
18
|
+
# class_zset :instances, class: self
|
19
|
+
# end
|
20
|
+
# # When adding: Customer.instances.add(customer_obj)
|
21
|
+
# # Stores: customer_obj.identifier (e.g., "customer_123")
|
22
|
+
#
|
23
|
+
module IdentifierExtractor
|
24
|
+
# Extracts a Redis-storable identifier from a Familia object or class.
|
25
|
+
#
|
26
|
+
# @param value [Object] The value to extract an identifier from
|
27
|
+
# @return [String] The extracted identifier or class name
|
28
|
+
# @raise [Familia::NotDistinguishableError] If value is not a Class or Familia::Base
|
29
|
+
#
|
30
|
+
def identifier_extractor(value, strict_values: true)
|
31
|
+
case value
|
32
|
+
when ::Symbol, ::String, ::Integer, ::Float
|
33
|
+
Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'simple_value' if Familia.debug?
|
34
|
+
# DataTypes (lists, sets, zsets) can store simple values directly
|
35
|
+
# Convert to string for Redis storage
|
36
|
+
value.to_s
|
37
|
+
|
38
|
+
when Class
|
39
|
+
Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'class' if Familia.debug?
|
40
|
+
value.name
|
41
|
+
|
42
|
+
when Familia::Base
|
43
|
+
Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'base_instance' if Familia.debug?
|
44
|
+
value.identifier
|
45
|
+
|
46
|
+
else
|
47
|
+
# Check if value's class inherits from Familia::Base
|
48
|
+
if value.class.ancestors.member?(Familia::Base)
|
49
|
+
Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'base_ancestor' if Familia.debug?
|
50
|
+
value.identifier
|
51
|
+
else
|
52
|
+
Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'error' if Familia.debug?
|
53
|
+
raise Familia::NotDistinguishableError, value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
extend IdentifierExtractor
|
60
|
+
end
|