familia 2.0.0.pre14 → 2.0.0.pre16
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/code-quality.yml +138 -0
- data/.github/workflows/code-smellage.yml +145 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +1 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +48 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +66 -6
- data/CLAUDE.md +1 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +4 -4
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre13.md +1 -1
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +623 -19
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +6 -6
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +49 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +13 -10
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +26 -15
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +75 -47
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/{autoloader.rb → features/autoloader.rb} +49 -23
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +68 -66
- data/lib/familia/features/expiration/extensions.rb +61 -0
- data/lib/familia/features/expiration.rb +35 -87
- data/lib/familia/features/external_identifier.rb +11 -12
- data/lib/familia/features/object_identifier.rb +58 -20
- data/lib/familia/features/quantization.rb +17 -22
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
- data/lib/familia/features/relationships/indexing.rb +176 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +69 -271
- data/lib/familia/features/safe_dump.rb +127 -132
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +5 -5
- data/lib/familia/features.rb +21 -21
- data/lib/familia/field_type.rb +24 -4
- data/lib/familia/horreum/core/connection.rb +229 -26
- data/lib/familia/horreum/core/database_commands.rb +27 -17
- data/lib/familia/horreum/core/serialization.rb +40 -20
- data/lib/familia/horreum/core/utils.rb +2 -1
- data/lib/familia/horreum/shared/settings.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +33 -45
- data/lib/familia/horreum/subclass/management.rb +72 -24
- data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
- data/lib/familia/horreum.rb +196 -114
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -15
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +1 -1
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +129 -11
- data/try/core/connection_try.rb +7 -7
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +10 -10
- data/try/core/errors_try.rb +8 -11
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +1 -1
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_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 +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +433 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +3 -3
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +1 -1
- data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +1 -1
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +8 -10
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- metadata +77 -45
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -228
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/autoloadable.rb +0 -113
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/autoloadable/autoloadable_try.rb +0 -61
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
data/lib/familia/settings.rb
CHANGED
@@ -1,18 +1,23 @@
|
|
1
1
|
# lib/familia/settings.rb
|
2
2
|
|
3
|
+
# Familia
|
4
|
+
#
|
3
5
|
module Familia
|
4
|
-
@delim = ':'
|
6
|
+
@delim = ':'.freeze
|
5
7
|
@prefix = nil
|
6
8
|
@suffix = :object
|
7
9
|
@default_expiration = 0 # see update_expiration. Zero is skip. nil is an exception.
|
8
10
|
@logical_database = nil
|
9
11
|
@encryption_keys = nil
|
10
12
|
@current_key_version = nil
|
11
|
-
@encryption_personalization = 'FamilialMatters'
|
13
|
+
@encryption_personalization = 'FamilialMatters'.freeze
|
14
|
+
@pipeline_mode = :warn
|
12
15
|
|
16
|
+
# Familia::Settings
|
17
|
+
#
|
13
18
|
module Settings
|
14
19
|
attr_writer :delim, :suffix, :default_expiration, :logical_database, :prefix, :encryption_keys,
|
15
|
-
:current_key_version, :encryption_personalization
|
20
|
+
:current_key_version, :encryption_personalization, :transaction_mode
|
16
21
|
|
17
22
|
def delim(val = nil)
|
18
23
|
@delim = val if val
|
@@ -35,7 +40,7 @@ module Familia
|
|
35
40
|
end
|
36
41
|
|
37
42
|
def logical_database(v = nil)
|
38
|
-
Familia.trace :DB,
|
43
|
+
Familia.trace :DB, nil, "#{@logical_database} #{v}" if Familia.debug?
|
39
44
|
@logical_database = v unless v.nil?
|
40
45
|
@logical_database
|
41
46
|
end
|
@@ -61,8 +66,7 @@ module Familia
|
|
61
66
|
# unique per application even with identical master keys and contexts.
|
62
67
|
# Must be 16 bytes or less (automatically padded with null bytes).
|
63
68
|
#
|
64
|
-
# @example
|
65
|
-
# Familia.configure do |config|
|
69
|
+
# @example Familia.configure do |config|
|
66
70
|
# config.encryption_personalization = 'MyApp1.0'
|
67
71
|
# end
|
68
72
|
#
|
@@ -77,8 +81,80 @@ module Familia
|
|
77
81
|
@encryption_personalization
|
78
82
|
end
|
79
83
|
|
80
|
-
|
84
|
+
# Controls transaction behavior when connection handlers don't support transactions
|
85
|
+
#
|
86
|
+
# @param val [Symbol, nil] The transaction mode or nil to get current value
|
87
|
+
# @return [Symbol] Current transaction mode (:strict, :warn, :permissive)
|
88
|
+
#
|
89
|
+
# Available modes:
|
90
|
+
# - :warn (default): Log warning and execute commands individually
|
91
|
+
# - :strict: Raise OperationModeError when transaction unavailable
|
92
|
+
# - :permissive: Silently execute commands individually
|
93
|
+
#
|
94
|
+
# @example Setting transaction mode
|
95
|
+
# Familia.configure do |config|
|
96
|
+
# config.transaction_mode = :warn
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
def transaction_mode(val = nil)
|
100
|
+
if val
|
101
|
+
unless [:strict, :warn, :permissive].include?(val)
|
102
|
+
raise ArgumentError, 'Transaction mode must be :strict, :warn, or :permissive'
|
103
|
+
end
|
104
|
+
@transaction_mode = val
|
105
|
+
end
|
106
|
+
@transaction_mode || :warn # default to warn mode
|
107
|
+
end
|
108
|
+
|
109
|
+
# Controls pipeline behavior when connection handlers don't support pipelines
|
110
|
+
#
|
111
|
+
# @param val [Symbol, nil] The pipeline mode or nil to get current value
|
112
|
+
# @return [Symbol] Current pipeline mode (:strict, :warn, :permissive)
|
113
|
+
#
|
114
|
+
# Available modes:
|
115
|
+
# - :warn (default): Log warning and execute commands individually
|
116
|
+
# - :strict: Raise OperationModeError when pipeline unavailable
|
117
|
+
# - :permissive: Silently execute commands individually
|
118
|
+
#
|
119
|
+
# @example Setting pipeline mode
|
120
|
+
# Familia.configure do |config|
|
121
|
+
# config.pipeline_mode = :permissive
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
def pipeline_mode(val = nil)
|
125
|
+
if val
|
126
|
+
unless [:strict, :warn, :permissive].include?(val)
|
127
|
+
raise ArgumentError, 'Pipeline mode must be :strict, :warn, or :permissive'
|
128
|
+
end
|
129
|
+
@pipeline_mode = val
|
130
|
+
end
|
131
|
+
@pipeline_mode || :warn # default to warn mode
|
132
|
+
end
|
133
|
+
|
134
|
+
def pipeline_mode=(val)
|
135
|
+
unless [:strict, :warn, :permissive].include?(val)
|
136
|
+
raise ArgumentError, 'Pipeline mode must be :strict, :warn, or :permissive'
|
137
|
+
end
|
138
|
+
@pipeline_mode = val
|
139
|
+
end
|
140
|
+
|
141
|
+
# Configure Familia settings
|
142
|
+
#
|
143
|
+
# @yield [Settings] self for block-based configuration
|
144
|
+
# @return [Settings] self for method chaining
|
145
|
+
#
|
146
|
+
# @example Block-based configuration
|
147
|
+
# Familia.configure do |config|
|
148
|
+
# config.redis_uri = "redis://localhost:6379/1"
|
149
|
+
# config.ttl = 3600
|
150
|
+
# end
|
151
|
+
#
|
152
|
+
# @example Method chaining
|
153
|
+
# Familia.configure.redis_uri = "redis://localhost:6379/1"
|
154
|
+
def configure
|
155
|
+
yield self if block_given?
|
81
156
|
self
|
82
157
|
end
|
158
|
+
alias config configure
|
83
159
|
end
|
84
160
|
end
|
data/lib/familia/utils.rb
CHANGED
@@ -1,11 +1,9 @@
|
|
1
1
|
# lib/familia/utils.rb
|
2
2
|
|
3
3
|
module Familia
|
4
|
-
|
5
4
|
# Family-related utility methods
|
6
5
|
#
|
7
6
|
module Utils
|
8
|
-
|
9
7
|
using Familia::Refinements::TimeLiterals
|
10
8
|
|
11
9
|
# Joins array elements with Familia delimiter
|
@@ -38,10 +36,10 @@ module Familia
|
|
38
36
|
end
|
39
37
|
|
40
38
|
# Returns current time in UTC as a float
|
41
|
-
# @param
|
39
|
+
# @param current_time [Time] time object (default: current time)
|
42
40
|
# @return [Float] time in seconds since epoch
|
43
|
-
def now(
|
44
|
-
|
41
|
+
def now(current_time = Time.now)
|
42
|
+
current_time.utc.to_f
|
45
43
|
end
|
46
44
|
|
47
45
|
# A quantized timestamp
|
@@ -51,12 +49,11 @@ module Familia
|
|
51
49
|
# @param time [Integer, Float, Time, nil] A specific time to quantize (default: current time).
|
52
50
|
# @return [Integer, String] A unix timestamp or formatted timestamp string.
|
53
51
|
#
|
54
|
-
# @example
|
55
|
-
# Familia.qstamp # Returns an integer timestamp rounded to the nearest 10 minutes
|
52
|
+
# @example Familia.qstamp # Returns an integer timestamp rounded to the nearest 10 minutes
|
56
53
|
# Familia.qstamp(1.hour) # Uses 1 hour quantum
|
57
54
|
# Familia.qstamp(10.minutes, pattern: '%H:%M') # Returns a formatted string like "12:30"
|
58
55
|
# Familia.qstamp(10.minutes, time: 1302468980) # Quantizes the given Unix timestamp
|
59
|
-
# Familia.qstamp(10.minutes, time:
|
56
|
+
# Familia.qstamp(10.minutes, time: Familia.now) # Quantizes the given Time object
|
60
57
|
# Familia.qstamp(10.minutes, pattern: '%H:%M', time: 1302468980) # Formats a specific time
|
61
58
|
#
|
62
59
|
def qstamp(quantum = 10.minutes, pattern: nil, time: nil)
|
@@ -72,84 +69,6 @@ module Familia
|
|
72
69
|
end
|
73
70
|
end
|
74
71
|
|
75
|
-
# This method determines the appropriate transformation to apply based on
|
76
|
-
# the class of the input argument.
|
77
|
-
#
|
78
|
-
# @param [Object] value_to_distinguish The value to be processed. Keep in
|
79
|
-
# mind that all data is stored as a string so whatever the type
|
80
|
-
# of the value, it will be converted to a string.
|
81
|
-
# @param [Boolean] strict_values Whether to enforce strict value handling.
|
82
|
-
# Defaults to true.
|
83
|
-
# @return [String, nil] The processed value as a string or nil for unsupported
|
84
|
-
# classes.
|
85
|
-
#
|
86
|
-
# The method uses a case statement to handle different classes:
|
87
|
-
# - For `Symbol`, `String`, `Integer`, and `Float` classes, it traces the
|
88
|
-
# operation and converts the value to a string.
|
89
|
-
# - For `Familia::Horreum` class, it traces the operation and returns the
|
90
|
-
# identifier of the value.
|
91
|
-
# - For `TrueClass`, `FalseClass`, and `NilClass`, it traces the operation and
|
92
|
-
# converts the value to a string ("true", "false", or "").
|
93
|
-
# - For any other class, it traces the operation and returns nil.
|
94
|
-
#
|
95
|
-
# Alternative names for `value_to_distinguish` could be `input_value`, `value`,
|
96
|
-
# or `object`.
|
97
|
-
#
|
98
|
-
def distinguisher(value_to_distinguish, strict_values: true)
|
99
|
-
case value_to_distinguish
|
100
|
-
when ::Symbol, ::String, ::Integer, ::Float
|
101
|
-
Familia.trace :TOREDIS_DISTINGUISHER, dbclient, 'string', caller(1..1) if Familia.debug?
|
102
|
-
|
103
|
-
# Symbols and numerics are naturally serializable to strings
|
104
|
-
# so it's a relatively low risk operation.
|
105
|
-
value_to_distinguish.to_s
|
106
|
-
|
107
|
-
when ::TrueClass, ::FalseClass, ::NilClass
|
108
|
-
Familia.trace :TOREDIS_DISTINGUISHER, dbclient, 'true/false/nil', caller(1..1) if Familia.debug?
|
109
|
-
|
110
|
-
# TrueClass, FalseClass, and NilClass are considered high risk because their
|
111
|
-
# original types cannot be reliably determined from their serialized string
|
112
|
-
# representations. This can lead to unexpected behavior during deserialization.
|
113
|
-
# For instance, a TrueClass value serialized as "true" might be deserialized as
|
114
|
-
# a String, causing application errors. Even more problematic, a NilClass value
|
115
|
-
# serialized as an empty string makes it impossible to distinguish between a
|
116
|
-
# nil value and an empty string upon deserialization. Such scenarios can result
|
117
|
-
# in subtle, hard-to-diagnose bugs. To mitigate these risks, we raise an
|
118
|
-
# exception when encountering these types unless the strict_values option is
|
119
|
-
# explicitly set to false.
|
120
|
-
#
|
121
|
-
raise Familia::HighRiskFactor, value_to_distinguish if strict_values
|
122
|
-
|
123
|
-
value_to_distinguish.to_s #=> "true", "false", ""
|
124
|
-
|
125
|
-
when Familia::Base, Class
|
126
|
-
Familia.trace :TOREDIS_DISTINGUISHER, dbclient, 'base', caller(1..1) if Familia.debug?
|
127
|
-
|
128
|
-
# When called with a class we simply transform it to its name. For
|
129
|
-
# instances of Familia class, we store the identifier.
|
130
|
-
if value_to_distinguish.is_a?(Class)
|
131
|
-
value_to_distinguish.name
|
132
|
-
else
|
133
|
-
value_to_distinguish.identifier
|
134
|
-
end
|
135
|
-
|
136
|
-
else
|
137
|
-
Familia.trace :TOREDIS_DISTINGUISHER, dbclient, "else1 #{strict_values}", caller(1..1) if Familia.debug?
|
138
|
-
|
139
|
-
if value_to_distinguish.class.ancestors.member?(Familia::Base)
|
140
|
-
Familia.trace :TOREDIS_DISTINGUISHER, dbclient, 'isabase', caller(1..1) if Familia.debug?
|
141
|
-
|
142
|
-
value_to_distinguish.identifier
|
143
|
-
|
144
|
-
else
|
145
|
-
Familia.trace :TOREDIS_DISTINGUISHER, dbclient, "else2 #{strict_values}", caller(1..1) if Familia.debug?
|
146
|
-
raise Familia::HighRiskFactor, value_to_distinguish if strict_values
|
147
|
-
|
148
|
-
nil
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
72
|
# Converts an absolute file path to a path relative to the current working
|
154
73
|
# directory. This simplifies logging and error reporting by showing
|
155
74
|
# only the relevant parts of file paths instead of lengthy absolute paths.
|
@@ -191,6 +110,5 @@ module Familia
|
|
191
110
|
def pretty_stack(skip: 1, limit: 5)
|
192
111
|
caller(skip..(skip + limit + 1)).first(limit).map { |frame| pretty_path(frame) }.join("\n")
|
193
112
|
end
|
194
|
-
|
195
113
|
end
|
196
114
|
end
|
@@ -8,7 +8,7 @@ module Familia
|
|
8
8
|
# allowing for stateless verification of an identifier's authenticity.
|
9
9
|
module VerifiableIdentifier
|
10
10
|
# By extending SecureIdentifier, we gain access to its instance methods
|
11
|
-
# (like
|
11
|
+
# (like generate_id) as class methods on this module.
|
12
12
|
extend Familia::SecureIdentifier
|
13
13
|
|
14
14
|
# The secret key for HMAC generation, loaded from an environment variable.
|
@@ -33,7 +33,7 @@ module Familia
|
|
33
33
|
# $ openssl rand -hex 32
|
34
34
|
# > cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d
|
35
35
|
#
|
36
|
-
# 2.
|
36
|
+
# 2. UnsortedSet it as an environment variable in your production environment:
|
37
37
|
# export VERIFIABLE_ID_HMAC_SECRET="cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d"
|
38
38
|
#
|
39
39
|
SECRET_KEY = ENV.fetch('VERIFIABLE_ID_HMAC_SECRET', 'cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d')
|
@@ -61,8 +61,8 @@ module Familia
|
|
61
61
|
# base remains as passed in keyword argument or default
|
62
62
|
end
|
63
63
|
|
64
|
-
# Re-use
|
65
|
-
random_hex =
|
64
|
+
# Re-use generate_id from the SecureIdentifier module.
|
65
|
+
random_hex = generate_id(16)
|
66
66
|
tag_hex = generate_tag(random_hex, scope: scope)
|
67
67
|
|
68
68
|
combined_hex = random_hex + tag_hex
|
data/lib/familia/version.rb
CHANGED
data/lib/familia.rb
CHANGED
@@ -7,13 +7,14 @@ require 'connection_pool'
|
|
7
7
|
|
8
8
|
# OJ configuration is handled internally by Familia::JsonSerializer
|
9
9
|
|
10
|
+
require_relative 'multi_result'
|
10
11
|
require_relative 'familia/refinements'
|
11
12
|
require_relative 'familia/errors'
|
12
13
|
require_relative 'familia/version'
|
13
14
|
|
14
|
-
# Familia - A family warehouse for Redis
|
15
|
+
# Familia - A family warehouse for Valkey/Redis
|
15
16
|
#
|
16
|
-
# Familia provides a way to organize and store Ruby objects in
|
17
|
+
# Familia provides a way to organize and store Ruby objects in the database.
|
17
18
|
# It includes various modules and classes to facilitate object-Database interactions.
|
18
19
|
#
|
19
20
|
# @example Basic usage
|
@@ -32,18 +33,31 @@ require_relative 'familia/version'
|
|
32
33
|
# @see https://github.com/delano/familia
|
33
34
|
#
|
34
35
|
module Familia
|
35
|
-
|
36
36
|
@debug = ENV['FAMILIA_DEBUG'].to_s.downcase.match?(/^(true|1)$/i).freeze
|
37
37
|
@members = []
|
38
38
|
|
39
|
+
using Refinements::StylizeWords
|
40
|
+
|
39
41
|
class << self
|
40
|
-
attr_accessor :debug
|
42
|
+
attr_accessor :debug # rubocop:disable ThreadSafety/ClassAndModuleAttributes
|
41
43
|
attr_reader :members
|
42
44
|
|
43
45
|
def included(member)
|
44
46
|
raise Problem, "#{member} should subclass Familia::Horreum"
|
45
47
|
end
|
46
48
|
|
49
|
+
def resolve_class(target)
|
50
|
+
case target
|
51
|
+
when Class
|
52
|
+
target
|
53
|
+
when ::String, Symbol
|
54
|
+
config_name = target.to_s.demodularize.snake_case
|
55
|
+
member_by_config_name(config_name)
|
56
|
+
else
|
57
|
+
raise ArgumentError, "Expected Class, String, or Symbol, got #{target.class}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
47
61
|
# A convenience pattern for configuring Familia.
|
48
62
|
#
|
49
63
|
# @example
|
@@ -65,6 +79,59 @@ module Familia
|
|
65
79
|
def debug?
|
66
80
|
@debug == true
|
67
81
|
end
|
82
|
+
|
83
|
+
# Remove a member class from the members array.
|
84
|
+
# Used for test cleanup to prevent anonymous classes from polluting
|
85
|
+
# the global registry.
|
86
|
+
#
|
87
|
+
# @param klass [Class] The class to remove from members
|
88
|
+
# @return [Class, nil] The removed class or nil if not found
|
89
|
+
def unload_member(klass)
|
90
|
+
Familia.ld "[unload_member] Removing #{klass} from members"
|
91
|
+
@members.delete(klass)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Remove all anonymous/test classes from members array.
|
95
|
+
# Anonymous classes have nil names, which cause issues in member_by_config_name.
|
96
|
+
#
|
97
|
+
# @return [Array<Class>] The removed anonymous classes
|
98
|
+
def clear_anonymous_members
|
99
|
+
anonymous_classes = @members.select { |m| m.name.nil? }
|
100
|
+
Familia.ld "[clear_anonymous_members] Removing #{anonymous_classes.size} anonymous classes"
|
101
|
+
@members.reject! { |m| m.name.nil? }
|
102
|
+
anonymous_classes
|
103
|
+
end
|
104
|
+
|
105
|
+
# Check if we're in test mode by looking for test-related constants
|
106
|
+
# or environment variables
|
107
|
+
#
|
108
|
+
# @return [Boolean] true if running in test mode
|
109
|
+
def test_mode?
|
110
|
+
defined?(Tryouts) || ENV['FAMILIA_TEST_MODE'] == 'true'
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
# Finds a member class by its symbolized name
|
116
|
+
#
|
117
|
+
# NOTE: If you are not getting the expected results, check the load order of
|
118
|
+
# the models. The one your looking for may not be loaded yet. Currently
|
119
|
+
# models are loaded naively -- that is, they are loaded in the order
|
120
|
+
# they are defined in the codebase.
|
121
|
+
#
|
122
|
+
# @param config_name [Symbol, String] The symbolized name of the member class
|
123
|
+
# @return [Class, nil] The member class if found, nil otherwise
|
124
|
+
#
|
125
|
+
# @example
|
126
|
+
# Familia.member_by_config_name(:flower) # => Flower class
|
127
|
+
# Familia.member_by_config_name('flower') # => Flower class
|
128
|
+
# Familia.member_by_config_name(:nonexistent) # => nil
|
129
|
+
#
|
130
|
+
def member_by_config_name(config_name)
|
131
|
+
Familia.ld "[member_by_config_name] #{members.map(&:config_name)} #{config_name}"
|
132
|
+
|
133
|
+
members.find { |m| m.config_name.to_s.eql?(config_name.to_s) }
|
134
|
+
end
|
68
135
|
end
|
69
136
|
|
70
137
|
require_relative 'familia/secure_identifier'
|
@@ -72,6 +139,7 @@ module Familia
|
|
72
139
|
require_relative 'familia/connection'
|
73
140
|
require_relative 'familia/settings'
|
74
141
|
require_relative 'familia/utils'
|
142
|
+
require_relative 'familia/distinguisher'
|
75
143
|
require_relative 'familia/json_serializer'
|
76
144
|
|
77
145
|
extend SecureIdentifier
|
@@ -82,18 +150,7 @@ module Familia
|
|
82
150
|
end
|
83
151
|
|
84
152
|
require_relative 'familia/base'
|
85
|
-
require_relative 'familia/features/autoloadable'
|
86
153
|
require_relative 'familia/features'
|
87
154
|
require_relative 'familia/data_type'
|
88
155
|
require_relative 'familia/horreum'
|
89
156
|
require_relative 'familia/encryption'
|
90
|
-
|
91
|
-
# Ensure JSON constant is available for backward compatibility with existing code
|
92
|
-
# This approach is safer than monkey-patching core classes globally
|
93
|
-
begin
|
94
|
-
require 'json'
|
95
|
-
rescue LoadError
|
96
|
-
# If json gem is not available, define a minimal JSON constant
|
97
|
-
# that delegates to Familia::JsonSerializer for compatibility
|
98
|
-
JSON = Familia::JsonSerializer
|
99
|
-
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'concurrent-ruby'
|
4
4
|
|
5
|
-
# DatabaseLogger is RedisClient middleware.
|
5
|
+
# DatabaseLogger is Valkey/RedisClient middleware.
|
6
6
|
#
|
7
7
|
# This middleware addresses the need for detailed Database command logging, which
|
8
8
|
# was removed from the redis-rb gem due to performance concerns. However, in
|
@@ -13,6 +13,13 @@ require 'concurrent-ruby'
|
|
13
13
|
# DatabaseLogger.logger = Logger.new(STDOUT)
|
14
14
|
# RedisClient.register(DatabaseLogger)
|
15
15
|
#
|
16
|
+
# @example Capture commands for testing
|
17
|
+
# commands = DatabaseLogger.capture_commands do
|
18
|
+
# redis.set('key', 'value')
|
19
|
+
# redis.get('key')
|
20
|
+
# end
|
21
|
+
# puts commands.first[:command] # => ["SET", "key", "value"]
|
22
|
+
#
|
16
23
|
# @see https://github.com/redis-rb/redis-client?tab=readme-ov-file#instrumentation-and-middlewares
|
17
24
|
#
|
18
25
|
# @note While there were concerns about the performance impact of logging in
|
@@ -22,39 +29,75 @@ require 'concurrent-ruby'
|
|
22
29
|
# often outweigh the slight performance cost when enabled.
|
23
30
|
module DatabaseLogger
|
24
31
|
@logger = nil
|
32
|
+
@commands = []
|
25
33
|
|
26
34
|
class << self
|
27
35
|
# Gets/sets the logger instance used by DatabaseLogger.
|
28
36
|
# @return [Logger, nil] The current logger instance or nil if not set.
|
29
37
|
attr_accessor :logger
|
38
|
+
|
39
|
+
# Gets the captured commands for testing purposes.
|
40
|
+
# @return [Array] Array of command hashes with :command, :duration, :timestamp
|
41
|
+
attr_reader :commands
|
42
|
+
|
43
|
+
# Clears the captured commands array.
|
44
|
+
# @return [Array] Empty array
|
45
|
+
def clear_commands
|
46
|
+
@commands = []
|
47
|
+
end
|
48
|
+
|
49
|
+
# Captures commands in a block and returns them.
|
50
|
+
# This is useful for testing to see what commands were executed.
|
51
|
+
#
|
52
|
+
# @yield [] The block of code to execute while capturing commands.
|
53
|
+
# @return [Array] Array of captured commands with timing information.
|
54
|
+
# Each command is a hash with :command, :duration, :timestamp keys.
|
55
|
+
#
|
56
|
+
# @example Test what Redis commands your code executes
|
57
|
+
# commands = DatabaseLogger.capture_commands do
|
58
|
+
# my_library_method()
|
59
|
+
# end
|
60
|
+
# assert_equal "SET", commands.first[:command][0]
|
61
|
+
# assert commands.first[:duration] > 0
|
62
|
+
def capture_commands
|
63
|
+
clear_commands
|
64
|
+
yield
|
65
|
+
@commands.dup
|
66
|
+
end
|
30
67
|
end
|
31
68
|
|
32
69
|
# Logs the Database command and its execution time.
|
33
70
|
#
|
34
71
|
# This method is called for each Database command when the middleware is active.
|
35
|
-
# It
|
72
|
+
# It always captures commands for testing and logs them if a logger is set.
|
36
73
|
#
|
37
74
|
# @param command [Array] The Database command and its arguments.
|
38
|
-
# @param _config [Hash] The configuration options for the Redis
|
75
|
+
# @param _config [Hash] The configuration options for the Valkey/Redis
|
39
76
|
# connection.
|
40
77
|
# @return [Object] The result of the Database command execution.
|
41
78
|
#
|
42
|
-
# @note
|
43
|
-
#
|
44
|
-
# is set, the minimal overhead is often offset by the valuable insights
|
45
|
-
# gained during development and debugging.
|
79
|
+
# @note Commands are always captured with minimal overhead for testing purposes.
|
80
|
+
# Logging only occurs when DatabaseLogger.logger is set.
|
46
81
|
def call(command, _config)
|
47
|
-
return yield unless DatabaseLogger.logger
|
48
|
-
|
49
82
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
50
83
|
result = yield
|
51
84
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start
|
52
|
-
|
85
|
+
|
86
|
+
# Always capture commands for testing purposes
|
87
|
+
DatabaseLogger.instance_variable_get(:@commands) << {
|
88
|
+
command: command.dup,
|
89
|
+
duration: duration,
|
90
|
+
timestamp: Time.now,
|
91
|
+
}
|
92
|
+
|
93
|
+
# Log if logger is set
|
94
|
+
DatabaseLogger.logger&.debug("Redis: #{command.inspect} (#{duration}µs)")
|
95
|
+
|
53
96
|
result
|
54
97
|
end
|
55
98
|
end
|
56
99
|
|
57
|
-
# DatabaseCommandCounter is RedisClient middleware.
|
100
|
+
# DatabaseCommandCounter is Valkey/RedisClient middleware.
|
58
101
|
#
|
59
102
|
# This middleware counts the number of Database commands executed. It can be
|
60
103
|
# useful for performance monitoring and debugging, allowing you to track
|
@@ -66,7 +109,6 @@ end
|
|
66
109
|
#
|
67
110
|
# @see https://github.com/redis-rb/redis-client?tab=readme-ov-file#instrumentation-and-middlewares
|
68
111
|
#
|
69
|
-
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
70
112
|
module DatabaseCommandCounter
|
71
113
|
@count = Concurrent::AtomicFixnum.new(0)
|
72
114
|
|
@@ -75,11 +117,11 @@ module DatabaseCommandCounter
|
|
75
117
|
# a configuration where there's a connection to each logical db, there's only
|
76
118
|
# one when the connection is made. When using a provider of via thread local
|
77
119
|
# it could theoretically double the number of statements executed.
|
78
|
-
@skip_commands = Set.new(['SELECT']).freeze
|
120
|
+
@skip_commands = ::Set.new(['SELECT']).freeze
|
79
121
|
|
80
122
|
class << self
|
81
123
|
# Gets the set of commands to skip counting.
|
82
|
-
# @return [
|
124
|
+
# @return [UnsortedSet] The commands that won't be counted.
|
83
125
|
attr_reader :skip_commands
|
84
126
|
|
85
127
|
# Gets the current count of Database commands executed.
|
@@ -1,30 +1,29 @@
|
|
1
|
-
# lib/
|
1
|
+
# lib/multi_result.rb
|
2
2
|
|
3
|
-
#
|
3
|
+
# Represents the result of a Valkey/Redis transaction operation.
|
4
4
|
#
|
5
|
-
# This
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# keeps the results safe in its pocket dimension.
|
5
|
+
# This class encapsulates the outcome of a Database transaction,
|
6
|
+
# providing access to both the success status and the individual
|
7
|
+
# command results returned by the transaction.
|
9
8
|
#
|
10
|
-
# @attr_reader success [Boolean]
|
11
|
-
#
|
12
|
-
# @attr_reader results [Array<String>]
|
13
|
-
#
|
9
|
+
# @attr_reader success [Boolean] Indicates whether all commands
|
10
|
+
# in the transaction completed successfully.
|
11
|
+
# @attr_reader results [Array<String>] Array of return values
|
12
|
+
# from the Database commands executed in the transaction.
|
14
13
|
#
|
15
|
-
# @example
|
14
|
+
# @example Creating a MultiResult instance
|
16
15
|
# result = MultiResult.new(true, ["OK", "OK"])
|
17
16
|
#
|
18
|
-
# @example
|
17
|
+
# @example Checking transaction success
|
19
18
|
# if result.successful?
|
20
|
-
# puts "
|
19
|
+
# puts "Transaction completed successfully"
|
21
20
|
# else
|
22
|
-
# puts "
|
21
|
+
# puts "Transaction failed"
|
23
22
|
# end
|
24
23
|
#
|
25
|
-
# @example
|
24
|
+
# @example Accessing individual command results
|
26
25
|
# result.results.each_with_index do |value, index|
|
27
|
-
# puts "Command #{index + 1}
|
26
|
+
# puts "Command #{index + 1} returned: #{value}"
|
28
27
|
# end
|
29
28
|
#
|
30
29
|
class MultiResult
|
@@ -58,6 +57,13 @@ class MultiResult
|
|
58
57
|
end
|
59
58
|
alias to_a tuple
|
60
59
|
|
60
|
+
# Returns the number of results in the multi-operation.
|
61
|
+
#
|
62
|
+
# @return [Integer] The number of individual command results returned by the transaction.
|
63
|
+
def size
|
64
|
+
results.size
|
65
|
+
end
|
66
|
+
|
61
67
|
def to_h
|
62
68
|
{ success: successful?, results: results }
|
63
69
|
end
|
@@ -69,4 +75,5 @@ class MultiResult
|
|
69
75
|
@success
|
70
76
|
end
|
71
77
|
alias success? successful?
|
78
|
+
alias areyouhappynow? successful?
|
72
79
|
end
|