familia 2.0.0.pre15 → 2.0.0.pre17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smells.yml +85 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +3 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +54 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +86 -4
- data/CLAUDE.md +39 -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 +42 -42
- 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 +624 -20
- 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 +7 -7
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +51 -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/class_methods.rb +63 -0
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +15 -11
- 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 +128 -14
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +12 -171
- 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 +71 -66
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +57 -19
- data/lib/familia/features/object_identifier.rb +134 -25
- 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 +306 -0
- data/lib/familia/features/relationships/indexing.rb +182 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +164 -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 +10 -7
- data/lib/familia/features.rb +10 -14
- data/lib/familia/field_type.rb +6 -4
- data/lib/familia/horreum/connection.rb +297 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
- data/lib/familia/horreum/serialization.rb +172 -0
- data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
- data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
- data/lib/familia/horreum.rb +222 -119
- 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 +2 -2
- 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 +10 -10
- 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 +11 -10
- data/try/core/errors_try.rb +11 -14
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/familia_try.rb +1 -1
- 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 +3 -3
- 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/sorted_set_zadd_options_try.rb +625 -0
- 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/field_groups_try.rb +244 -0
- 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 +443 -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 +3 -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 +6 -7
- data/try/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +3 -1
- data/try/horreum/defensive_initialization_try.rb +86 -0
- data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -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/horreum/settings_try.rb +2 -0
- 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 +2 -2
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +13 -15
- 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
- data/try/valkey.conf +26 -0
- metadata +92 -52
- 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/horreum/core/connection.rb +0 -73
- data/lib/familia/horreum/core.rb +0 -21
- 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)
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# lib/familia/features/transient_fields.rb
|
2
2
|
|
3
3
|
require_relative 'transient_fields/redacted_string'
|
4
|
+
require_relative 'transient_fields/transient_field_type'
|
4
5
|
|
5
6
|
module Familia
|
6
7
|
module Features
|
@@ -104,18 +105,17 @@ module Familia
|
|
104
105
|
# (HashiCorp Vault, AWS Secrets Manager) or languages with secure memory handling.
|
105
106
|
#
|
106
107
|
module TransientFields
|
107
|
-
|
108
|
-
Familia::Base.add_feature self, :transient_fields, depends_on: nil
|
108
|
+
Familia::Base.add_feature self, :transient_fields, depends_on: nil, field_group: :transient_fields
|
109
109
|
|
110
110
|
def self.included(base)
|
111
|
-
Familia.trace :LOADED, self, base
|
112
|
-
base.extend
|
111
|
+
Familia.trace :LOADED, self, base if Familia.debug?
|
112
|
+
base.extend ModelClassMethods
|
113
113
|
|
114
114
|
# Initialize transient fields tracking
|
115
115
|
base.instance_variable_set(:@transient_fields, []) unless base.instance_variable_defined?(:@transient_fields)
|
116
116
|
end
|
117
117
|
|
118
|
-
module
|
118
|
+
module ModelClassMethods
|
119
119
|
# Define a transient field that automatically wraps values in RedactedString
|
120
120
|
#
|
121
121
|
# Transient fields are not persisted to Redis/Valkey and exist only in memory.
|
@@ -144,8 +144,12 @@ module Familia
|
|
144
144
|
@transient_fields ||= []
|
145
145
|
@transient_fields << name unless @transient_fields.include?(name)
|
146
146
|
|
147
|
+
# Add to field_groups if the group exists
|
148
|
+
if field_groups&.key?(:transient_fields)
|
149
|
+
field_groups[:transient_fields] << name
|
150
|
+
end
|
151
|
+
|
147
152
|
# Use the field type system for proper integration
|
148
|
-
require_relative 'transient_fields/transient_field_type'
|
149
153
|
field_type = TransientFieldType.new(name, as: as, **kwargs.merge(fast_method: false))
|
150
154
|
register_field_type(field_type)
|
151
155
|
end
|
@@ -223,7 +227,6 @@ module Familia
|
|
223
227
|
end
|
224
228
|
end
|
225
229
|
end
|
226
|
-
|
227
230
|
end
|
228
231
|
end
|
229
232
|
end
|
data/lib/familia/features.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
require_relative 'features/autoloader'
|
5
5
|
|
6
6
|
module Familia
|
7
|
-
FeatureDefinition = Data.define(:name, :depends_on)
|
7
|
+
FeatureDefinition = Data.define(:name, :depends_on, :field_group)
|
8
8
|
|
9
9
|
# Familia::Features
|
10
10
|
#
|
@@ -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]
|
@@ -149,23 +147,21 @@ module Familia
|
|
149
147
|
calling_location = caller_locations(1, 1)&.first
|
150
148
|
options[:calling_location] = calling_location&.path
|
151
149
|
|
152
|
-
#
|
153
|
-
if respond_to?(:
|
154
|
-
|
150
|
+
# Initialize field group if feature declares one
|
151
|
+
if feature_def&.field_group && respond_to?(:field_group)
|
152
|
+
field_group(feature_def.field_group)
|
155
153
|
end
|
156
154
|
|
155
|
+
# Add feature options if the class supports them (Horreum classes)
|
156
|
+
add_feature_options(feature_name, **options) if respond_to?(:add_feature_options)
|
157
|
+
|
157
158
|
# Extend the Familia::Base subclass (e.g. Customer) with the feature module
|
158
159
|
include feature_module
|
159
160
|
|
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
161
|
# NOTE: Do we want to extend Familia::DataType here? That would make it
|
166
162
|
# possible to call safe_dump on relations fields (e.g. list, zset, hashkey).
|
167
163
|
#
|
168
|
-
# The challenge is that DataType classes (List,
|
164
|
+
# The challenge is that DataType classes (List, UnsortedSet, etc.) are shared across
|
169
165
|
# all Horreum models. If Customer extends DataType with safe_dump, then
|
170
166
|
# Session's lists would also have it. Not ideal. If that's all we wanted
|
171
167
|
# then we can do that by looping through every DataType class here.
|
data/lib/familia/field_type.rb
CHANGED
@@ -116,6 +116,8 @@ 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
|
+
|
119
121
|
end
|
120
122
|
end
|
121
123
|
end
|
@@ -145,7 +147,7 @@ module Familia
|
|
145
147
|
|
146
148
|
begin
|
147
149
|
# Trace the operation if debugging is enabled
|
148
|
-
Familia.trace :FAST_WRITER,
|
150
|
+
Familia.trace :FAST_WRITER, nil, "#{field_name}: #{val.inspect}" if Familia.debug?
|
149
151
|
|
150
152
|
# Convert value for database storage
|
151
153
|
prepared = serialize_value(val)
|
@@ -190,7 +192,7 @@ module Familia
|
|
190
192
|
# The default implementation passes values through unchanged.
|
191
193
|
#
|
192
194
|
# @param value [Object] The value to serialize
|
193
|
-
# @param
|
195
|
+
# @param _record [Object] The record instance (for context)
|
194
196
|
# @return [Object] The serialized value
|
195
197
|
#
|
196
198
|
def serialize(value, _record = nil)
|
@@ -203,7 +205,7 @@ module Familia
|
|
203
205
|
# The default implementation passes values through unchanged.
|
204
206
|
#
|
205
207
|
# @param value [Object] The value to deserialize
|
206
|
-
# @param
|
208
|
+
# @param _record [Object] The record instance (for context)
|
207
209
|
# @return [Object] The deserialized value
|
208
210
|
#
|
209
211
|
def deserialize(value, _record = nil)
|
@@ -228,7 +230,7 @@ module Familia
|
|
228
230
|
"method_name=#{@method_name}",
|
229
231
|
"fast_method_name=#{@fast_method_name}",
|
230
232
|
"on_conflict=#{@on_conflict}",
|
231
|
-
"category=#{category}"
|
233
|
+
"category=#{category}",
|
232
234
|
]
|
233
235
|
"#<#{self.class.name} #{attributes.join(' ')}>"
|
234
236
|
end
|