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
data/docs/overview.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
# Familia - Overview
|
3
3
|
|
4
4
|
> [!NOTE]
|
5
|
-
> This document refers to Valkey throughout, but all examples and patterns work identically with Redis. Familia supports both Valkey and Redis as they share the same protocol and data structures.
|
5
|
+
> This document refers to Valkey throughout, but all examples and patterns work identically with Valkey/Redis. Familia supports both Valkey and Valkey/Redis as they share the same protocol and data structures.
|
6
6
|
|
7
7
|
## Introduction
|
8
8
|
|
@@ -11,13 +11,14 @@ Familia is a Ruby ORM for Valkey (Redis) that provides object-oriented access to
|
|
11
11
|
**Why Familia?**
|
12
12
|
- Maps Ruby objects directly to Valkey's native data structures (strings, lists, sets, etc.)
|
13
13
|
- Maintains Valkey's atomic operations and performance characteristics
|
14
|
-
- Handles complex patterns (quantization, encryption, expiration) out of the box
|
14
|
+
- Handles complex patterns (quantization, encryption, expiration, relationships) out of the box
|
15
|
+
- Modular feature system for organizing functionality across complex projects
|
15
16
|
|
16
17
|
## Core Concepts
|
17
18
|
|
18
19
|
### What is a Horreum Class?
|
19
20
|
|
20
|
-
The
|
21
|
+
The `Horreum` class is Familia's foundation, representing Valkey-compatible objects. It's named after ancient Roman storehouses, reflecting its purpose as a structured data repository.
|
21
22
|
|
22
23
|
```ruby
|
23
24
|
class Flower < Familia::Horreum
|
@@ -95,6 +96,82 @@ end
|
|
95
96
|
|
96
97
|
Each type maintains Valkey's native operations while providing Ruby-friendly interfaces.
|
97
98
|
|
99
|
+
### DataType Naming Options
|
100
|
+
|
101
|
+
Familia provides both traditional concise names and explicit names for DataType methods to avoid namespace confusion with Ruby core types:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
class Product < Familia::Horreum
|
105
|
+
# Traditional naming (concise, safe for lowercase)
|
106
|
+
string :view_count # Creates StringKey instance
|
107
|
+
list :categories # Creates ListKey instance
|
108
|
+
|
109
|
+
# Explicit naming (clear intent, namespace-safe)
|
110
|
+
stringkey :description # Creates StringKey instance
|
111
|
+
listkey :history # Creates ListKey instance
|
112
|
+
|
113
|
+
# Both work identically - choose based on preference
|
114
|
+
set :tags # UnsortedSet (unchanged)
|
115
|
+
sorted_set :ratings # SortedSet (unchanged)
|
116
|
+
hashkey :attributes # HashKey (unchanged)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Access patterns are identical
|
120
|
+
product.view_count.class # => Familia::StringKey
|
121
|
+
product.description.class # => Familia::StringKey
|
122
|
+
product.categories.class # => Familia::ListKey
|
123
|
+
product.history.class # => Familia::ListKey
|
124
|
+
```
|
125
|
+
|
126
|
+
**Key Benefits:**
|
127
|
+
- **Developer Choice**: Use concise (`string`, `list`) or explicit (`stringkey`, `listkey`) method names
|
128
|
+
- **Namespace Safety**: No confusion with Ruby's core `String`, `Array`, `Set`, `Hash` types
|
129
|
+
- **Backward Compatibility**: All existing code continues to work unchanged
|
130
|
+
- **Future-Proof**: Clear naming convention for any new DataTypes
|
131
|
+
|
132
|
+
### Generated Method Patterns
|
133
|
+
|
134
|
+
Familia automatically generates methods for all field and data type declarations:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
class Product < Familia::Horreum
|
138
|
+
# Field declarations generate three methods each
|
139
|
+
field :name, :price
|
140
|
+
# → name, name=, name! # getter, setter, fast writer
|
141
|
+
# → price, price=, price! # getter, setter, fast writer
|
142
|
+
|
143
|
+
# Data type declarations generate accessor, setter, and type check
|
144
|
+
set :tags
|
145
|
+
list :categories # Traditional method
|
146
|
+
listkey :search_history # Explicit method (same functionality)
|
147
|
+
hashkey :attributes
|
148
|
+
# → tags, tags=, tags? # accessor, setter, type check
|
149
|
+
# → categories, categories=, categories?
|
150
|
+
# → search_history, search_history=, search_history?
|
151
|
+
# → attributes, attributes=, attributes?
|
152
|
+
|
153
|
+
# Class-level data types
|
154
|
+
class_set :global_tags
|
155
|
+
class_counter :total_products
|
156
|
+
# → Product.global_tags, Product.global_tags?
|
157
|
+
# → Product.total_products, Product.total_products?
|
158
|
+
end
|
159
|
+
|
160
|
+
# Field method usage
|
161
|
+
product.name = "Ruby Gem" # Set field value
|
162
|
+
product.name # Get field value
|
163
|
+
product.name!("New Name") # Set and save immediately
|
164
|
+
|
165
|
+
# Data type method usage
|
166
|
+
product.tags # Get UnsortedSet instance
|
167
|
+
product.tags = new_set # Replace UnsortedSet instance
|
168
|
+
product.tags? # => true (confirms it's an UnsortedSet)
|
169
|
+
|
170
|
+
# Method conflict resolution
|
171
|
+
field :type, on_conflict: :skip # Skip if method exists
|
172
|
+
field :id, on_conflict: :overwrite # Force overwrite existing method
|
173
|
+
```
|
174
|
+
|
98
175
|
## Essential Features
|
99
176
|
|
100
177
|
### Automatic Expiration
|
@@ -133,7 +210,7 @@ class User < Familia::Horreum
|
|
133
210
|
end
|
134
211
|
|
135
212
|
user.safe_dump
|
136
|
-
|
213
|
+
# => {id: "123", email: "alice@example.com", full_name: "Alice Windows"}
|
137
214
|
```
|
138
215
|
|
139
216
|
The new DSL prevents accidental exposure of sensitive data and makes field definitions easier to organize in feature modules.
|
@@ -151,6 +228,292 @@ end
|
|
151
228
|
|
152
229
|
This automatically groups metrics into 10-minute intervals formatted as "HH:MM", ideal for analytics dashboards.
|
153
230
|
|
231
|
+
**Key Benefits:**
|
232
|
+
- **Time Bucketing**: Group time-based data into configurable intervals (minutes, hours, days)
|
233
|
+
- **Reduced Storage**: Aggregate similar data points to optimize memory usage
|
234
|
+
- **Analytics Ready**: Perfect for dashboards and time-series data visualization
|
235
|
+
|
236
|
+
> For advanced quantization strategies, value bucketing, geographic quantization, and performance patterns, see the [Technical Reference](reference/api-technical.md#quantization-feature-v200-pre7).
|
237
|
+
|
238
|
+
### Object Identifiers
|
239
|
+
|
240
|
+
Automatically generate unique identifiers for objects:
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
class Document < Familia::Horreum
|
244
|
+
feature :object_identifier, generator: :uuid_v4
|
245
|
+
field :title
|
246
|
+
field :content
|
247
|
+
end
|
248
|
+
|
249
|
+
class Session < Familia::Horreum
|
250
|
+
feature :object_identifier, generator: :hex
|
251
|
+
field :user_id
|
252
|
+
field :data
|
253
|
+
end
|
254
|
+
|
255
|
+
# Objects get automatic IDs
|
256
|
+
doc = Document.create(title: "My Doc")
|
257
|
+
doc.objid # => "550e8400-e29b-41d4-a716-446655440000" (UUID)
|
258
|
+
|
259
|
+
session = Session.create(user_id: "123")
|
260
|
+
session.objid # => "a1b2c3d4e5f6" (hex)
|
261
|
+
```
|
262
|
+
|
263
|
+
**Available Generators:**
|
264
|
+
- `:uuid_v4` - Standard UUID format for global uniqueness
|
265
|
+
- `:hex` - Compact hexadecimal identifiers for internal use
|
266
|
+
|
267
|
+
> For custom generators, collision detection, and advanced identifier patterns, see the [Technical Reference](reference/api-technical.md#object-identifier-feature-v200-pre7).
|
268
|
+
|
269
|
+
### Specialized Field Types
|
270
|
+
|
271
|
+
Familia provides specialized field types beyond basic fields:
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
class SecureModel < Familia::Horreum
|
275
|
+
feature :encrypted_fields
|
276
|
+
feature :transient_fields
|
277
|
+
feature :object_identifier
|
278
|
+
|
279
|
+
# Regular fields generate: name, name=, name!
|
280
|
+
field :name, :email
|
281
|
+
|
282
|
+
# Encrypted fields return ConcealedString instances
|
283
|
+
encrypted_field :api_key, :credit_card
|
284
|
+
# → api_key, api_key=, api_key!
|
285
|
+
# → Values wrapped in ConcealedString for safety
|
286
|
+
|
287
|
+
# Transient fields never persist to database
|
288
|
+
transient_field :password, :session_token
|
289
|
+
# → password, password= (no fast writer method)
|
290
|
+
# → Values wrapped in RedactedString
|
291
|
+
|
292
|
+
# Redacted fields are persisted but return [REDACTED] in logs
|
293
|
+
redacted_field :security_question
|
294
|
+
# → security_question, security_question=, security_question!
|
295
|
+
|
296
|
+
# Object identifier fields auto-generate unique IDs
|
297
|
+
# → objid, objid= (lazy generation, preserves initialization values)
|
298
|
+
# → objid_generator_used (provenance tracking)
|
299
|
+
end
|
300
|
+
|
301
|
+
# Usage examples
|
302
|
+
model = SecureModel.create(name: "Alice", api_key: "secret123")
|
303
|
+
|
304
|
+
# Encrypted field safety
|
305
|
+
model.api_key.class # => ConcealedString
|
306
|
+
model.api_key.to_s # => "[CONCEALED]" (safe for logs)
|
307
|
+
model.api_key.reveal # => "secret123" (actual value)
|
308
|
+
|
309
|
+
# Transient field behavior
|
310
|
+
model.password = "temp123"
|
311
|
+
model.save
|
312
|
+
model.reload
|
313
|
+
model.password # => nil (not persisted)
|
314
|
+
|
315
|
+
# Object identifier generation
|
316
|
+
model.objid # => Auto-generated UUID or hex
|
317
|
+
model.objid_generator_used # => :uuid_v7 (provenance)
|
318
|
+
```
|
319
|
+
|
320
|
+
**Field Type Features:**
|
321
|
+
- **Method Conflict Resolution**: Use `on_conflict: :skip/:warn/:overwrite` for existing methods
|
322
|
+
- **Fast Writer Control**: Use `fast_method: false` to disable fast writers
|
323
|
+
- **Custom Method Names**: Use `as: :custom_name` for different method names
|
324
|
+
- **Security by Default**: Encrypted and transient fields prevent accidental exposure
|
325
|
+
|
326
|
+
### External Identifiers
|
327
|
+
|
328
|
+
Integrate with external systems and validate identifiers:
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
class ExternalUser < Familia::Horreum
|
332
|
+
feature :external_identifier
|
333
|
+
|
334
|
+
field :external_id
|
335
|
+
field :name
|
336
|
+
field :sync_status
|
337
|
+
|
338
|
+
# Validate external system identifiers
|
339
|
+
def valid_external_id?
|
340
|
+
external_id.present? && external_id.match?(/^ext_\d+$/)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Map external identifiers to internal objects
|
345
|
+
user = ExternalUser.create(external_id: "ext_12345", name: "Alice")
|
346
|
+
```
|
347
|
+
|
348
|
+
This feature helps maintain consistency when integrating with external APIs or legacy systems.
|
349
|
+
|
350
|
+
> For advanced external identifier patterns, batch operations, and sync status management, see the [Technical Reference](reference/api-technical.md#external-identifier-feature-v200-pre7).
|
351
|
+
|
352
|
+
### Relationships
|
353
|
+
|
354
|
+
Manage complex object relationships with CRUD operations:
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
|
358
|
+
# Define relationships
|
359
|
+
class User < Familia::Horreum
|
360
|
+
feature :relationships
|
361
|
+
identifier_field :email
|
362
|
+
field :email, :name
|
363
|
+
|
364
|
+
participates_in Team, :teams
|
365
|
+
end
|
366
|
+
|
367
|
+
class Team < Familia::Horreum
|
368
|
+
feature :relationships
|
369
|
+
identifier_field :name
|
370
|
+
field :name, :description
|
371
|
+
end
|
372
|
+
|
373
|
+
# Create relationships
|
374
|
+
alice = User.create(email: "alice@example.com", name: "Alice")
|
375
|
+
dev_team = Team.create(name: "developers", description: "Dev Team")
|
376
|
+
|
377
|
+
# Add relationships
|
378
|
+
alice.teams << dev_team
|
379
|
+
|
380
|
+
# Query relationships
|
381
|
+
alice.teams.to_a # => [dev_team identifiers]
|
382
|
+
dev_team.in_user_teams?(alice) # => true
|
383
|
+
|
384
|
+
# Remove relationships
|
385
|
+
alice.teams.delete(dev_team.identifier)
|
386
|
+
|
387
|
+
# Bulk operations
|
388
|
+
alice.teams.merge([qa_team.identifier, design_team.identifier])
|
389
|
+
alice.teams.clear
|
390
|
+
```
|
391
|
+
|
392
|
+
#### Generated Relationship Method Patterns
|
393
|
+
|
394
|
+
The relationships feature automatically generates comprehensive method patterns:
|
395
|
+
|
396
|
+
```ruby
|
397
|
+
# Participation methods (on User class)
|
398
|
+
alice.in_team_teams?(dev_team) # Check membership
|
399
|
+
alice.add_to_team_teams(dev_team, 1.0) # Add with score
|
400
|
+
alice.remove_from_team_teams(dev_team) # Remove membership
|
401
|
+
alice.score_in_team_teams(dev_team) # Get participation score
|
402
|
+
|
403
|
+
# Target class methods (on Team class)
|
404
|
+
dev_team.teams # Collection getter (SortedSet)
|
405
|
+
dev_team.add_team(alice, 1.0) # Add single member with score
|
406
|
+
dev_team.remove_team(alice) # Remove single member
|
407
|
+
dev_team.add_teams([alice, bob]) # Bulk add members
|
408
|
+
|
409
|
+
# Indexing methods (if using indexed_by)
|
410
|
+
class User < Familia::Horreum
|
411
|
+
indexed_by :email, :email_index, target: Team
|
412
|
+
end
|
413
|
+
|
414
|
+
# Generated index methods on User:
|
415
|
+
alice.add_to_team_email_index(dev_team) # Add to index
|
416
|
+
alice.remove_from_team_email_index(dev_team) # Remove from index
|
417
|
+
alice.update_in_team_email_index(dev_team, old_email) # Update index
|
418
|
+
|
419
|
+
# Generated finder methods on Team:
|
420
|
+
Team.find_by_email("alice@example.com") # Find user by email
|
421
|
+
Team.find_all_by_email(["alice@example.com"]) # Bulk find by emails
|
422
|
+
Team.email_index_for("alice@example.com") # Direct index access
|
423
|
+
```
|
424
|
+
|
425
|
+
**Key Features:**
|
426
|
+
- **Bidirectional Links**: Automatic reverse relationship management
|
427
|
+
- **Ruby-like Syntax**: Clean `customer.domains << domain` collection operations
|
428
|
+
- **Automatic Indexing**: Efficient O(1) lookups with automatic index maintenance
|
429
|
+
- **Performance Optimized**: Bulk operations and efficient sorted set operations
|
430
|
+
|
431
|
+
> For advanced relationship patterns, permission-encoded relationships, time-series tracking, and performance optimization, see the [Technical Reference](reference/api-technical.md#relationships-feature-v200-pre7).
|
432
|
+
|
433
|
+
### Transient Fields
|
434
|
+
|
435
|
+
Handle temporary or sensitive data that shouldn't persist:
|
436
|
+
|
437
|
+
```ruby
|
438
|
+
class LoginAttempt < Familia::Horreum
|
439
|
+
feature :transient_fields
|
440
|
+
|
441
|
+
field :username
|
442
|
+
field :timestamp
|
443
|
+
transient_field :password
|
444
|
+
redacted_field :security_token
|
445
|
+
end
|
446
|
+
|
447
|
+
attempt = LoginAttempt.new(
|
448
|
+
username: "alice",
|
449
|
+
password: "secret123",
|
450
|
+
security_token: "sensitive_data"
|
451
|
+
)
|
452
|
+
|
453
|
+
# Transient fields aren't saved to Valkey
|
454
|
+
attempt.save
|
455
|
+
attempt.reload
|
456
|
+
attempt.password # => nil (not persisted)
|
457
|
+
|
458
|
+
# Redacted fields return safe values
|
459
|
+
attempt.security_token.class # => RedactedString
|
460
|
+
attempt.security_token.to_s # => "[REDACTED]"
|
461
|
+
attempt.security_token.reveal # => "sensitive_data"
|
462
|
+
```
|
463
|
+
|
464
|
+
**Field Types:**
|
465
|
+
- **Transient Fields**: Exist only in memory, never persisted
|
466
|
+
- **Redacted Fields**: Return `[REDACTED]` when converted to strings for logging safety
|
467
|
+
|
468
|
+
> For RedactedString implementation details, single-use patterns, and security considerations, see the [Technical Reference](reference/api-technical.md#transient-fields-feature-v200-pre5).
|
469
|
+
|
470
|
+
### Permission Management
|
471
|
+
|
472
|
+
The relationships feature includes a powerful permission management system:
|
473
|
+
|
474
|
+
```ruby
|
475
|
+
class Document < Familia::Horreum
|
476
|
+
feature :relationships
|
477
|
+
permission_tracking :user_permissions
|
478
|
+
|
479
|
+
field :title, :content
|
480
|
+
end
|
481
|
+
|
482
|
+
# Generated permission control methods
|
483
|
+
doc.grant(user, :read, :write) # Grant permissions to user
|
484
|
+
doc.revoke(user, :write) # Revoke specific permissions
|
485
|
+
doc.add_permission(user, :delete) # Add to existing permissions
|
486
|
+
doc.set_permissions(user, :read, :edit) # Replace all permissions
|
487
|
+
|
488
|
+
# Generated permission query methods
|
489
|
+
doc.can?(user, :read) # Check if user has permission
|
490
|
+
doc.permissions_for(user) # Get user's permission array
|
491
|
+
doc.category?(user, :content_editor) # Check permission category
|
492
|
+
doc.permission_tier_for(user) # Get tier: :administrator, :content_editor, :viewer, :none
|
493
|
+
|
494
|
+
# Generated bulk operations
|
495
|
+
doc.all_permissions # Hash of all users and permissions
|
496
|
+
doc.clear_all_permissions # Remove all permissions
|
497
|
+
doc.users_by_category(:viewer) # Filter users by permission level
|
498
|
+
|
499
|
+
# Generated collection filtering
|
500
|
+
doc.accessible_items("org:123:documents") # Get items with scores
|
501
|
+
doc.items_by_permission("org:123:documents", :readable) # Filter by permission
|
502
|
+
doc.permission_matrix("org:123:documents") # Count by permission level
|
503
|
+
doc.admin_access?(user, "org:123:documents") # Check admin privileges
|
504
|
+
```
|
505
|
+
|
506
|
+
**Permission Categories:**
|
507
|
+
- `:viewer` - Read-only access
|
508
|
+
- `:content_editor` - Read and edit access
|
509
|
+
- `:administrator` - Full access including user management
|
510
|
+
|
511
|
+
**Key Features:**
|
512
|
+
- **Granular Control**: Fine-grained permission assignment per user
|
513
|
+
- **Category-based Queries**: Efficient filtering by permission levels
|
514
|
+
- **Bulk Operations**: Manage permissions across collections
|
515
|
+
- **Performance Optimized**: O(1) permission checks using Valkey/Redis sorted sets
|
516
|
+
|
154
517
|
## Advanced Patterns
|
155
518
|
|
156
519
|
### Custom Methods and Logic
|
@@ -182,7 +545,7 @@ Execute multiple Valkey commands atomically:
|
|
182
545
|
```ruby
|
183
546
|
user.transaction do |conn|
|
184
547
|
conn.set("user:#{user.id}:status", "active")
|
185
|
-
conn.zadd("active_users",
|
548
|
+
conn.zadd("active_users", Familia.now.to_i, user.id)
|
186
549
|
end
|
187
550
|
```
|
188
551
|
|
@@ -209,21 +572,66 @@ This ensures efficient Valkey connection usage in multi-threaded applications.
|
|
209
572
|
|
210
573
|
### Encrypted Fields
|
211
574
|
|
212
|
-
Protect sensitive data at rest:
|
575
|
+
Protect sensitive data at rest with transparent encryption:
|
213
576
|
|
214
577
|
```ruby
|
578
|
+
# First, configure encryption keys
|
579
|
+
Familia.configure do |config|
|
580
|
+
config.encryption_keys = {
|
581
|
+
v1: ENV['FAMILIA_ENCRYPTION_KEY'],
|
582
|
+
v2: ENV['FAMILIA_ENCRYPTION_KEY_V2']
|
583
|
+
}
|
584
|
+
config.current_key_version = :v2
|
585
|
+
config.encryption_personalization = 'MyApp-2024' # Optional
|
586
|
+
end
|
587
|
+
|
588
|
+
# Validate configuration before use
|
589
|
+
Familia::Encryption.validate_configuration!
|
590
|
+
|
215
591
|
class SecureUser < Familia::Horreum
|
216
592
|
feature :encrypted_fields
|
217
593
|
|
218
594
|
identifier_field :id
|
219
|
-
field :id
|
220
|
-
|
221
|
-
encrypted_field :
|
222
|
-
encrypted_field :notes, aad_fields: [:id, :email] # With
|
595
|
+
field :id, :email # Plaintext fields
|
596
|
+
encrypted_field :ssn # Encrypted with field-specific key
|
597
|
+
encrypted_field :credit_card # Another encrypted field
|
598
|
+
encrypted_field :notes, aad_fields: [:id, :email] # With tamper protection
|
599
|
+
end
|
600
|
+
|
601
|
+
# Usage is transparent
|
602
|
+
user = SecureUser.new(
|
603
|
+
id: 'user123',
|
604
|
+
email: 'alice@example.com',
|
605
|
+
ssn: '123-45-6789',
|
606
|
+
credit_card: '4111-1111-1111-1111',
|
607
|
+
notes: 'VIP customer'
|
608
|
+
)
|
609
|
+
user.save
|
610
|
+
|
611
|
+
# Access returns ConcealedString to help prevent accidentally logging or displaying the value
|
612
|
+
user.ssn.class # => ConcealedString
|
613
|
+
user.ssn.reveal # => "123-45-6789" (actual value)
|
614
|
+
user.ssn.to_s # => "[CONCEALED]" (safe for logging)
|
615
|
+
|
616
|
+
# Performance optimization for multiple operations
|
617
|
+
Familia::Encryption.with_request_cache do
|
618
|
+
user.ssn = "new-ssn"
|
619
|
+
user.credit_card = "new-card"
|
620
|
+
user.save # Reuses derived keys
|
223
621
|
end
|
622
|
+
|
623
|
+
# Key rotation support
|
624
|
+
user.re_encrypt_fields! # Re-encrypt with current key version
|
625
|
+
user.encrypted_fields_status # Check encryption status
|
224
626
|
```
|
225
627
|
|
226
|
-
|
628
|
+
**Key Features:**
|
629
|
+
- **Transparent Encryption**: Fields encrypted/decrypted automatically
|
630
|
+
- **Security by Default**: ConcealedString prevents accidental value exposure
|
631
|
+
- **Key Rotation**: Seamless updates with backward compatibility
|
632
|
+
- **Multiple Algorithms**: XChaCha20-Poly1305 (preferred) with AES-256-GCM fallback
|
633
|
+
|
634
|
+
> For advanced encryption configuration, multiple providers, request caching, and key rotation procedures, see the [Technical Reference](reference/api-technical.md#encrypted-fields-feature-v200-pre5).
|
227
635
|
|
228
636
|
### Open-ended Serialization
|
229
637
|
|
@@ -243,6 +651,28 @@ end
|
|
243
651
|
|
244
652
|
Enables integration with custom serialization formats beyond Familia's defaults.
|
245
653
|
|
654
|
+
### Feature System Architecture
|
655
|
+
|
656
|
+
Familia's modular feature system helps organize functionality across complex projects:
|
657
|
+
|
658
|
+
```ruby
|
659
|
+
class ComplexModel < Familia::Horreum
|
660
|
+
# Enable features as needed
|
661
|
+
feature :expiration # TTL management
|
662
|
+
feature :safe_dump # API-safe serialization
|
663
|
+
feature :relationships # Object relationships
|
664
|
+
feature :encrypted_fields # Secure field storage
|
665
|
+
end
|
666
|
+
```
|
667
|
+
|
668
|
+
**Key Benefits:**
|
669
|
+
- **Per-Class Configuration**: Each model can configure features independently
|
670
|
+
- **Automatic Loading**: Use autoloader for large projects to organize features in separate files
|
671
|
+
- **Dependency Management**: Features can depend on other features for complex functionality
|
672
|
+
- **Reusable Modules**: Share common functionality across multiple models
|
673
|
+
|
674
|
+
> For advanced feature organization patterns, autoloader configuration, and complex dependency management, see the [Technical Reference](reference/api-technical.md#advanced-feature-system-architecture).
|
675
|
+
|
246
676
|
## Configuration
|
247
677
|
|
248
678
|
### Basic Setup
|
@@ -260,17 +690,83 @@ Familia.redis_config = {
|
|
260
690
|
}
|
261
691
|
```
|
262
692
|
|
263
|
-
###
|
693
|
+
### Production Configuration
|
694
|
+
|
695
|
+
**Environment-based Setup:**
|
696
|
+
```ruby
|
697
|
+
# config/familia.rb
|
698
|
+
case ENV['RAILS_ENV'] || ENV['RACK_ENV']
|
699
|
+
when 'production'
|
700
|
+
Familia.redis_config = {
|
701
|
+
host: ENV['REDIS_HOST'],
|
702
|
+
port: ENV['REDIS_PORT'],
|
703
|
+
password: ENV['REDIS_PASSWORD'],
|
704
|
+
ssl: true,
|
705
|
+
timeout: 10,
|
706
|
+
reconnect_attempts: 3
|
707
|
+
}
|
708
|
+
when 'development'
|
709
|
+
Familia.uri = 'redis://localhost:6379/0'
|
710
|
+
when 'test'
|
711
|
+
Familia.uri = 'redis://localhost:6379/15'
|
712
|
+
end
|
713
|
+
```
|
714
|
+
|
715
|
+
**Advanced Connection Pooling:**
|
716
|
+
```ruby
|
717
|
+
# Multi-database with connection pooling
|
718
|
+
require 'connection_pool'
|
719
|
+
|
720
|
+
primary_pool = ConnectionPool.new(size: 20) { Redis.new(url: ENV['PRIMARY_REDIS_URL']) }
|
721
|
+
cache_pool = ConnectionPool.new(size: 10) { Redis.new(url: ENV['CACHE_REDIS_URL']) }
|
722
|
+
|
723
|
+
Familia.connection_provider = lambda do |uri|
|
724
|
+
case uri
|
725
|
+
when /primary/
|
726
|
+
primary_pool.with { |conn| yield conn }
|
727
|
+
when /cache/
|
728
|
+
cache_pool.with { |conn| yield conn }
|
729
|
+
else
|
730
|
+
Redis.new(url: uri)
|
731
|
+
end
|
732
|
+
end
|
733
|
+
```
|
734
|
+
|
735
|
+
### Encryption Setup
|
264
736
|
|
737
|
+
**Development Keys:**
|
265
738
|
```ruby
|
266
739
|
# Generate base64-encoded 32-byte keys
|
267
|
-
Familia.
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
740
|
+
Familia.configure do |config|
|
741
|
+
config.encryption_keys = {
|
742
|
+
v1: Base64.strict_encode64(SecureRandom.bytes(32)),
|
743
|
+
v2: Base64.strict_encode64(SecureRandom.bytes(32))
|
744
|
+
}
|
745
|
+
config.current_key_version = :v2
|
746
|
+
config.encryption_personalization = "#{Rails.application.class.name}-#{Rails.env}"
|
747
|
+
end
|
748
|
+
```
|
749
|
+
|
750
|
+
**Production Security:**
|
751
|
+
```ruby
|
752
|
+
# Use secure key management
|
753
|
+
Familia.configure do |config|
|
754
|
+
# Load keys from secure key management service
|
755
|
+
config.encryption_keys = {
|
756
|
+
v1: ENV['FAMILIA_ENCRYPTION_KEY_V1'],
|
757
|
+
v2: ENV['FAMILIA_ENCRYPTION_KEY_V2'],
|
758
|
+
v3: ENV['FAMILIA_ENCRYPTION_KEY_V3'] # For rotation
|
759
|
+
}
|
760
|
+
config.current_key_version = :v3
|
761
|
+
config.encryption_personalization = ENV['FAMILIA_ENCRYPTION_CONTEXT']
|
762
|
+
|
763
|
+
# Validate configuration on startup
|
764
|
+
Familia::Encryption.validate_configuration!
|
765
|
+
end
|
272
766
|
```
|
273
767
|
|
768
|
+
> For production configuration patterns, advanced connection pooling, multi-database setup, and environment-based configuration, see the [Technical Reference](reference/api-technical.md#connection-management-v200-pre).
|
769
|
+
|
274
770
|
## Common Patterns
|
275
771
|
|
276
772
|
### Bulk Operations
|
@@ -282,7 +778,7 @@ users = User.multiget('alice@example.com', 'bob@example.com')
|
|
282
778
|
# Batch operations
|
283
779
|
User.transaction do |conn|
|
284
780
|
conn.set('user:alice:status', 'active')
|
285
|
-
conn.zadd('active_users',
|
781
|
+
conn.zadd('active_users', Familia.now.to_i, 'alice')
|
286
782
|
end
|
287
783
|
```
|
288
784
|
|
@@ -313,13 +809,44 @@ Familia.connect_to_uri('redis://localhost:6379/0')
|
|
313
809
|
```ruby
|
314
810
|
# Debug key names
|
315
811
|
user = User.new(email: 'test@example.com')
|
316
|
-
puts user.
|
812
|
+
puts user.dbkey # Shows the Valkey key that would be used
|
317
813
|
```
|
318
814
|
|
319
815
|
**Encryption Issues:**
|
320
816
|
```ruby
|
321
817
|
# Validate encryption config
|
322
818
|
Familia::Encryption.validate_configuration!
|
819
|
+
|
820
|
+
# Check encryption status for specific fields
|
821
|
+
user.encrypted_fields_status
|
822
|
+
# => {ssn: {encrypted: true, key_version: :v2}, credit_card: {encrypted: false}}
|
823
|
+
|
824
|
+
# Re-encrypt all fields with current key
|
825
|
+
user.re_encrypt_fields!
|
826
|
+
```
|
827
|
+
|
828
|
+
**Relationship Issues:**
|
829
|
+
```ruby
|
830
|
+
# Debug relationship indexes
|
831
|
+
alice.relationships_debug_info
|
832
|
+
# => Shows internal relationship state and indexes
|
833
|
+
|
834
|
+
# Check relationship consistency
|
835
|
+
User.validate_relationship_indexes! # Raises if inconsistent
|
836
|
+
```
|
837
|
+
|
838
|
+
**Feature Conflicts:**
|
839
|
+
```ruby
|
840
|
+
# Check which features are enabled
|
841
|
+
MyModel.features_enabled
|
842
|
+
# => [:safe_dump, :encrypted_fields, :relationships]
|
843
|
+
|
844
|
+
# Check feature dependencies
|
845
|
+
MyModel.feature_dependencies(:relationships)
|
846
|
+
# => Shows required features
|
847
|
+
|
848
|
+
# Verify feature loading order
|
849
|
+
Familia.debug = true # Shows feature loading sequence
|
323
850
|
```
|
324
851
|
|
325
852
|
### Debug Mode
|
@@ -355,4 +882,81 @@ Familia.config.current_key_version = :v1
|
|
355
882
|
def clear_redis
|
356
883
|
Familia.redis.flushdb
|
357
884
|
end
|
885
|
+
|
886
|
+
# Feature-specific testing patterns
|
887
|
+
def setup_encryption_for_tests
|
888
|
+
test_keys = {
|
889
|
+
v1: Base64.strict_encode64('a' * 32),
|
890
|
+
v2: Base64.strict_encode64('b' * 32)
|
891
|
+
}
|
892
|
+
Familia.configure do |config|
|
893
|
+
config.encryption_keys = test_keys
|
894
|
+
config.current_key_version = :v1
|
895
|
+
config.encryption_personalization = 'TestApp-Test'
|
896
|
+
end
|
897
|
+
end
|
898
|
+
|
899
|
+
def test_relationships_cleanup
|
900
|
+
# Clean up relationship indexes
|
901
|
+
Familia.redis.keys('*:relationships:*').each do |key|
|
902
|
+
Familia.redis.del(key)
|
903
|
+
end
|
904
|
+
end
|
905
|
+
```
|
906
|
+
|
907
|
+
### Feature Testing Strategies
|
908
|
+
|
909
|
+
**Testing with Encrypted Fields:**
|
910
|
+
```ruby
|
911
|
+
# test/models/secure_user_test.rb
|
912
|
+
require 'test_helper'
|
913
|
+
|
914
|
+
class SecureUserTest < Minitest::Test
|
915
|
+
def setup
|
916
|
+
setup_encryption_for_tests
|
917
|
+
clear_redis
|
918
|
+
end
|
919
|
+
|
920
|
+
def test_encrypted_field_concealment
|
921
|
+
user = SecureUser.create(
|
922
|
+
id: 'test123',
|
923
|
+
email: 'test@example.com',
|
924
|
+
ssn: '123-45-6789'
|
925
|
+
)
|
926
|
+
|
927
|
+
assert_instance_of Familia::Features::EncryptedFields::ConcealedString, user.ssn
|
928
|
+
assert_equal '[CONCEALED]', user.ssn.to_s
|
929
|
+
assert_equal '123-45-6789', user.ssn.reveal
|
930
|
+
end
|
931
|
+
end
|
932
|
+
```
|
933
|
+
|
934
|
+
**Testing Relationships:**
|
935
|
+
```ruby
|
936
|
+
def test_relationship_bidirectionality
|
937
|
+
alice = User.create(email: "alice@test.com")
|
938
|
+
team = Team.create(name: "test-team")
|
939
|
+
|
940
|
+
alice.add_membership(team)
|
941
|
+
|
942
|
+
assert_includes alice.memberships, team
|
943
|
+
assert_includes team.members, alice
|
944
|
+
end
|
945
|
+
```
|
946
|
+
|
947
|
+
**Testing Transient Fields:**
|
948
|
+
```ruby
|
949
|
+
def test_transient_field_not_persisted
|
950
|
+
attempt = LoginAttempt.new(
|
951
|
+
username: "alice",
|
952
|
+
password: "secret"
|
953
|
+
)
|
954
|
+
attempt.save
|
955
|
+
|
956
|
+
reloaded = LoginAttempt.load(attempt.identifier)
|
957
|
+
assert_nil reloaded.password # Not persisted
|
958
|
+
assert_equal "alice", reloaded.username # Regular field persisted
|
959
|
+
end
|
358
960
|
```
|
961
|
+
|
962
|
+
> For comprehensive testing patterns, advanced test helpers, and feature-specific testing strategies, see the [Technical Reference](reference/api-technical.md#testing-patterns).
|