familia 2.0.0.pre15 → 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 +64 -4
- 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 +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- 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/features/autoloader.rb +30 -12
- 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 +66 -64
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +9 -12
- data/lib/familia/features/object_identifier.rb +56 -19
- data/lib/familia/features/quantization.rb +16 -21
- 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 +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- 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 +3 -5
- data/lib/familia/features.rb +4 -13
- 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 -14
- 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 +120 -2
- 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 +75 -43
- 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 -198
- 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/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/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- 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
@@ -1,158 +1,155 @@
|
|
1
1
|
# lib/familia/features/safe_dump.rb
|
2
2
|
|
3
|
-
|
4
|
-
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
5
3
|
#
|
6
4
|
# Class instance variables are used here for feature configuration
|
7
5
|
# (e.g., @dump_method, @load_method). These are set once and not mutated
|
8
6
|
# at runtime, so thread safety is not a concern for this feature.
|
9
7
|
#
|
10
|
-
module Familia
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
using Familia::Refinements::SnakeCase
|
48
|
-
|
49
|
-
@dump_method = :to_json
|
50
|
-
@load_method = :from_json
|
51
|
-
|
52
|
-
def self.included(base)
|
53
|
-
|
54
|
-
Familia.trace(:LOADED, self, base, caller(1..1)) if Familia.debug?
|
55
|
-
base.extend ClassMethods
|
8
|
+
module Familia
|
9
|
+
module Features
|
10
|
+
# SafeDump is a mixin that allows models to define a list of fields that are
|
11
|
+
# safe to dump. This is useful for serializing objects to JSON or other
|
12
|
+
# formats where you want to ensure that only certain fields are exposed.
|
13
|
+
#
|
14
|
+
# To use SafeDump, include it in your model and use the DSL methods to define
|
15
|
+
# safe dump fields. The fields can be either symbols or hashes. If a field is
|
16
|
+
# a symbol, the method with the same name will be called on the object to
|
17
|
+
# retrieve the value. If the field is a hash, the key is the field name and
|
18
|
+
# the value is a lambda that will be called with the object as an argument.
|
19
|
+
# The hash syntax allows you to:
|
20
|
+
# * define a field name that is different from the method name
|
21
|
+
# * define a field that requires some computation on-the-fly
|
22
|
+
# * define a field that is not a method on the object
|
23
|
+
#
|
24
|
+
# Example:
|
25
|
+
#
|
26
|
+
# feature :safe_dump
|
27
|
+
#
|
28
|
+
# safe_dump_field :objid
|
29
|
+
# safe_dump_field :updated
|
30
|
+
# safe_dump_field :created
|
31
|
+
# safe_dump_field :active, ->(obj) { obj.active? }
|
32
|
+
#
|
33
|
+
# Alternatively, you can define multiple fields at once:
|
34
|
+
#
|
35
|
+
# safe_dump_fields :objid, :updated, :created,
|
36
|
+
# { active: ->(obj) { obj.active? } }
|
37
|
+
#
|
38
|
+
#
|
39
|
+
# Internally, all fields are normalized to the hash syntax and stored in
|
40
|
+
# `@safe_dump_field_map`. SafeDump.safe_dump_fields returns only the list
|
41
|
+
# of symbols in the order they were defined.
|
42
|
+
#
|
43
|
+
module SafeDump
|
44
|
+
Familia::Base.add_feature self, :safe_dump
|
56
45
|
|
57
|
-
|
58
|
-
base.instance_variable_set(:@safe_dump_field_map, {})
|
59
|
-
end
|
46
|
+
using Familia::Refinements::StylizeWords
|
60
47
|
|
61
|
-
|
62
|
-
|
63
|
-
# These methods become available on the model class
|
64
|
-
module ClassMethods
|
65
|
-
# Define a single safe dump field
|
66
|
-
# @param field_name [Symbol] The name of the field
|
67
|
-
# @param callable [Proc, nil] Optional callable to transform the value
|
68
|
-
def safe_dump_field(field_name, callable = nil)
|
69
|
-
@safe_dump_field_map ||= {}
|
48
|
+
@dump_method = :to_json
|
49
|
+
@load_method = :from_json
|
70
50
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
obj[field_name] # Familia::DataType classes
|
75
|
-
elsif obj.respond_to?(field_name)
|
76
|
-
obj.send(field_name) # Regular method calls
|
77
|
-
end
|
78
|
-
}
|
51
|
+
def self.included(base)
|
52
|
+
Familia.trace(:LOADED, self, base) if Familia.debug?
|
53
|
+
base.extend ModelClassMethods
|
79
54
|
|
80
|
-
|
55
|
+
# Initialize the safe dump field map
|
56
|
+
base.instance_variable_set(:@safe_dump_field_map, {})
|
81
57
|
end
|
82
58
|
|
83
|
-
#
|
84
|
-
#
|
85
|
-
|
86
|
-
|
87
|
-
|
59
|
+
# SafeDump::ModelClassMethods
|
60
|
+
#
|
61
|
+
# These methods become available on the model class
|
62
|
+
module ModelClassMethods
|
63
|
+
# Define a single safe dump field
|
64
|
+
# @param field_name [Symbol] The name of the field
|
65
|
+
# @param callable [Proc, nil] Optional callable to transform the value
|
66
|
+
def safe_dump_field(field_name, callable = nil)
|
67
|
+
@safe_dump_field_map ||= {}
|
68
|
+
|
69
|
+
field_name = field_name.to_sym
|
70
|
+
field_value = callable || lambda { |obj|
|
71
|
+
if obj.respond_to?(:[]) && obj[field_name]
|
72
|
+
obj[field_name] # Familia::DataType classes
|
73
|
+
elsif obj.respond_to?(field_name)
|
74
|
+
obj.send(field_name) # Regular method calls
|
75
|
+
end
|
76
|
+
}
|
88
77
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
78
|
+
@safe_dump_field_map[field_name] = field_value
|
79
|
+
end
|
80
|
+
|
81
|
+
# Define multiple safe dump fields at once
|
82
|
+
# @param fields [Array] Mixed array of symbols and hashes
|
83
|
+
def safe_dump_fields(*fields)
|
84
|
+
# If no arguments, return field names (getter behavior)
|
85
|
+
return safe_dump_field_names if fields.empty?
|
86
|
+
|
87
|
+
# Otherwise, define fields (setter behavior)
|
88
|
+
fields.each do |field|
|
89
|
+
if field.is_a?(Symbol)
|
90
|
+
safe_dump_field(field)
|
91
|
+
elsif field.is_a?(Hash)
|
92
|
+
field.each do |name, callable|
|
93
|
+
safe_dump_field(name, callable)
|
94
|
+
end
|
96
95
|
end
|
97
96
|
end
|
98
97
|
end
|
99
|
-
end
|
100
98
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
99
|
+
# Returns an array of safe dump field names in the order they were defined
|
100
|
+
def safe_dump_field_names
|
101
|
+
(@safe_dump_field_map || {}).keys
|
102
|
+
end
|
105
103
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
104
|
+
# Returns the field map used for dumping
|
105
|
+
def safe_dump_field_map
|
106
|
+
@safe_dump_field_map || {}
|
107
|
+
end
|
110
108
|
|
111
|
-
|
112
|
-
|
113
|
-
|
109
|
+
# Legacy method for setting safe dump fields (for backward compatibility)
|
110
|
+
def set_safe_dump_fields(*fields)
|
111
|
+
safe_dump_fields(*fields)
|
112
|
+
end
|
114
113
|
end
|
115
|
-
end
|
116
114
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
115
|
+
# Returns a hash of safe fields and their values. This method
|
116
|
+
# calls the callables defined in the safe_dump_field_map with
|
117
|
+
# the instance object as an argument.
|
118
|
+
#
|
119
|
+
# The return values are not cached, so if you call this method
|
120
|
+
# multiple times, the callables will be called each time.
|
121
|
+
#
|
122
|
+
# Example:
|
123
|
+
#
|
124
|
+
# class Customer < Familia::HashKey
|
125
|
+
# include SafeDump
|
126
|
+
# @safe_dump_fields = [
|
127
|
+
# :name,
|
128
|
+
# { :active => ->(cust) { cust.active? } }
|
129
|
+
# ]
|
130
|
+
#
|
131
|
+
# def active?
|
132
|
+
# true # or false
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# cust = Customer.new :name => 'Lucy'
|
136
|
+
# cust.safe_dump
|
137
|
+
# #=> { :name => 'Lucy', :active => true }
|
138
|
+
#
|
139
|
+
def safe_dump
|
140
|
+
self.class.safe_dump_field_map.transform_values do |callable|
|
141
|
+
transformed_value = callable.call(self)
|
142
|
+
|
143
|
+
# If the value is a relative ancestor of SafeDump we can
|
144
|
+
# call safe_dump on it, otherwise we'll just return the value as-is.
|
145
|
+
if transformed_value.is_a?(SafeDump)
|
146
|
+
transformed_value.safe_dump
|
147
|
+
else
|
148
|
+
transformed_value
|
149
|
+
end
|
151
150
|
end
|
152
151
|
end
|
153
152
|
end
|
154
|
-
|
155
|
-
extend ClassMethods
|
156
153
|
end
|
157
154
|
end
|
158
155
|
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
@@ -37,10 +37,10 @@
|
|
37
37
|
#
|
38
38
|
# 2. Every .dup, .to_s, +, interpolation, or method call may create hidden
|
39
39
|
# copies:
|
40
|
-
# s = "secret"
|
41
|
-
# t = s.dup # New object, same content — now two copies
|
42
|
-
# u = s + "123" # New string — third copy
|
43
|
-
# "#{t}" # Interpolation — fourth copy
|
40
|
+
# `s = "secret"`
|
41
|
+
# `t = s.dup` # New object, same content — now two copies
|
42
|
+
# `u = s + "123"` # New string — third copy
|
43
|
+
# `"#{t}"` # Interpolation — fourth copy
|
44
44
|
# These copies are *not* controlled by RedactedString and may persist.
|
45
45
|
#
|
46
46
|
# 3. String Freezing & Immutability
|
@@ -95,8 +95,8 @@ class RedactedString
|
|
95
95
|
# Example:
|
96
96
|
# token.expose do |plain|
|
97
97
|
# # Good: use directly without copying
|
98
|
-
# HTTP.post('/api', headers: { 'X-Token' => plain })
|
99
|
-
# # Avoid: plain.dup
|
98
|
+
# `HTTP.post('/api', headers: { 'X-Token' => plain })`
|
99
|
+
# # Avoid: `plain.dup`, `"prefix#{plain}"`, `plain[0..-1]`, etc.
|
100
100
|
# end
|
101
101
|
# # Value is still accessible after block
|
102
102
|
# token.clear! # Explicitly clear when done
|
@@ -76,7 +76,7 @@ module Familia
|
|
76
76
|
# Transient fields should not have fast writers since they're not
|
77
77
|
# persisted to the database.
|
78
78
|
#
|
79
|
-
# @param
|
79
|
+
# @param _klass [Class] The class to define the method on
|
80
80
|
#
|
81
81
|
def define_fast_writer(_klass)
|
82
82
|
# No fast writer for transient fields since they're not persisted
|
@@ -111,8 +111,8 @@ module Familia
|
|
111
111
|
# This method should not be called since transient fields are not
|
112
112
|
# persisted, but we provide it for completeness.
|
113
113
|
#
|
114
|
-
# @param
|
115
|
-
# @param
|
114
|
+
# @param _value [Object] The value to serialize
|
115
|
+
# @param _record [Object] The record instance
|
116
116
|
# @return [nil] Always nil since transient fields are not serialized
|
117
117
|
#
|
118
118
|
def serialize(_value, _record = nil)
|
@@ -126,8 +126,8 @@ module Familia
|
|
126
126
|
# This method should not be called since transient fields are not
|
127
127
|
# persisted, but we provide it for completeness.
|
128
128
|
#
|
129
|
-
# @param
|
130
|
-
# @param
|
129
|
+
# @param _value [Object] The value to deserialize
|
130
|
+
# @param _record [Object] The record instance
|
131
131
|
# @return [nil] Always nil since transient fields are not stored
|
132
132
|
#
|
133
133
|
def deserialize(_value, _record = nil)
|
@@ -104,18 +104,17 @@ module Familia
|
|
104
104
|
# (HashiCorp Vault, AWS Secrets Manager) or languages with secure memory handling.
|
105
105
|
#
|
106
106
|
module TransientFields
|
107
|
-
|
108
107
|
Familia::Base.add_feature self, :transient_fields, depends_on: nil
|
109
108
|
|
110
109
|
def self.included(base)
|
111
|
-
Familia.trace :LOADED, self, base
|
112
|
-
base.extend
|
110
|
+
Familia.trace :LOADED, self, base if Familia.debug?
|
111
|
+
base.extend ModelClassMethods
|
113
112
|
|
114
113
|
# Initialize transient fields tracking
|
115
114
|
base.instance_variable_set(:@transient_fields, []) unless base.instance_variable_defined?(:@transient_fields)
|
116
115
|
end
|
117
116
|
|
118
|
-
module
|
117
|
+
module ModelClassMethods
|
119
118
|
# Define a transient field that automatically wraps values in RedactedString
|
120
119
|
#
|
121
120
|
# Transient fields are not persisted to Redis/Valkey and exist only in memory.
|
@@ -223,7 +222,6 @@ module Familia
|
|
223
222
|
end
|
224
223
|
end
|
225
224
|
end
|
226
|
-
|
227
225
|
end
|
228
226
|
end
|
229
227
|
end
|
data/lib/familia/features.rb
CHANGED
@@ -120,9 +120,7 @@ module Familia
|
|
120
120
|
# If there's a value provided check that it's a valid feature
|
121
121
|
feature_name = feature_name.to_sym
|
122
122
|
feature_module = Familia::Base.find_feature(feature_name, self)
|
123
|
-
unless feature_module
|
124
|
-
raise Familia::Problem, "Unsupported feature: #{feature_name}"
|
125
|
-
end
|
123
|
+
raise Familia::Problem, "Unsupported feature: #{feature_name}" unless feature_module
|
126
124
|
|
127
125
|
# If the feature is already available, do nothing but log about it
|
128
126
|
if features_enabled.member?(feature_name)
|
@@ -130,7 +128,7 @@ module Familia
|
|
130
128
|
return
|
131
129
|
end
|
132
130
|
|
133
|
-
Familia.trace :FEATURE, nil, "#{self} includes #{feature_name.inspect}"
|
131
|
+
Familia.trace :FEATURE, nil, "#{self} includes #{feature_name.inspect}" if Familia.debug?
|
134
132
|
|
135
133
|
# Check dependencies and raise error if missing
|
136
134
|
feature_def = Familia::Base.feature_definitions[feature_name]
|
@@ -150,22 +148,15 @@ module Familia
|
|
150
148
|
options[:calling_location] = calling_location&.path
|
151
149
|
|
152
150
|
# Add feature options if the class supports them (Horreum classes)
|
153
|
-
if respond_to?(:add_feature_options)
|
154
|
-
add_feature_options(feature_name, **options)
|
155
|
-
end
|
151
|
+
add_feature_options(feature_name, **options) if respond_to?(:add_feature_options)
|
156
152
|
|
157
153
|
# Extend the Familia::Base subclass (e.g. Customer) with the feature module
|
158
154
|
include feature_module
|
159
155
|
|
160
|
-
# Trigger post-inclusion autoloading for features that support it
|
161
|
-
if feature_module.respond_to?(:post_inclusion_autoload)
|
162
|
-
feature_module.post_inclusion_autoload(self, feature_name, options)
|
163
|
-
end
|
164
|
-
|
165
156
|
# NOTE: Do we want to extend Familia::DataType here? That would make it
|
166
157
|
# possible to call safe_dump on relations fields (e.g. list, zset, hashkey).
|
167
158
|
#
|
168
|
-
# The challenge is that DataType classes (List,
|
159
|
+
# The challenge is that DataType classes (List, UnsortedSet, etc.) are shared across
|
169
160
|
# all Horreum models. If Customer extends DataType with safe_dump, then
|
170
161
|
# Session's lists would also have it. Not ideal. If that's all we wanted
|
171
162
|
# then we can do that by looping through every DataType class here.
|
data/lib/familia/field_type.rb
CHANGED
@@ -116,6 +116,26 @@ module Familia
|
|
116
116
|
handle_method_conflict(klass, :"#{method_name}=") do
|
117
117
|
klass.define_method :"#{method_name}=" do |value|
|
118
118
|
instance_variable_set(:"@#{field_name}", value)
|
119
|
+
|
120
|
+
# If this field is the identifier and object_identifier feature is loaded,
|
121
|
+
# update objid_lookup mapping when identifier is set after objid generation
|
122
|
+
if respond_to?(:objid) &&
|
123
|
+
self.class.respond_to?(:identifier_field) &&
|
124
|
+
self.class.identifier_field == field_name &&
|
125
|
+
self.class.respond_to?(:objid_lookup)
|
126
|
+
current_objid = instance_variable_get(:@objid)
|
127
|
+
self.class.objid_lookup[current_objid] = value if current_objid && value
|
128
|
+
end
|
129
|
+
|
130
|
+
# If this field is the identifier and external_identifier feature is loaded,
|
131
|
+
# update extid_lookup mapping when identifier is set after extid generation
|
132
|
+
if respond_to?(:extid) &&
|
133
|
+
self.class.respond_to?(:identifier_field) &&
|
134
|
+
self.class.identifier_field == field_name &&
|
135
|
+
self.class.respond_to?(:extid_lookup)
|
136
|
+
current_extid = instance_variable_get(:@extid)
|
137
|
+
self.class.extid_lookup[current_extid] = value if current_extid && value
|
138
|
+
end
|
119
139
|
end
|
120
140
|
end
|
121
141
|
end
|
@@ -145,7 +165,7 @@ module Familia
|
|
145
165
|
|
146
166
|
begin
|
147
167
|
# Trace the operation if debugging is enabled
|
148
|
-
Familia.trace :FAST_WRITER,
|
168
|
+
Familia.trace :FAST_WRITER, nil, "#{field_name}: #{val.inspect}" if Familia.debug?
|
149
169
|
|
150
170
|
# Convert value for database storage
|
151
171
|
prepared = serialize_value(val)
|
@@ -190,7 +210,7 @@ module Familia
|
|
190
210
|
# The default implementation passes values through unchanged.
|
191
211
|
#
|
192
212
|
# @param value [Object] The value to serialize
|
193
|
-
# @param
|
213
|
+
# @param _record [Object] The record instance (for context)
|
194
214
|
# @return [Object] The serialized value
|
195
215
|
#
|
196
216
|
def serialize(value, _record = nil)
|
@@ -203,7 +223,7 @@ module Familia
|
|
203
223
|
# The default implementation passes values through unchanged.
|
204
224
|
#
|
205
225
|
# @param value [Object] The value to deserialize
|
206
|
-
# @param
|
226
|
+
# @param _record [Object] The record instance (for context)
|
207
227
|
# @return [Object] The deserialized value
|
208
228
|
#
|
209
229
|
def deserialize(value, _record = nil)
|
@@ -228,7 +248,7 @@ module Familia
|
|
228
248
|
"method_name=#{@method_name}",
|
229
249
|
"fast_method_name=#{@fast_method_name}",
|
230
250
|
"on_conflict=#{@on_conflict}",
|
231
|
-
"category=#{category}"
|
251
|
+
"category=#{category}",
|
232
252
|
]
|
233
253
|
"#<#{self.class.name} #{attributes.join(' ')}>"
|
234
254
|
end
|