familia 2.0.0.pre16 → 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/.github/workflows/ci.yml +2 -2
- data/.github/workflows/{code-smellage.yml → code-smells.yml} +3 -63
- data/.gitignore +2 -0
- data/.rubocop.yml +6 -0
- data/CHANGELOG.rst +82 -0
- data/CLAUDE.md +47 -2
- data/Gemfile.lock +1 -1
- data/README.md +13 -0
- data/bin/irb +1 -1
- data/docs/archive/FAMILIA_TECHNICAL.md +1 -1
- data/docs/guides/core-field-system.md +48 -26
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/overview.md +2 -2
- data/docs/qodo-merge-compliance.md +96 -0
- data/docs/reference/api-technical.md +1 -1
- data/examples/encrypted_fields.rb +1 -1
- data/examples/safe_dump.rb +1 -1
- data/lib/familia/base.rb +6 -6
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection.rb +1 -1
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/connection.rb +83 -0
- 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/settings.rb +96 -0
- data/lib/familia/data_type/types/hashkey.rb +2 -1
- data/lib/familia/data_type/types/sorted_set.rb +113 -10
- data/lib/familia/data_type/types/stringkey.rb +0 -4
- data/lib/familia/data_type.rb +8 -195
- 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/encrypted_fields.rb +5 -2
- data/lib/familia/features/external_identifier.rb +49 -8
- data/lib/familia/features/object_identifier.rb +84 -12
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +45 -26
- data/lib/familia/features/relationships/indexing.rb +7 -1
- data/lib/familia/features/relationships/participation/participant_methods.rb +6 -2
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/features/transient_fields.rb +7 -2
- data/lib/familia/features.rb +6 -1
- data/lib/familia/field_type.rb +0 -18
- data/lib/familia/horreum/{core/connection.rb → connection.rb} +21 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +1 -1
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +102 -56
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +18 -15
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +73 -170
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +22 -2
- data/lib/familia/horreum/serialization.rb +190 -0
- data/lib/familia/horreum.rb +39 -14
- 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/field_groups_try.rb +244 -0
- 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 +16 -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 +3 -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 +3 -3
- 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 +12 -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 +6 -6
- 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 +2 -2
- 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 +15 -7
- data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +1 -1
- 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 +5 -5
- data/try/{core → unit/core}/errors_try.rb +4 -4
- 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 +2 -2
- 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 +3 -3
- 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/unit/data_types/sorted_set_zadd_options_try.rb +625 -0
- 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/unit/horreum/auto_indexing_on_save_try.rb +212 -0
- 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 +3 -1
- data/try/unit/horreum/defensive_initialization_try.rb +86 -0
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +3 -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 +3 -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
- data/try/valkey.conf +26 -0
- metadata +149 -132
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/lib/familia/horreum/{shared/settings.rb → settings.rb} +0 -0
- /data/lib/familia/horreum/{core/utils.rb → utils.rb} +0 -0
- /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_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
@@ -1,4 +1,4 @@
|
|
1
|
-
# lib/familia/horreum/
|
1
|
+
# lib/familia/horreum/persistence.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
# Familia::Horreum
|
@@ -34,7 +34,7 @@ module Familia
|
|
34
34
|
# Serialization - Instance-level methods for object persistence and retrieval
|
35
35
|
# Handles conversion between Ruby objects and Valkey hash storage
|
36
36
|
#
|
37
|
-
module
|
37
|
+
module Persistence
|
38
38
|
# Persists the object to Valkey storage with automatic timestamping.
|
39
39
|
#
|
40
40
|
# Saves the current object state to Valkey storage, automatically setting
|
@@ -68,11 +68,18 @@ module Familia
|
|
68
68
|
self.updated = Familia.now.to_i if respond_to?(:updated)
|
69
69
|
|
70
70
|
# Commit our tale to the Database chronicles
|
71
|
-
#
|
71
|
+
# Wrap in transaction for atomicity between save and indexing
|
72
72
|
ret = commit_fields(update_expiration: update_expiration)
|
73
73
|
|
74
|
-
#
|
75
|
-
|
74
|
+
# Auto-index for class-level indexes after successful save
|
75
|
+
# Use transaction to ensure atomicity with the save operation
|
76
|
+
if ret
|
77
|
+
transaction do |conn|
|
78
|
+
auto_update_class_indexes
|
79
|
+
# Add to class-level instances collection after successful save
|
80
|
+
self.class.instances.add(identifier, Familia.now) if self.class.respond_to?(:instances)
|
81
|
+
end
|
82
|
+
end
|
76
83
|
|
77
84
|
Familia.ld "[save] #{self.class} #{dbkey} #{ret} (update_expiration: #{update_expiration})"
|
78
85
|
|
@@ -128,7 +135,7 @@ module Familia
|
|
128
135
|
Familia.ld "[save_if_not_exists]: #{self.class} #{identifier_field}=#{identifier}"
|
129
136
|
Familia.trace :SAVE_IF_NOT_EXISTS, nil, uri if Familia.debug?
|
130
137
|
|
131
|
-
dbclient.watch(dbkey) do
|
138
|
+
success = dbclient.watch(dbkey) do
|
132
139
|
if dbclient.exists(dbkey).positive?
|
133
140
|
dbclient.unwatch
|
134
141
|
raise Familia::RecordExistsError, dbkey
|
@@ -140,6 +147,16 @@ module Familia
|
|
140
147
|
|
141
148
|
result.is_a?(Array) # transaction succeeded
|
142
149
|
end
|
150
|
+
|
151
|
+
# Auto-index for class-level indexes after successful save
|
152
|
+
# Use transaction to ensure atomicity with the save operation
|
153
|
+
if success
|
154
|
+
transaction do |conn|
|
155
|
+
auto_update_class_indexes
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
success
|
143
160
|
end
|
144
161
|
|
145
162
|
# Commits object fields to the DB storage.
|
@@ -337,7 +354,7 @@ module Familia
|
|
337
354
|
# their uninitialized state during refresh operations
|
338
355
|
reset_transient_fields!
|
339
356
|
|
340
|
-
|
357
|
+
naive_refresh(**fields)
|
341
358
|
end
|
342
359
|
|
343
360
|
# Refreshes object state from the DB and returns self for method chaining.
|
@@ -361,169 +378,6 @@ module Familia
|
|
361
378
|
self
|
362
379
|
end
|
363
380
|
|
364
|
-
# Converts the object's persistent fields to a hash for external use.
|
365
|
-
#
|
366
|
-
# Serializes persistent field values for external consumption (APIs, logs),
|
367
|
-
# excluding non-loggable fields like encrypted fields for security.
|
368
|
-
# Only non-nil values are included in the resulting hash.
|
369
|
-
#
|
370
|
-
# @return [Hash] Hash with field names as keys and serialized values
|
371
|
-
# safe for external exposure
|
372
|
-
#
|
373
|
-
# @example Converting an object to hash format for API response
|
374
|
-
# user = User.new(name: "John", email: "john@example.com", age: 30)
|
375
|
-
# user.to_h
|
376
|
-
# # => {"name"=>"John", "email"=>"john@example.com", "age"=>"30"}
|
377
|
-
# # encrypted fields are excluded for security
|
378
|
-
#
|
379
|
-
# @note Only loggable fields are included for security
|
380
|
-
# @note Only fields with non-nil values are included
|
381
|
-
#
|
382
|
-
def to_h
|
383
|
-
self.class.persistent_fields.each_with_object({}) do |field, hsh|
|
384
|
-
field_type = self.class.field_types[field]
|
385
|
-
|
386
|
-
# Security: Skip non-loggable fields (e.g., encrypted fields)
|
387
|
-
next unless field_type.loggable
|
388
|
-
|
389
|
-
method_name = field_type.method_name
|
390
|
-
val = send(method_name)
|
391
|
-
prepared = serialize_value(val)
|
392
|
-
Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
|
393
|
-
|
394
|
-
# Only include non-nil values in the hash for Valkey
|
395
|
-
# Use string key for database compatibility
|
396
|
-
hsh[field.to_s] = prepared unless prepared.nil?
|
397
|
-
end
|
398
|
-
end
|
399
|
-
|
400
|
-
# Converts the object's persistent fields to a hash for database storage.
|
401
|
-
#
|
402
|
-
# Serializes ALL persistent field values for database storage, including
|
403
|
-
# encrypted fields. This is used internally by commit_fields and other
|
404
|
-
# persistence operations.
|
405
|
-
#
|
406
|
-
# @return [Hash] Hash with field names as keys and serialized values
|
407
|
-
# ready for database storage
|
408
|
-
#
|
409
|
-
# @note Includes ALL persistent fields, including encrypted fields
|
410
|
-
# @note Only fields with non-nil values are included for storage efficiency
|
411
|
-
#
|
412
|
-
def to_h_for_storage
|
413
|
-
self.class.persistent_fields.each_with_object({}) do |field, hsh|
|
414
|
-
field_type = self.class.field_types[field]
|
415
|
-
method_name = field_type.method_name
|
416
|
-
val = send(method_name)
|
417
|
-
prepared = serialize_value(val)
|
418
|
-
Familia.ld " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
|
419
|
-
|
420
|
-
# Only include non-nil values in the hash for Valkey
|
421
|
-
# Use string key for database compatibility
|
422
|
-
hsh[field.to_s] = prepared unless prepared.nil?
|
423
|
-
end
|
424
|
-
end
|
425
|
-
|
426
|
-
# Converts the object's persistent fields to an array.
|
427
|
-
#
|
428
|
-
# Serializes all persistent field values in field definition order,
|
429
|
-
# preparing them for Valkey storage. Each value is processed through
|
430
|
-
# the serialization pipeline to ensure Valkey compatibility.
|
431
|
-
#
|
432
|
-
# @return [Array] Array of serialized field values in field order
|
433
|
-
#
|
434
|
-
# @example Converting an object to array format
|
435
|
-
# user = User.new(name: "John", email: "john@example.com", age: 30)
|
436
|
-
# user.to_a
|
437
|
-
# # => ["John", "john@example.com", "30"]
|
438
|
-
#
|
439
|
-
# @note Values are serialized using the same process as other persistence
|
440
|
-
# methods to maintain data consistency across operations.
|
441
|
-
#
|
442
|
-
def to_a
|
443
|
-
self.class.persistent_fields.filter_map do |field|
|
444
|
-
field_type = self.class.field_types[field]
|
445
|
-
|
446
|
-
# Security: Skip non-loggable fields (e.g., encrypted fields)
|
447
|
-
next unless field_type.loggable
|
448
|
-
|
449
|
-
method_name = field_type.method_name
|
450
|
-
val = send(method_name)
|
451
|
-
prepared = serialize_value(val)
|
452
|
-
Familia.ld " [to_a] field: #{field} method: #{method_name} val: #{val.class} prepared: #{prepared.class}"
|
453
|
-
prepared
|
454
|
-
end
|
455
|
-
end
|
456
|
-
|
457
|
-
# Serializes a Ruby object for Valkey storage.
|
458
|
-
#
|
459
|
-
# Converts Ruby objects into the DB-compatible string representations using
|
460
|
-
# the Familia distinguisher for type coercion. Falls back to JSON serialization
|
461
|
-
# for complex types (Hash, Array) when the primary distinguisher returns nil.
|
462
|
-
#
|
463
|
-
# The serialization process:
|
464
|
-
# 1. Attempts conversion using Familia.distinguisher with relaxed type checking
|
465
|
-
# 2. For Hash/Array types that return nil, tries custom dump_method or Familia::JsonSerializer.dump
|
466
|
-
# 3. Logs warnings when serialization fails completely
|
467
|
-
#
|
468
|
-
# @param val [Object] The Ruby object to serialize for Valkey storage
|
469
|
-
#
|
470
|
-
# @return [String, nil] The serialized value ready for Valkey storage, or nil
|
471
|
-
# if serialization failed
|
472
|
-
#
|
473
|
-
# @example Serializing different data types
|
474
|
-
# serialize_value("hello") # => "hello"
|
475
|
-
# serialize_value(42) # => "42"
|
476
|
-
# serialize_value({name: "John"}) # => '{"name":"John"}'
|
477
|
-
# serialize_value([1, 2, 3]) # => "[1,2,3]"
|
478
|
-
#
|
479
|
-
# @note This method integrates with Familia's type system and supports
|
480
|
-
# custom serialization methods when available on the object
|
481
|
-
#
|
482
|
-
# @see Familia.distinguisher The primary serialization mechanism
|
483
|
-
#
|
484
|
-
def serialize_value(val)
|
485
|
-
# Security: Handle ConcealedString safely - extract encrypted data for storage
|
486
|
-
return val.encrypted_value if val.respond_to?(:encrypted_value)
|
487
|
-
|
488
|
-
prepared = Familia.distinguisher(val, strict_values: false)
|
489
|
-
|
490
|
-
# If the distinguisher returns nil, try using the dump_method but only
|
491
|
-
# use JSON serialization for complex types that need it.
|
492
|
-
if prepared.nil? && (val.is_a?(Hash) || val.is_a?(Array))
|
493
|
-
prepared = val.respond_to?(dump_method) ? val.send(dump_method) : Familia::JsonSerializer.dump(val)
|
494
|
-
end
|
495
|
-
|
496
|
-
# If both the distinguisher and dump_method return nil, log an error
|
497
|
-
Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}" if prepared.nil?
|
498
|
-
|
499
|
-
prepared
|
500
|
-
end
|
501
|
-
|
502
|
-
# Converts a Database string value back to its original Ruby type
|
503
|
-
#
|
504
|
-
# This method attempts to deserialize JSON strings back to their original
|
505
|
-
# Hash or Array types. Simple string values are returned as-is.
|
506
|
-
#
|
507
|
-
# @param val [String] The string value from Database to deserialize
|
508
|
-
# @param symbolize [Boolean] Whether to symbolize hash keys (default: true for compatibility)
|
509
|
-
# @return [Object] The deserialized value (Hash, Array, or original string)
|
510
|
-
#
|
511
|
-
def deserialize_value(val, symbolize: true)
|
512
|
-
return val if val.nil? || val == ''
|
513
|
-
|
514
|
-
# Try to parse as JSON first for complex types
|
515
|
-
begin
|
516
|
-
parsed = Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
|
517
|
-
# Only return parsed value if it's a complex type (Hash/Array)
|
518
|
-
# Simple values should remain as strings
|
519
|
-
return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
|
520
|
-
rescue Familia::SerializerError
|
521
|
-
# Not valid JSON, return as-is
|
522
|
-
end
|
523
|
-
|
524
|
-
val
|
525
|
-
end
|
526
|
-
|
527
381
|
private
|
528
382
|
|
529
383
|
# Reset all transient fields to nil
|
@@ -547,6 +401,55 @@ module Familia
|
|
547
401
|
Familia.ld "[reset_transient_fields!] Reset #{field_name} to nil"
|
548
402
|
end
|
549
403
|
end
|
404
|
+
|
405
|
+
# Automatically update class-level indexes after save
|
406
|
+
#
|
407
|
+
# Iterates through class-level indexing relationships and calls their
|
408
|
+
# corresponding add_to_class_* methods to populate indexes. Only processes
|
409
|
+
# class-level indexes (where target_class == self.class), skipping
|
410
|
+
# instance-scoped indexes which require parent context.
|
411
|
+
#
|
412
|
+
# Uses idempotent Redis commands (HSET for unique_index) so repeated calls
|
413
|
+
# are safe and have negligible performance overhead. Note that multi_index
|
414
|
+
# always requires within: parameter, so only unique_index benefits from this.
|
415
|
+
#
|
416
|
+
# @return [void]
|
417
|
+
#
|
418
|
+
# @example Automatic indexing on save
|
419
|
+
# class Customer < Familia::Horreum
|
420
|
+
# feature :relationships
|
421
|
+
# unique_index :email, :email_lookup
|
422
|
+
# end
|
423
|
+
#
|
424
|
+
# customer = Customer.new(email: 'test@example.com')
|
425
|
+
# customer.save # Automatically calls add_to_class_email_lookup
|
426
|
+
#
|
427
|
+
# @note Only class-level unique_index declarations auto-populate.
|
428
|
+
# Instance-scoped indexes (with within:) require manual population:
|
429
|
+
# employee.add_to_company_badge_index(company)
|
430
|
+
#
|
431
|
+
# @see Familia::Features::Relationships::Indexing For index declaration details
|
432
|
+
#
|
433
|
+
def auto_update_class_indexes
|
434
|
+
return unless self.class.respond_to?(:indexing_relationships)
|
435
|
+
|
436
|
+
self.class.indexing_relationships.each do |rel|
|
437
|
+
# Skip instance-scoped indexes (require parent context)
|
438
|
+
# Instance-scoped indexes must be manually populated because they need
|
439
|
+
# the parent object reference (e.g., employee.add_to_company_badge_index(company))
|
440
|
+
unless rel.target_class == self.class
|
441
|
+
Familia.ld <<~LOG_MESSAGE
|
442
|
+
[auto_update_class_indexes] Skipping #{rel.index_name} (requires parent context)
|
443
|
+
LOG_MESSAGE
|
444
|
+
next
|
445
|
+
end
|
446
|
+
|
447
|
+
# Call the existing add_to_class_* methods
|
448
|
+
add_method = :"add_to_class_#{rel.index_name}"
|
449
|
+
send(add_method) if respond_to?(add_method)
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
550
453
|
end
|
551
454
|
end
|
552
455
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# lib/familia/horreum/
|
1
|
+
# lib/familia/horreum/related_fields.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
|
@@ -170,7 +170,27 @@ module Familia
|
|
170
170
|
|
171
171
|
related_fields[name] = RelatedFieldDefinition.new(name, klass, opts)
|
172
172
|
|
173
|
-
|
173
|
+
# Create lazy-initializing accessor that calls initialize_relatives if needed
|
174
|
+
define_method name do
|
175
|
+
ivar = :"@#{name}"
|
176
|
+
value = instance_variable_get(ivar)
|
177
|
+
|
178
|
+
# If nil and we haven't initialized relatives, do it now
|
179
|
+
# Check singleton class to avoid polluting instance variables
|
180
|
+
if value.nil? && !singleton_class.instance_variable_defined?(:"@relatives_initialized")
|
181
|
+
initialize_relatives
|
182
|
+
value = instance_variable_get(ivar)
|
183
|
+
end
|
184
|
+
|
185
|
+
# If still nil after lazy initialization attempt, raise helpful error
|
186
|
+
# Only raise if we tried to initialize but it's still nil
|
187
|
+
if value.nil? && singleton_class.instance_variable_defined?(:"@relatives_initialized")
|
188
|
+
raise "#{self.class}##{name} is nil. Did you override initialize without calling super? " \
|
189
|
+
"(Field is nil after initialization attempt)"
|
190
|
+
end
|
191
|
+
|
192
|
+
value
|
193
|
+
end
|
174
194
|
|
175
195
|
define_method :"#{name}=" do |val|
|
176
196
|
send(name).replace val
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# lib/familia/horreum/serialization.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
class Horreum
|
5
|
+
# Serialization - Instance-level methods for object serialization
|
6
|
+
# Handles conversion between Ruby objects and Valkey hash storage
|
7
|
+
module Serialization
|
8
|
+
# Converts the object's persistent fields to a hash for external use.
|
9
|
+
#
|
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
|
+
#
|
14
|
+
# @return [Hash] Hash with field names as string keys and Ruby values
|
15
|
+
#
|
16
|
+
# @example Converting an object to hash format for API response
|
17
|
+
# user = User.new(name: "John", email: "john@example.com", age: 30)
|
18
|
+
# user.to_h
|
19
|
+
# # => {"name"=>"John", "email"=>"john@example.com", "age"=>30}
|
20
|
+
# # Note: Returns actual Ruby types, not JSON strings
|
21
|
+
#
|
22
|
+
# @note Only loggable fields are included. Encrypted fields are excluded.
|
23
|
+
# @note Nil values are excluded from the returned hash (storage optimization)
|
24
|
+
#
|
25
|
+
def to_h
|
26
|
+
self.class.persistent_fields.each_with_object({}) do |field, hsh|
|
27
|
+
field_type = self.class.field_types[field]
|
28
|
+
|
29
|
+
# Security: Skip non-loggable fields (e.g., encrypted fields)
|
30
|
+
next unless field_type.loggable
|
31
|
+
|
32
|
+
val = send(field_type.method_name)
|
33
|
+
Familia.ld " [to_h] field: #{field} val: #{val.class}"
|
34
|
+
|
35
|
+
# Use string key for external API compatibility
|
36
|
+
# Return Ruby values, not JSON-encoded strings
|
37
|
+
hsh[field.to_s] = val
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Converts the object's persistent fields to a hash for database storage.
|
42
|
+
#
|
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
|
48
|
+
#
|
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
|
54
|
+
#
|
55
|
+
# @note This is an internal method used by commit_fields and hmset
|
56
|
+
# @note Nil values are excluded to optimize Redis storage
|
57
|
+
#
|
58
|
+
def to_h_for_storage
|
59
|
+
self.class.persistent_fields.each_with_object({}) do |field, hsh|
|
60
|
+
field_type = self.class.field_types[field]
|
61
|
+
|
62
|
+
val = send(field_type.method_name)
|
63
|
+
prepared = serialize_value(val)
|
64
|
+
|
65
|
+
if Familia.debug?
|
66
|
+
Familia.ld " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Use string key for database compatibility
|
70
|
+
hsh[field.to_s] = prepared
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Converts the object's persistent fields to an array.
|
75
|
+
#
|
76
|
+
# Serializes all persistent field values in field definition order,
|
77
|
+
# preparing them for Valkey storage. Each value is processed through
|
78
|
+
# the serialization pipeline to ensure Valkey compatibility.
|
79
|
+
#
|
80
|
+
# @return [Array] Array of serialized field values in field order
|
81
|
+
#
|
82
|
+
# @example Converting an object to array format
|
83
|
+
# user = User.new(name: "John", email: "john@example.com", age: 30)
|
84
|
+
# user.to_a
|
85
|
+
# # => ["John", "john@example.com", "30"]
|
86
|
+
#
|
87
|
+
# @note Values are serialized using the same process as other persistence
|
88
|
+
# methods to maintain data consistency across operations.
|
89
|
+
#
|
90
|
+
def to_a
|
91
|
+
self.class.persistent_fields.map do |field|
|
92
|
+
field_type = self.class.field_types[field]
|
93
|
+
|
94
|
+
# Security: Skip non-loggable fields (e.g., encrypted fields)
|
95
|
+
next unless field_type.loggable
|
96
|
+
|
97
|
+
method_name = field_type.method_name
|
98
|
+
val = send(method_name)
|
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
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Serializes a Ruby object for Valkey storage.
|
107
|
+
#
|
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.
|
111
|
+
#
|
112
|
+
# The serialization process:
|
113
|
+
# 1. ConcealedStrings (encrypted values) → extract encrypted_value
|
114
|
+
# 2. ALL other types → JSON serialization (String, Integer, Boolean, Float, nil, Hash, Array)
|
115
|
+
#
|
116
|
+
# @param val [Object] The Ruby object to serialize for Valkey storage
|
117
|
+
#
|
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)
|
125
|
+
#
|
126
|
+
# @note Strings are JSON-encoded to prevent type coercion bugs where
|
127
|
+
# string "123" would be indistinguishable from integer 123 in storage
|
128
|
+
#
|
129
|
+
# @note This method integrates with Familia's type system and supports
|
130
|
+
# custom serialization methods when available on the object
|
131
|
+
#
|
132
|
+
# @see Familia.identifier_extractor For extracting identifiers from Familia objects
|
133
|
+
#
|
134
|
+
def serialize_value(val)
|
135
|
+
# Security: Handle ConcealedString safely - extract encrypted data for storage
|
136
|
+
return val.encrypted_value if val.respond_to?(:encrypted_value)
|
137
|
+
|
138
|
+
# ALWAYS write valid JSON for type preservation
|
139
|
+
# This includes strings, which get JSON-encoded with wrapping quotes
|
140
|
+
Familia::JsonSerializer.dump(val)
|
141
|
+
end
|
142
|
+
|
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.
|
148
|
+
#
|
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
|
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
|
158
|
+
#
|
159
|
+
def deserialize_value(val, symbolize: false, field_name: nil)
|
160
|
+
return nil if val.nil? || val == ''
|
161
|
+
|
162
|
+
begin
|
163
|
+
Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
|
164
|
+
rescue Familia::SerializerError
|
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})"
|
180
|
+
end
|
181
|
+
|
182
|
+
Familia.le(msg)
|
183
|
+
end
|
184
|
+
|
185
|
+
def looks_like_json?(val)
|
186
|
+
val.start_with?('{', '[', '"') || %w[true false null].include?(val)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
data/lib/familia/horreum.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
# lib/familia/horreum.rb
|
2
2
|
|
3
|
-
require_relative 'horreum/
|
4
|
-
require_relative 'horreum/
|
5
|
-
require_relative 'horreum/
|
6
|
-
require_relative 'horreum/
|
3
|
+
require_relative 'horreum/settings'
|
4
|
+
require_relative 'horreum/connection'
|
5
|
+
require_relative 'horreum/database_commands'
|
6
|
+
require_relative 'horreum/related_fields'
|
7
|
+
require_relative 'horreum/definition'
|
8
|
+
require_relative 'horreum/management'
|
9
|
+
require_relative 'horreum/persistence'
|
10
|
+
require_relative 'horreum/serialization'
|
11
|
+
require_relative 'horreum/utils'
|
7
12
|
|
8
13
|
module Familia
|
9
14
|
#
|
@@ -44,8 +49,12 @@ module Familia
|
|
44
49
|
#
|
45
50
|
class Horreum
|
46
51
|
include Familia::Base
|
47
|
-
include Familia::Horreum::
|
52
|
+
include Familia::Horreum::Persistence
|
53
|
+
include Familia::Horreum::Serialization
|
54
|
+
include Familia::Horreum::Connection
|
55
|
+
include Familia::Horreum::DatabaseCommands
|
48
56
|
include Familia::Horreum::Settings
|
57
|
+
include Familia::Horreum::Utils
|
49
58
|
|
50
59
|
using Familia::Refinements::TimeLiterals
|
51
60
|
|
@@ -223,6 +232,15 @@ module Familia
|
|
223
232
|
init
|
224
233
|
end
|
225
234
|
|
235
|
+
# Override this method in subclasses for custom initialization logic.
|
236
|
+
# This is called AFTER fields are set and relatives are initialized.
|
237
|
+
#
|
238
|
+
# DO NOT override initialize() - use this init() hook instead.
|
239
|
+
#
|
240
|
+
# Example:
|
241
|
+
# def init(name = nil)
|
242
|
+
# @name = name || SecureRandom.hex(4)
|
243
|
+
# end
|
226
244
|
def init(*args, **kwargs)
|
227
245
|
# Default no-op
|
228
246
|
end
|
@@ -233,6 +251,8 @@ module Familia
|
|
233
251
|
# This needs to be called in the initialize method.
|
234
252
|
#
|
235
253
|
def initialize_relatives
|
254
|
+
# Store initialization flag on singleton class to avoid polluting instance variables
|
255
|
+
return if singleton_class.instance_variable_defined?(:"@relatives_initialized")
|
236
256
|
# Generate instances of each DataType. These need to be
|
237
257
|
# unique for each instance of this class so they can piggyback
|
238
258
|
# on the specifc index of this instance.
|
@@ -272,11 +292,16 @@ module Familia
|
|
272
292
|
# e.g. customer.name #=> `#<Familia::HashKey:0x0000...>`
|
273
293
|
instance_variable_set :"@#{name}", related_object
|
274
294
|
end
|
295
|
+
|
296
|
+
# Mark relatives as initialized on singleton class to avoid polluting instance variables
|
297
|
+
singleton_class.instance_variable_set(:"@relatives_initialized", true)
|
275
298
|
end
|
276
299
|
|
277
300
|
def initialize_with_keyword_args_deserialize_value(**fields)
|
278
301
|
# Deserialize Database string values back to their original types
|
279
|
-
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
|
280
305
|
initialize_with_keyword_args(**deserialized_fields)
|
281
306
|
end
|
282
307
|
|
@@ -285,7 +310,7 @@ module Familia
|
|
285
310
|
#
|
286
311
|
# This method is part of horreum.rb rather than serialization.rb because it
|
287
312
|
# operates solely on the provided values and doesn't query Database or other
|
288
|
-
# external sources. That's why it's called "
|
313
|
+
# external sources. That's why it's called "naive" refresh: it assumes
|
289
314
|
# the provided values are correct and updates the object accordingly.
|
290
315
|
#
|
291
316
|
# @see #refresh!
|
@@ -293,8 +318,8 @@ module Familia
|
|
293
318
|
# @param fields [Hash] A hash of field names and their new values to update
|
294
319
|
# the object with.
|
295
320
|
# @return [Array] The list of field names that were updated.
|
296
|
-
def
|
297
|
-
Familia.ld "[
|
321
|
+
def naive_refresh(**fields)
|
322
|
+
Familia.ld "[naive_refresh] #{self.class} #{dbkey} #{fields.keys}"
|
298
323
|
initialize_with_keyword_args_deserialize_value(**fields)
|
299
324
|
end
|
300
325
|
|
@@ -311,7 +336,7 @@ module Familia
|
|
311
336
|
send(definition)
|
312
337
|
when Proc
|
313
338
|
definition.call(self)
|
314
|
-
|
339
|
+
end
|
315
340
|
|
316
341
|
# Return nil for unpopulated identifiers (like unsaved ActiveRecord objects)
|
317
342
|
# Only raise errors when the identifier is actually needed for db operations
|
@@ -331,7 +356,6 @@ module Familia
|
|
331
356
|
# @return [Redis] the Database connection instance.
|
332
357
|
#
|
333
358
|
|
334
|
-
|
335
359
|
def generate_id
|
336
360
|
@objid ||= Familia.generate_id
|
337
361
|
end
|
@@ -379,8 +403,10 @@ module Familia
|
|
379
403
|
self.class.fields.filter_map do |field|
|
380
404
|
# Database will give us field names as strings back, but internally
|
381
405
|
# we use symbols. So we check for both.
|
382
|
-
|
383
|
-
|
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?
|
384
410
|
# Use the mapped method name, not the field name
|
385
411
|
method_name = self.class.field_method_map[field] || field
|
386
412
|
send(:"#{method_name}=", value)
|
@@ -390,6 +416,5 @@ module Familia
|
|
390
416
|
end
|
391
417
|
|
392
418
|
# Builds the instance-level connection chain with handlers in priority order
|
393
|
-
|
394
419
|
end
|
395
420
|
end
|