familia 2.0.0.pre17 → 2.0.0.pre19
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 +118 -6
- data/CLAUDE.md +43 -11
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +52 -0
- data/bin/irb +1 -1
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/core-field-system.md +48 -26
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +2 -1
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
- data/lib/familia/data_type/serialization.rb +9 -5
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- 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/errors.rb +51 -14
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +130 -11
- data/lib/familia/horreum/definition.rb +8 -38
- data/lib/familia/horreum/management.rb +38 -27
- data/lib/familia/horreum/persistence.rb +191 -67
- data/lib/familia/horreum/serialization.rb +94 -73
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +41 -18
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +268 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -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 +5 -5
- 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 +2 -2
- 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 +34 -5
- 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 +5 -5
- 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 +4 -4
- 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 +2 -2
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
- 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 +23 -23
- data/try/integration/cross_component_try.rb +1 -1
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- 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 +6 -2
- 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 +2 -2
- data/try/{core → integration}/persistence_operations_try.rb +163 -11
- 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 +2 -2
- 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 +2 -2
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/{horreum → unit/horreum}/base_try.rb +4 -4
- data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
- 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 +3 -3
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- 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 +147 -126
- 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
@@ -35,17 +35,23 @@ module Familia
|
|
35
35
|
# Handles conversion between Ruby objects and Valkey hash storage
|
36
36
|
#
|
37
37
|
module Persistence
|
38
|
-
# Persists the object to Valkey storage with automatic timestamping.
|
38
|
+
# Persists the object to Valkey storage with automatic timestamping and validation.
|
39
39
|
#
|
40
40
|
# Saves the current object state to Valkey storage, automatically setting
|
41
41
|
# created and updated timestamps if the object supports them. The method
|
42
|
-
#
|
42
|
+
# validates unique indexes before the transaction, commits all persistent
|
43
|
+
# fields, and optionally updates the key's expiration.
|
43
44
|
#
|
44
45
|
# @param update_expiration [Boolean] Whether to update the key's expiration
|
45
46
|
# time after saving. Defaults to true.
|
46
47
|
#
|
47
48
|
# @return [Boolean] true if the save operation was successful, false otherwise.
|
48
49
|
#
|
50
|
+
# @raise [Familia::OperationModeError] If called within an existing transaction.
|
51
|
+
# Guards need to read current values, which is not possible inside MULTI/EXEC.
|
52
|
+
# @raise [Familia::RecordExistsError] If a unique index constraint is violated
|
53
|
+
# for any class-level unique_index relationships.
|
54
|
+
#
|
49
55
|
# @example Save an object to Valkey
|
50
56
|
# user = User.new(name: "John", email: "john@example.com")
|
51
57
|
# user.save
|
@@ -55,36 +61,60 @@ module Familia
|
|
55
61
|
# user.save(update_expiration: false)
|
56
62
|
# # => true
|
57
63
|
#
|
64
|
+
# @example Handle duplicate unique index
|
65
|
+
# user2 = User.new(name: "Jane", email: "john@example.com")
|
66
|
+
# user2.save
|
67
|
+
# # => raises Familia::RecordExistsError
|
68
|
+
#
|
69
|
+
# @note Cannot be called within a transaction. Call save first to start
|
70
|
+
# the transaction, or use commit_fields/hmset for manual field updates
|
71
|
+
# within transactions.
|
72
|
+
#
|
58
73
|
# @note When Familia.debug? is enabled, this method will trace the save
|
59
74
|
# operation for debugging purposes.
|
60
75
|
#
|
61
76
|
# @see #commit_fields The underlying method that performs the field persistence
|
77
|
+
# @see #guard_unique_indexes! Automatic validation of class-level unique indexes
|
62
78
|
#
|
63
79
|
def save(update_expiration: true)
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
# Commit our tale to the Database chronicles
|
71
|
-
# Wrap in transaction for atomicity between save and indexing
|
72
|
-
ret = commit_fields(update_expiration: update_expiration)
|
73
|
-
|
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
|
80
|
+
# Prevent save within transaction - unique index guards require read operations
|
81
|
+
# which are not available in Redis MULTI/EXEC blocks
|
82
|
+
if Fiber[:familia_transaction]
|
83
|
+
raise Familia::OperationModeError,
|
84
|
+
"Cannot call save within a transaction. Save operations must be called outside transactions to ensure unique constraints can be validated."
|
82
85
|
end
|
83
86
|
|
84
|
-
Familia.
|
87
|
+
Familia.trace :SAVE, nil, self.class.uri if Familia.debug?
|
88
|
+
|
89
|
+
# Update timestamp fields before saving
|
90
|
+
self.created ||= Familia.now if respond_to?(:created)
|
91
|
+
self.updated = Familia.now if respond_to?(:updated)
|
92
|
+
|
93
|
+
# Validate unique indexes BEFORE the transaction
|
94
|
+
guard_unique_indexes!
|
95
|
+
|
96
|
+
# Everything in ONE transaction for complete atomicity
|
97
|
+
result = transaction do |_conn|
|
98
|
+
# 1. Save all fields
|
99
|
+
prepared_h = to_h_for_storage
|
100
|
+
hmset_result = hmset(prepared_h)
|
85
101
|
|
86
|
-
|
87
|
-
|
102
|
+
# 2. Set expiration in same transaction
|
103
|
+
self.update_expiration if update_expiration
|
104
|
+
|
105
|
+
# 3. Update class-level indexes
|
106
|
+
auto_update_class_indexes
|
107
|
+
|
108
|
+
# 4. Add to instances collection if available
|
109
|
+
self.class.instances.add(identifier, Familia.now) if self.class.respond_to?(:instances)
|
110
|
+
|
111
|
+
hmset_result
|
112
|
+
end
|
113
|
+
|
114
|
+
Familia.ld "[save] #{self.class} #{dbkey} #{result} (update_expiration: #{update_expiration})"
|
115
|
+
|
116
|
+
# Return boolean indicating success
|
117
|
+
!result.nil?
|
88
118
|
end
|
89
119
|
|
90
120
|
# Saves the object to Valkey storage only if it doesn't already exist.
|
@@ -129,34 +159,51 @@ module Familia
|
|
129
159
|
# Check if record exists
|
130
160
|
# If exists, raise Familia::RecordExistsError
|
131
161
|
# If not exists, save
|
132
|
-
def save_if_not_exists(update_expiration: true)
|
162
|
+
def save_if_not_exists!(update_expiration: true)
|
163
|
+
# Prevent save_if_not_exists! within transaction - needs to read existence state
|
164
|
+
if Fiber[:familia_transaction]
|
165
|
+
raise Familia::OperationModeError,
|
166
|
+
"Cannot call save_if_not_exists! within a transaction. This method must be called outside transactions to properly check existence."
|
167
|
+
end
|
168
|
+
|
133
169
|
identifier_field = self.class.identifier_field
|
134
170
|
|
135
171
|
Familia.ld "[save_if_not_exists]: #{self.class} #{identifier_field}=#{identifier}"
|
136
|
-
Familia.trace :SAVE_IF_NOT_EXISTS, nil, uri if Familia.debug?
|
172
|
+
Familia.trace :SAVE_IF_NOT_EXISTS, nil, self.class.uri if Familia.debug?
|
137
173
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
raise Familia::RecordExistsError, dbkey
|
142
|
-
end
|
174
|
+
attempts = 0
|
175
|
+
begin
|
176
|
+
attempts += 1
|
143
177
|
|
144
|
-
|
145
|
-
|
146
|
-
end
|
178
|
+
watch do
|
179
|
+
raise Familia::RecordExistsError, dbkey if exists?
|
147
180
|
|
148
|
-
|
149
|
-
|
181
|
+
txn_result = transaction do |_multi|
|
182
|
+
hmset(to_h_for_storage)
|
183
|
+
|
184
|
+
self.update_expiration if update_expiration
|
185
|
+
|
186
|
+
# Auto-index for class-level indexes after successful save
|
187
|
+
auto_update_class_indexes
|
188
|
+
end
|
150
189
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
transaction do |conn|
|
155
|
-
auto_update_class_indexes
|
190
|
+
Familia.ld "[save_if_not_exists]: txn_result=#{txn_result.inspect}"
|
191
|
+
|
192
|
+
txn_result.successful?
|
156
193
|
end
|
194
|
+
rescue OptimisticLockError => e
|
195
|
+
Familia.ld "[save_if_not_exists]: OptimisticLockError (#{attempts}): #{e.message}"
|
196
|
+
raise if attempts >= 3
|
197
|
+
|
198
|
+
sleep(0.001 * (2**attempts))
|
199
|
+
retry
|
157
200
|
end
|
201
|
+
end
|
158
202
|
|
159
|
-
|
203
|
+
def save_if_not_exists(...)
|
204
|
+
save_if_not_exists!(...)
|
205
|
+
rescue RecordExistsError, OptimisticLockError
|
206
|
+
false
|
160
207
|
end
|
161
208
|
|
162
209
|
# Commits object fields to the DB storage.
|
@@ -188,14 +235,15 @@ module Familia
|
|
188
235
|
prepared_value = to_h_for_storage
|
189
236
|
Familia.ld "[commit_fields] Begin #{self.class} #{dbkey} #{prepared_value} (exp: #{update_expiration})"
|
190
237
|
|
191
|
-
|
238
|
+
transaction do |_conn|
|
239
|
+
# Set all fields atomically
|
240
|
+
result = hmset(prepared_value)
|
192
241
|
|
193
|
-
|
194
|
-
|
195
|
-
# this will be a no-op that simply logs the attempt.
|
196
|
-
update_expiration(default_expiration: nil) if update_expiration
|
242
|
+
# Update expiration in same transaction to ensure atomicity
|
243
|
+
self.update_expiration if result && update_expiration
|
197
244
|
|
198
|
-
|
245
|
+
result
|
246
|
+
end
|
199
247
|
end
|
200
248
|
|
201
249
|
# Updates multiple fields atomically in a Database transaction.
|
@@ -216,20 +264,57 @@ module Familia
|
|
216
264
|
|
217
265
|
Familia.trace :BATCH_UPDATE, nil, fields.keys if Familia.debug?
|
218
266
|
|
219
|
-
|
267
|
+
transaction do |_conn|
|
268
|
+
# 1. Update all fields atomically
|
220
269
|
fields.each do |field, value|
|
221
270
|
prepared_value = serialize_value(value)
|
222
|
-
|
271
|
+
hset field, prepared_value
|
223
272
|
# Update instance variable to keep object in sync
|
224
273
|
send("#{field}=", value) if respond_to?("#{field}=")
|
225
274
|
end
|
275
|
+
|
276
|
+
# 2. Update expiration in same transaction
|
277
|
+
self.update_expiration if update_expiration
|
226
278
|
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# Persists only the specified fields to Redis.
|
282
|
+
#
|
283
|
+
# Saves the current in-memory values of specified fields to Redis without
|
284
|
+
# modifying them first. Fields must already be set on the instance.
|
285
|
+
#
|
286
|
+
# @param field_names [Array<Symbol, String>] Names of fields to persist
|
287
|
+
# @param update_expiration [Boolean] Whether to refresh key expiration
|
288
|
+
# @return [self] Returns self for method chaining
|
289
|
+
#
|
290
|
+
# @example Persist only passphrase fields after updating them
|
291
|
+
# customer.update_passphrase('secret').save_fields(:passphrase, :passphrase_encryption)
|
292
|
+
#
|
293
|
+
def save_fields(*field_names, update_expiration: true)
|
294
|
+
raise ArgumentError, 'No fields specified' if field_names.empty?
|
227
295
|
|
228
|
-
|
229
|
-
|
296
|
+
Familia.trace :SAVE_FIELDS, nil, field_names if Familia.debug?
|
297
|
+
|
298
|
+
transaction do |_conn|
|
299
|
+
# Build hash of field names to serialized values
|
300
|
+
fields_hash = {}
|
301
|
+
field_names.each do |field|
|
302
|
+
field_sym = field.to_sym
|
303
|
+
raise ArgumentError, "Unknown field: #{field}" unless respond_to?(field_sym)
|
304
|
+
|
305
|
+
value = send(field_sym)
|
306
|
+
prepared_value = serialize_value(value)
|
307
|
+
fields_hash[field] = prepared_value
|
308
|
+
end
|
230
309
|
|
231
|
-
|
232
|
-
|
310
|
+
# Set all fields at once using hmset
|
311
|
+
hmset(fields_hash)
|
312
|
+
|
313
|
+
# Update expiration in same transaction
|
314
|
+
self.update_expiration if update_expiration
|
315
|
+
end
|
316
|
+
|
317
|
+
self
|
233
318
|
end
|
234
319
|
|
235
320
|
# Updates the object by applying multiple field values.
|
@@ -279,22 +364,22 @@ module Familia
|
|
279
364
|
# @see #delete! The underlying method that performs the key deletion
|
280
365
|
#
|
281
366
|
def destroy!
|
282
|
-
Familia.trace :DESTROY
|
367
|
+
Familia.trace :DESTROY!, dbkey, self.class.uri
|
283
368
|
|
284
369
|
# Execute all deletion operations within a transaction
|
285
|
-
transaction do |
|
370
|
+
transaction do |_conn|
|
286
371
|
# Delete the main object key
|
287
|
-
|
372
|
+
delete!
|
288
373
|
|
289
374
|
# Delete all related fields if present
|
290
375
|
if self.class.relations?
|
291
376
|
Familia.trace :DELETE_RELATED_FIELDS!, nil,
|
292
377
|
"#{self.class} has relations: #{self.class.related_fields.keys}"
|
293
378
|
|
294
|
-
self.class.related_fields.
|
379
|
+
self.class.related_fields.each_key do |name|
|
295
380
|
obj = send(name)
|
296
381
|
Familia.trace :DELETE_RELATED_FIELD, name, "Deleting related field #{name} (#{obj.dbkey})"
|
297
|
-
|
382
|
+
obj.delete!
|
298
383
|
end
|
299
384
|
end
|
300
385
|
end
|
@@ -318,6 +403,7 @@ module Familia
|
|
318
403
|
# after clear_fields! if you want to persist the cleared state.
|
319
404
|
#
|
320
405
|
def clear_fields!
|
406
|
+
Familia.trace :CLEAR_FIELDS!, dbkey, self.class.uri
|
321
407
|
self.class.field_method_map.each_value { |method_name| send("#{method_name}=", nil) }
|
322
408
|
end
|
323
409
|
|
@@ -343,7 +429,7 @@ module Familia
|
|
343
429
|
# no authoritative source in Valkey storage.
|
344
430
|
#
|
345
431
|
def refresh!
|
346
|
-
Familia.trace :REFRESH, nil, uri if Familia.debug?
|
432
|
+
Familia.trace :REFRESH, nil, self.class.uri if Familia.debug?
|
347
433
|
raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
|
348
434
|
|
349
435
|
fields = hgetall
|
@@ -354,7 +440,7 @@ module Familia
|
|
354
440
|
# their uninitialized state during refresh operations
|
355
441
|
reset_transient_fields!
|
356
442
|
|
357
|
-
|
443
|
+
naive_refresh(**fields)
|
358
444
|
end
|
359
445
|
|
360
446
|
# Refreshes object state from the DB and returns self for method chaining.
|
@@ -378,6 +464,11 @@ module Familia
|
|
378
464
|
self
|
379
465
|
end
|
380
466
|
|
467
|
+
# Convenience methods that forward to the class method of the same name
|
468
|
+
def transaction(...) = self.class.transaction(...)
|
469
|
+
def pipelined(...) = self.class.pipelined(...)
|
470
|
+
def dbclient(...) = self.class.dbclient(...)
|
471
|
+
|
381
472
|
private
|
382
473
|
|
383
474
|
# Reset all transient fields to nil
|
@@ -402,12 +493,46 @@ module Familia
|
|
402
493
|
end
|
403
494
|
end
|
404
495
|
|
496
|
+
# Validates that unique index constraints are satisfied before saving
|
497
|
+
# This must be called OUTSIDE of transactions to allow reading current values
|
498
|
+
#
|
499
|
+
# @raise [Familia::RecordExistsError] If a unique index constraint is violated
|
500
|
+
# for any class-level unique_index relationships
|
501
|
+
#
|
502
|
+
# @note Only validates class-level unique indexes (without within: parameter).
|
503
|
+
# Instance-scoped indexes (with within:) are validated automatically when
|
504
|
+
# calling add_to_*_index methods:
|
505
|
+
#
|
506
|
+
# @example Instance-scoped indexes need to be called explicitly but when
|
507
|
+
# called they will perform the validation automatically:
|
508
|
+
# employee.add_to_company_badge_index(company) # raises on duplicate
|
509
|
+
#
|
510
|
+
# @return [void]
|
511
|
+
#
|
512
|
+
def guard_unique_indexes!
|
513
|
+
return unless self.class.respond_to?(:indexing_relationships)
|
514
|
+
|
515
|
+
self.class.indexing_relationships.each do |rel|
|
516
|
+
# Only validate unique indexes (not multi_index)
|
517
|
+
next unless rel.cardinality == :unique
|
518
|
+
|
519
|
+
# Only validate class-level indexes (skip instance-scoped)
|
520
|
+
next if rel.within
|
521
|
+
|
522
|
+
# Call the validation method if it exists
|
523
|
+
validate_method = :"guard_unique_#{rel.index_name}!"
|
524
|
+
send(validate_method) if respond_to?(validate_method)
|
525
|
+
end
|
526
|
+
|
527
|
+
nil # Explicit nil return as documented
|
528
|
+
end
|
529
|
+
|
405
530
|
# Automatically update class-level indexes after save
|
406
531
|
#
|
407
532
|
# Iterates through class-level indexing relationships and calls their
|
408
533
|
# corresponding add_to_class_* methods to populate indexes. Only processes
|
409
|
-
# class-level indexes (where
|
410
|
-
#
|
534
|
+
# class-level indexes (where within is nil), skipping instance-scoped
|
535
|
+
# indexes which require scope context.
|
411
536
|
#
|
412
537
|
# Uses idempotent Redis commands (HSET for unique_index) so repeated calls
|
413
538
|
# are safe and have negligible performance overhead. Note that multi_index
|
@@ -434,12 +559,12 @@ module Familia
|
|
434
559
|
return unless self.class.respond_to?(:indexing_relationships)
|
435
560
|
|
436
561
|
self.class.indexing_relationships.each do |rel|
|
437
|
-
# Skip instance-scoped indexes (require
|
562
|
+
# Skip instance-scoped indexes (require scope context)
|
438
563
|
# Instance-scoped indexes must be manually populated because they need
|
439
|
-
# the
|
440
|
-
|
564
|
+
# the scope instance reference (e.g., employee.add_to_company_badge_index(company))
|
565
|
+
if rel.within
|
441
566
|
Familia.ld <<~LOG_MESSAGE
|
442
|
-
[auto_update_class_indexes] Skipping #{rel.index_name} (requires
|
567
|
+
[auto_update_class_indexes] Skipping #{rel.index_name} (requires scope context)
|
443
568
|
LOG_MESSAGE
|
444
569
|
next
|
445
570
|
end
|
@@ -449,7 +574,6 @@ module Familia
|
|
449
574
|
send(add_method) if respond_to?(add_method)
|
450
575
|
end
|
451
576
|
end
|
452
|
-
|
453
577
|
end
|
454
578
|
end
|
455
579
|
end
|
@@ -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).
|
49
46
|
#
|
50
|
-
# @return [Hash] Hash with field names as keys and
|
51
|
-
# ready for database storage
|
47
|
+
# @return [Hash] Hash with field names as string keys and JSON-encoded values
|
52
48
|
#
|
53
|
-
# @
|
54
|
-
#
|
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
|
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,97 @@ 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
|
147
144
|
#
|
148
|
-
# This method
|
149
|
-
#
|
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.
|
150
148
|
#
|
151
|
-
#
|
152
|
-
#
|
153
|
-
#
|
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
|
154
153
|
#
|
155
|
-
|
156
|
-
|
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
|
+
# Handle Redis::Future objects during transactions
|
163
|
+
return val if val.is_a?(Redis::Future)
|
157
164
|
|
158
|
-
# Try to parse as JSON first for complex types
|
159
165
|
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)
|
166
|
+
Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
|
164
167
|
rescue Familia::SerializerError
|
165
|
-
|
168
|
+
log_deserialization_issue(val, field_name)
|
169
|
+
val
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def log_deserialization_issue(val, field_name)
|
176
|
+
context = field_name ? "#{self.class}##{field_name}" : self.class.to_s
|
177
|
+
dbkey_info = respond_to?(:dbkey) ? dbkey : 'no dbkey'
|
178
|
+
|
179
|
+
msg = if looks_like_json?(val)
|
180
|
+
"Corrupted JSON in #{context}: #{val.inspect} (#{dbkey_info})"
|
181
|
+
else
|
182
|
+
"Legacy plain string in #{context}: #{val.inspect} (#{dbkey_info})"
|
166
183
|
end
|
167
184
|
|
168
|
-
|
185
|
+
Familia.le(msg)
|
186
|
+
end
|
187
|
+
|
188
|
+
def looks_like_json?(val)
|
189
|
+
val.start_with?('{', '[', '"') || %w[true false null].include?(val)
|
169
190
|
end
|
170
191
|
end
|
171
192
|
end
|
@@ -11,14 +11,6 @@ module Familia
|
|
11
11
|
# Provides identifier handling, dbkey generation, and object inspection
|
12
12
|
#
|
13
13
|
module Utils
|
14
|
-
# def uri
|
15
|
-
# base_uri = self.class.uri || Familia.uri
|
16
|
-
# u = base_uri.dup # make a copy to modify safely
|
17
|
-
# u.logical_database = logical_database if logical_database
|
18
|
-
# u.key = dbkey
|
19
|
-
# u
|
20
|
-
# end
|
21
|
-
|
22
14
|
# +suffix+ is the value to be used at the end of the db key
|
23
15
|
# (e.g. `customer:customer_id:scores` would have `scores` as the suffix
|
24
16
|
# and `customer_id` would have been the identifier in that case).
|