familia 2.0.0.pre18 → 2.0.0.pre21
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/claude-code-review.yml +4 -9
- data/.github/workflows/code-smells.yml +64 -3
- data/.pre-commit-config.yaml +8 -6
- data/.reek.yml +10 -9
- data/.rubocop.yml +4 -0
- data/CHANGELOG.rst +205 -88
- data/CLAUDE.md +62 -10
- data/Gemfile +3 -3
- data/Gemfile.lock +27 -62
- data/README.md +39 -0
- data/bin/try +16 -0
- data/bin/tryouts +16 -0
- data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
- data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
- data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
- data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
- data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
- data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
- data/docs/1106-participates_in-bidirectional-solution.md +129 -0
- data/docs/guides/encryption.md +486 -0
- data/docs/guides/feature-encrypted-fields.md +123 -7
- data/docs/guides/feature-expiration.md +177 -133
- data/docs/guides/feature-external-identifiers.md +415 -443
- data/docs/guides/feature-object-identifiers.md +400 -269
- data/docs/guides/feature-quantization.md +120 -6
- data/docs/guides/feature-relationships-indexing.md +318 -0
- data/docs/guides/feature-relationships-methods.md +146 -604
- data/docs/guides/feature-relationships-participation.md +263 -0
- data/docs/guides/feature-relationships.md +118 -136
- data/docs/guides/feature-system-devs.md +176 -693
- data/docs/guides/feature-system.md +119 -6
- data/docs/guides/feature-transient-fields.md +81 -0
- data/docs/guides/field-system.md +778 -0
- data/docs/guides/index.md +32 -15
- data/docs/guides/logging.md +187 -0
- data/docs/guides/optimized-loading.md +674 -0
- data/docs/guides/thread-safety-monitoring.md +61 -0
- data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/migrating/v2.0.0-pre22.md +241 -0
- data/docs/overview.md +7 -9
- data/docs/reference/api-technical.md +267 -320
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
- data/examples/autoloader/mega_customer.rb +2 -0
- data/examples/datatype_standalone.rb +282 -0
- data/examples/encrypted_fields.rb +2 -1
- data/examples/json_usage_patterns.rb +2 -0
- data/examples/relationships.rb +3 -0
- data/examples/safe_dump.rb +2 -1
- data/examples/sampling_demo.rb +53 -0
- data/examples/single_connection_transaction_confusions.rb +2 -1
- data/familia.gemspec +2 -1
- data/lib/familia/base.rb +2 -0
- data/lib/familia/connection/behavior.rb +254 -0
- data/lib/familia/connection/handlers.rb +97 -0
- data/lib/familia/connection/individual_command_proxy.rb +2 -0
- data/lib/familia/connection/middleware.rb +34 -24
- data/lib/familia/connection/operation_core.rb +3 -1
- data/lib/familia/connection/operations.rb +2 -0
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +4 -2
- data/lib/familia/connection/transaction_core.rb +75 -9
- data/lib/familia/connection.rb +21 -5
- data/lib/familia/data_type/class_methods.rb +3 -1
- data/lib/familia/data_type/connection.rb +153 -7
- data/lib/familia/data_type/database_commands.rb +9 -4
- data/lib/familia/data_type/serialization.rb +10 -4
- data/lib/familia/data_type/settings.rb +2 -0
- data/lib/familia/data_type/types/counter.rb +2 -0
- data/lib/familia/data_type/types/hashkey.rb +8 -6
- data/lib/familia/data_type/types/listkey.rb +2 -0
- data/lib/familia/data_type/types/lock.rb +2 -0
- data/lib/familia/data_type/types/sorted_set.rb +2 -0
- data/lib/familia/data_type/types/stringkey.rb +2 -0
- data/lib/familia/data_type/types/unsorted_set.rb +2 -0
- data/lib/familia/data_type.rb +2 -0
- data/lib/familia/encryption/encrypted_data.rb +4 -2
- data/lib/familia/encryption/manager.rb +2 -0
- data/lib/familia/encryption/provider.rb +2 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
- data/lib/familia/encryption/registry.rb +2 -0
- data/lib/familia/encryption/request_cache.rb +2 -0
- data/lib/familia/encryption.rb +9 -2
- data/lib/familia/errors.rb +53 -14
- data/lib/familia/features/autoloader.rb +2 -0
- data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
- data/lib/familia/features/encrypted_fields.rb +2 -2
- data/lib/familia/features/expiration/extensions.rb +11 -11
- data/lib/familia/features/expiration.rb +29 -21
- data/lib/familia/features/external_identifier.rb +33 -7
- data/lib/familia/features/object_identifier.rb +2 -0
- data/lib/familia/features/quantization.rb +3 -1
- data/lib/familia/features/relationships/README.md +3 -1
- data/lib/familia/features/relationships/collection_operations.rb +2 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +177 -47
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +203 -63
- data/lib/familia/features/relationships/indexing.rb +40 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +17 -5
- data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
- data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
- data/lib/familia/features/relationships/participation.rb +155 -69
- data/lib/familia/features/relationships/participation_membership.rb +69 -0
- data/lib/familia/features/relationships/participation_relationship.rb +34 -6
- data/lib/familia/features/relationships/score_encoding.rb +2 -0
- data/lib/familia/features/relationships.rb +5 -3
- data/lib/familia/features/safe_dump.rb +2 -0
- data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
- data/lib/familia/features/transient_fields.rb +2 -0
- data/lib/familia/features.rb +2 -0
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/connection.rb +28 -36
- data/lib/familia/horreum/database_commands.rb +131 -10
- data/lib/familia/horreum/definition.rb +18 -7
- data/lib/familia/horreum/management.rb +233 -57
- data/lib/familia/horreum/persistence.rb +314 -122
- data/lib/familia/horreum/related_fields.rb +2 -0
- data/lib/familia/horreum/serialization.rb +26 -4
- data/lib/familia/horreum/settings.rb +2 -0
- data/lib/familia/horreum/utils.rb +2 -8
- data/lib/familia/horreum.rb +46 -13
- data/lib/familia/identifier_extractor.rb +2 -0
- data/lib/familia/instrumentation.rb +156 -0
- data/lib/familia/json_serializer.rb +2 -0
- data/lib/familia/logging.rb +94 -37
- data/lib/familia/refinements/dear_json.rb +2 -0
- data/lib/familia/refinements/stylize_words.rb +2 -14
- data/lib/familia/refinements/time_literals.rb +2 -0
- data/lib/familia/refinements.rb +2 -0
- data/lib/familia/secure_identifier.rb +10 -2
- data/lib/familia/settings.rb +9 -7
- data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
- data/lib/familia/thread_safety/monitor.rb +328 -0
- data/lib/familia/utils.rb +13 -0
- data/lib/familia/verifiable_identifier.rb +3 -1
- data/lib/familia/version.rb +3 -1
- data/lib/familia.rb +31 -4
- data/lib/middleware/database_command_counter.rb +152 -0
- data/lib/middleware/database_logger.rb +325 -129
- data/lib/multi_result.rb +2 -0
- data/try/edge_cases/empty_identifiers_try.rb +2 -0
- data/try/edge_cases/hash_symbolization_try.rb +2 -0
- data/try/edge_cases/json_serialization_try.rb +2 -0
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
- data/try/edge_cases/race_conditions_try.rb +4 -0
- data/try/edge_cases/reserved_keywords_try.rb +4 -0
- data/try/edge_cases/string_coercion_try.rb +6 -4
- data/try/edge_cases/ttl_side_effects_try.rb +4 -0
- data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
- data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
- data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
- data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
- data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
- data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
- data/try/features/encrypted_fields/memory_security_try.rb +4 -0
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
- data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
- data/try/features/encryption/config_persistence_try.rb +4 -0
- data/try/features/encryption/core_try.rb +4 -0
- data/try/features/encryption/instance_variable_scope_try.rb +4 -0
- data/try/features/encryption/module_loading_try.rb +4 -0
- data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
- data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
- data/try/features/encryption/roundtrip_validation_try.rb +4 -0
- data/try/features/encryption/secure_memory_handling_try.rb +4 -0
- data/try/features/expiration/expiration_try.rb +5 -1
- data/try/features/external_identifier/external_identifier_try.rb +171 -8
- data/try/features/feature_dependencies_try.rb +2 -0
- data/try/features/feature_improvements_try.rb +2 -0
- data/try/features/field_groups_try.rb +2 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
- data/try/features/object_identifier/object_identifier_try.rb +2 -0
- data/try/features/quantization/quantization_try.rb +4 -0
- data/try/features/real_feature_integration_try.rb +2 -0
- data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
- data/try/features/relationships/indexing_rebuild_try.rb +600 -0
- data/try/features/relationships/indexing_try.rb +30 -4
- data/try/features/relationships/participation_bidirectional_try.rb +242 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
- data/try/features/relationships/participation_commands_verification_try.rb +2 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
- data/try/features/relationships/participation_reverse_index_try.rb +15 -13
- data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
- data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
- data/try/features/relationships/relationships_api_changes_try.rb +6 -4
- data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
- data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
- data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
- data/try/features/relationships/relationships_performance_try.rb +4 -0
- data/try/features/relationships/relationships_performance_working_try.rb +4 -0
- data/try/features/relationships/relationships_try.rb +6 -4
- data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
- data/try/features/safe_dump/safe_dump_try.rb +4 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/refresh_reset_try.rb +3 -0
- data/try/features/transient_fields/simple_refresh_test.rb +3 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
- data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
- data/try/integration/connection/fiber_context_preservation_try.rb +7 -3
- data/try/integration/connection/handler_constraints_try.rb +4 -0
- data/try/integration/connection/isolated_dbclient_try.rb +4 -0
- data/try/integration/connection/middleware_reconnect_try.rb +2 -0
- data/try/integration/connection/operation_mode_guards_try.rb +5 -1
- data/try/integration/connection/pipeline_fallback_integration_try.rb +15 -12
- data/try/integration/connection/pools_try.rb +4 -0
- data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
- data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
- data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
- data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
- data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
- data/try/integration/connection/transaction_modes_try.rb +4 -0
- data/try/integration/conventional_inheritance_try.rb +4 -0
- data/try/integration/create_method_try.rb +26 -22
- data/try/integration/cross_component_try.rb +4 -0
- data/try/integration/data_types/datatype_pipelines_try.rb +108 -0
- data/try/integration/data_types/datatype_transactions_try.rb +251 -0
- data/try/integration/database_consistency_try.rb +4 -0
- data/try/integration/familia_extended_try.rb +4 -0
- data/try/integration/familia_members_methods_try.rb +4 -0
- data/try/integration/models/customer_safe_dump_try.rb +9 -1
- data/try/integration/models/customer_try.rb +4 -0
- data/try/integration/models/datatype_base_try.rb +4 -0
- data/try/integration/models/familia_object_try.rb +5 -1
- data/try/integration/persistence_operations_try.rb +166 -10
- data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
- data/try/integration/save_methods_consistency_try.rb +241 -0
- data/try/integration/scenarios_try.rb +4 -0
- data/try/integration/secure_identifier_try.rb +4 -0
- data/try/integration/transaction_safety_core_try.rb +176 -0
- data/try/integration/transaction_safety_workflow_try.rb +291 -0
- data/try/integration/verifiable_identifier_try.rb +4 -0
- data/try/investigation/pipeline_routing/README.md +228 -0
- data/try/performance/benchmarks_try.rb +4 -0
- data/try/performance/transaction_safety_benchmark_try.rb +238 -0
- data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
- data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
- data/try/support/debugging/cache_behavior_tracer.rb +4 -0
- data/try/support/debugging/debug_aad_process.rb +3 -0
- data/try/support/debugging/debug_concealed_internal.rb +3 -0
- data/try/support/debugging/debug_concealed_reveal.rb +3 -0
- data/try/support/debugging/debug_context_aad.rb +3 -0
- data/try/support/debugging/debug_context_simple.rb +3 -0
- data/try/support/debugging/debug_cross_context.rb +3 -0
- data/try/support/debugging/debug_database_load.rb +3 -0
- data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
- data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
- data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
- data/try/support/debugging/debug_field_decrypt.rb +3 -0
- data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
- data/try/support/debugging/debug_load_path.rb +3 -0
- data/try/support/debugging/debug_method_definition.rb +3 -0
- data/try/support/debugging/debug_method_resolution.rb +3 -0
- data/try/support/debugging/debug_minimal.rb +3 -0
- data/try/support/debugging/debug_provider.rb +3 -0
- data/try/support/debugging/debug_secure_behavior.rb +3 -0
- data/try/support/debugging/debug_string_class.rb +3 -0
- data/try/support/debugging/debug_test.rb +3 -0
- data/try/support/debugging/debug_test_design.rb +3 -0
- data/try/support/debugging/encryption_method_tracer.rb +4 -0
- data/try/support/debugging/provider_diagnostics.rb +4 -0
- data/try/support/helpers/test_cleanup.rb +4 -0
- data/try/support/helpers/test_helpers.rb +5 -0
- data/try/support/memory/memory_basic_test.rb +4 -0
- data/try/support/memory/memory_detailed_test.rb +4 -0
- data/try/support/memory/memory_search_for_string.rb +4 -0
- data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
- data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
- data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
- data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
- data/try/support/prototypes/atomic_saves_v4.rb +4 -0
- data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
- data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
- data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
- data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
- data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
- data/try/support/prototypes/pooling/pool_siege.rb +4 -2
- data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
- data/try/thread_safety/README.md +496 -0
- data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
- data/try/thread_safety/connection_chain_race_try.rb +148 -0
- data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
- data/try/thread_safety/feature_registry_race_try.rb +226 -0
- data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
- data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
- data/try/thread_safety/field_registration_race_try.rb +222 -0
- data/try/thread_safety/logger_initialization_race_try.rb +170 -0
- data/try/thread_safety/middleware_registration_race_try.rb +154 -0
- data/try/thread_safety/module_config_race_try.rb +175 -0
- data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
- data/try/unit/core/autoloader_try.rb +4 -0
- data/try/unit/core/base_enhancements_try.rb +4 -0
- data/try/unit/core/connection_try.rb +4 -0
- data/try/unit/core/errors_try.rb +4 -0
- data/try/unit/core/extensions_try.rb +4 -0
- data/try/unit/core/familia_logger_try.rb +2 -0
- data/try/unit/core/familia_try.rb +4 -0
- data/try/unit/core/middleware_sampling_try.rb +335 -0
- data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
- data/try/unit/core/middleware_thread_safety_try.rb +245 -0
- data/try/unit/core/middleware_try.rb +4 -0
- data/try/unit/core/settings_try.rb +4 -0
- data/try/unit/core/time_utils_try.rb +4 -0
- data/try/unit/core/tools_try.rb +4 -0
- data/try/unit/core/utils_try.rb +37 -0
- data/try/unit/data_types/boolean_try.rb +5 -1
- data/try/unit/data_types/counter_try.rb +4 -0
- data/try/unit/data_types/datatype_base_try.rb +4 -0
- data/try/unit/data_types/hash_try.rb +4 -0
- data/try/unit/data_types/list_try.rb +4 -0
- data/try/unit/data_types/lock_try.rb +4 -0
- data/try/unit/data_types/sorted_set_try.rb +4 -0
- data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
- data/try/unit/data_types/string_try.rb +5 -1
- data/try/unit/data_types/unsortedset_try.rb +4 -0
- data/try/unit/familia_resolve_class_try.rb +116 -0
- data/try/unit/horreum/auto_indexing_on_save_try.rb +36 -16
- data/try/unit/horreum/automatic_index_validation_try.rb +255 -0
- data/try/unit/horreum/base_try.rb +5 -1
- data/try/unit/horreum/class_methods_try.rb +6 -2
- data/try/unit/horreum/commands_try.rb +4 -0
- data/try/unit/horreum/defensive_initialization_try.rb +4 -0
- data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
- data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
- data/try/unit/horreum/field_categories_try.rb +4 -0
- data/try/unit/horreum/field_definition_try.rb +4 -0
- data/try/unit/horreum/initialization_try.rb +5 -1
- data/try/unit/horreum/json_type_preservation_try.rb +2 -0
- data/try/unit/horreum/optimized_loading_try.rb +156 -0
- data/try/unit/horreum/relations_try.rb +8 -4
- data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
- data/try/unit/horreum/serialization_try.rb +6 -2
- data/try/unit/horreum/settings_try.rb +4 -0
- data/try/unit/horreum/unique_index_edge_cases_try.rb +380 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +283 -0
- data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
- data/try/unit/middleware/database_logger_methods_try.rb +251 -0
- data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
- data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
- data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
- data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
- data/try/unit/thread_safety_monitor_try.rb +149 -0
- metadata +81 -14
- data/.github/workflows/code-quality.yml +0 -138
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
- data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
- data/docs/archive/FAMILIA_UPDATE.md +0 -226
- data/docs/archive/README.md +0 -64
- data/docs/archive/api-reference.md +0 -333
- data/docs/guides/core-field-system.md +0 -806
- data/docs/guides/implementation.md +0 -276
- data/docs/guides/security-model.md +0 -183
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
# Field System Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Familia's Field System provides a flexible, extensible architecture for defining and managing object attributes with customizable behavior, conflict resolution, and serialization. The system uses a FieldType-based architecture that separates field definition from implementation, enabling custom field behaviors and advanced features.
|
|
6
|
+
|
|
7
|
+
## Core Architecture
|
|
8
|
+
|
|
9
|
+
### FieldType System
|
|
10
|
+
|
|
11
|
+
The Field System is built around the `FieldType` class hierarchy:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
FieldType # Base class for all field types
|
|
15
|
+
├── TransientFieldType # Non-persistent fields (memory only)
|
|
16
|
+
├── EncryptedFieldType # Encrypted storage fields
|
|
17
|
+
├── ExternalIdentifierFieldType # External ID fields
|
|
18
|
+
├── ObjectIdentifierFieldType # Object ID fields
|
|
19
|
+
└── Custom field types # User-defined field behaviors
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Field Definition Flow
|
|
23
|
+
|
|
24
|
+
1. **Field Declaration**: `field :name, options...`
|
|
25
|
+
2. **FieldType Creation**: Appropriate FieldType instance created
|
|
26
|
+
3. **Registration**: FieldType registered with the class
|
|
27
|
+
4. **Method Installation**: Getter, setter, and fast methods defined
|
|
28
|
+
5. **Runtime Usage**: Methods available on instances
|
|
29
|
+
|
|
30
|
+
## Basic Usage
|
|
31
|
+
|
|
32
|
+
### Simple Field Definition
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
class Customer < Familia::Horreum
|
|
36
|
+
# Basic field with default settings
|
|
37
|
+
field :name
|
|
38
|
+
|
|
39
|
+
# Field with custom method name
|
|
40
|
+
field :email_address, as: :email
|
|
41
|
+
|
|
42
|
+
# Field without accessor methods
|
|
43
|
+
field :internal_data, as: false
|
|
44
|
+
|
|
45
|
+
# Field without fast writer method
|
|
46
|
+
field :readonly_data, fast_method: false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
customer = Customer.new
|
|
50
|
+
customer.name = "Acme Corp" # Standard setter
|
|
51
|
+
customer.email = "admin@acme.com" # Custom method name
|
|
52
|
+
customer.name!("Updated Corp") # Fast writer (immediate DB persistence)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Method Conflict Resolution
|
|
56
|
+
|
|
57
|
+
Familia provides several strategies for handling method name conflicts:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
class Customer < Familia::Horreum
|
|
61
|
+
# Raise error if method exists (default)
|
|
62
|
+
field :status, on_conflict: :raise
|
|
63
|
+
|
|
64
|
+
# Skip field definition if method exists
|
|
65
|
+
field :type, on_conflict: :skip
|
|
66
|
+
|
|
67
|
+
# Warn but proceed with definition
|
|
68
|
+
field :class, on_conflict: :warn
|
|
69
|
+
|
|
70
|
+
# Silently overwrite existing method
|
|
71
|
+
field :id, on_conflict: :overwrite
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Special Field Types
|
|
76
|
+
|
|
77
|
+
### Transient Fields
|
|
78
|
+
|
|
79
|
+
Transient fields exist only in memory and are never persisted to the database. Values are automatically wrapped in `RedactedString` objects for security:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
class SecretService < Familia::Horreum
|
|
83
|
+
feature :transient_fields
|
|
84
|
+
|
|
85
|
+
field :name # Regular persistent field
|
|
86
|
+
transient_field :api_key # Wrapped in RedactedString
|
|
87
|
+
transient_field :password # Not persisted to database
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
service = SecretService.new
|
|
91
|
+
service.api_key = "sk-1234567890"
|
|
92
|
+
service.api_key.class #=> RedactedString
|
|
93
|
+
puts service.api_key #=> "[REDACTED]"
|
|
94
|
+
|
|
95
|
+
# Safe access pattern
|
|
96
|
+
service.api_key.expose do |key|
|
|
97
|
+
HTTP.post(url, headers: { 'Authorization' => "Bearer #{key}" })
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Encrypted Fields
|
|
102
|
+
|
|
103
|
+
Encrypted fields provide transparent encryption/decryption with strong cryptographic protection:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
class Document < Familia::Horreum
|
|
107
|
+
feature :encrypted_fields
|
|
108
|
+
|
|
109
|
+
field :title # Plaintext
|
|
110
|
+
encrypted_field :content # Encrypted storage
|
|
111
|
+
encrypted_field :api_key, aad_fields: [:title] # With additional authentication
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
doc = Document.new(title: "Secret", content: "classified info")
|
|
115
|
+
doc.content.class #=> ConcealedString
|
|
116
|
+
puts doc.content #=> "[CONCEALED]"
|
|
117
|
+
|
|
118
|
+
# Explicit access required
|
|
119
|
+
doc.content.reveal do |plaintext|
|
|
120
|
+
puts plaintext # => "classified info"
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Data Type Fields
|
|
125
|
+
|
|
126
|
+
Familia provides Redis/Valkey data structure fields through the related fields system:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
class User < Familia::Horreum
|
|
130
|
+
identifier_field :user_id
|
|
131
|
+
field :user_id, :name, :email
|
|
132
|
+
|
|
133
|
+
# Redis data structure fields
|
|
134
|
+
list :activity_log # Redis LIST
|
|
135
|
+
set :permissions # Redis SET
|
|
136
|
+
sorted_set :scores # Redis ZSET
|
|
137
|
+
hashkey :preferences # Redis HASH
|
|
138
|
+
counter :login_count # Redis counter
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
user = User.new(user_id: 'u123')
|
|
142
|
+
user.activity_log << "logged in"
|
|
143
|
+
user.permissions.add("read")
|
|
144
|
+
user.scores.add("quiz1", 95)
|
|
145
|
+
user.preferences["theme"] = "dark"
|
|
146
|
+
user.login_count.increment
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Advanced Field Types
|
|
150
|
+
|
|
151
|
+
### Creating Custom Field Types
|
|
152
|
+
|
|
153
|
+
Custom field types allow you to define specialized behavior for your fields:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
class TimestampFieldType < Familia::FieldType
|
|
157
|
+
def define_setter(klass)
|
|
158
|
+
field_name = @name
|
|
159
|
+
method_name = @method_name
|
|
160
|
+
|
|
161
|
+
handle_method_conflict(klass, :"#{method_name}=") do
|
|
162
|
+
klass.define_method :"#{method_name}=" do |value|
|
|
163
|
+
timestamp = case value
|
|
164
|
+
when Time then value.to_i
|
|
165
|
+
when String then Time.parse(value).to_i
|
|
166
|
+
when Numeric then value.to_i
|
|
167
|
+
else nil
|
|
168
|
+
end
|
|
169
|
+
instance_variable_set(:"@#{field_name}", timestamp)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def define_getter(klass)
|
|
175
|
+
field_name = @name
|
|
176
|
+
method_name = @method_name
|
|
177
|
+
|
|
178
|
+
handle_method_conflict(klass, method_name) do
|
|
179
|
+
klass.define_method method_name do
|
|
180
|
+
timestamp = instance_variable_get(:"@#{field_name}")
|
|
181
|
+
timestamp ? Time.at(timestamp) : nil
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def serialize(value, _record = nil)
|
|
187
|
+
value&.to_i
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def deserialize(value, _record = nil)
|
|
191
|
+
value ? Time.at(value.to_i) : nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def category
|
|
195
|
+
:timestamp
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
class Event < Familia::Horreum
|
|
200
|
+
def self.timestamp_field(name, **options)
|
|
201
|
+
field_type = TimestampFieldType.new(name, **options)
|
|
202
|
+
register_field_type(field_type)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
identifier_field :event_id
|
|
206
|
+
field :event_id, :name, :description
|
|
207
|
+
timestamp_field :created_at
|
|
208
|
+
timestamp_field :updated_at
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
event = Event.new(event_id: 'e123')
|
|
212
|
+
event.created_at = "2024-01-01 12:00:00 UTC"
|
|
213
|
+
event.created_at.class #=> Time
|
|
214
|
+
event.created_at.to_s #=> "2024-01-01 12:00:00 UTC"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Enum Field Type
|
|
218
|
+
|
|
219
|
+
Create fields with restricted values and validation:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
class EnumFieldType < Familia::FieldType
|
|
223
|
+
def initialize(name, values:, **options)
|
|
224
|
+
super(name, **options)
|
|
225
|
+
@valid_values = Array(values).map(&:to_s).freeze
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def define_setter(klass)
|
|
229
|
+
field_name = @name
|
|
230
|
+
method_name = @method_name
|
|
231
|
+
valid_values = @valid_values
|
|
232
|
+
|
|
233
|
+
handle_method_conflict(klass, :"#{method_name}=") do
|
|
234
|
+
klass.define_method :"#{method_name}=" do |value|
|
|
235
|
+
unless valid_values.include?(value.to_s)
|
|
236
|
+
raise ArgumentError, "Invalid #{field_name}: #{value}. Valid values: #{valid_values.join(', ')}"
|
|
237
|
+
end
|
|
238
|
+
instance_variable_set(:"@#{field_name}", value.to_s)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def install(klass)
|
|
244
|
+
super
|
|
245
|
+
# Add constants for enum values
|
|
246
|
+
@valid_values.each do |value|
|
|
247
|
+
const_name = "#{@name.to_s.upcase}_#{value.upcase}"
|
|
248
|
+
klass.const_set(const_name, value) unless klass.const_defined?(const_name)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def category
|
|
253
|
+
:enum
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
class Order < Familia::Horreum
|
|
258
|
+
def self.enum_field(name, values:, **options)
|
|
259
|
+
field_type = EnumFieldType.new(name, values: values, **options)
|
|
260
|
+
register_field_type(field_type)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
identifier_field :order_id
|
|
264
|
+
field :order_id, :customer_id
|
|
265
|
+
enum_field :status, values: [:pending, :processing, :shipped, :delivered]
|
|
266
|
+
enum_field :priority, values: [:low, :normal, :high, :urgent]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
order = Order.new(order_id: 'o123')
|
|
270
|
+
order.status = :pending # Valid
|
|
271
|
+
order.status = "processing" # Valid (string converted)
|
|
272
|
+
order.priority = Order::PRIORITY_HIGH # Using generated constant
|
|
273
|
+
|
|
274
|
+
# This raises ArgumentError
|
|
275
|
+
order.status = :invalid # Invalid value
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Field Metadata and Introspection
|
|
279
|
+
|
|
280
|
+
### Accessing Field Information
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
class Product < Familia::Horreum
|
|
284
|
+
feature :transient_fields
|
|
285
|
+
|
|
286
|
+
field :name
|
|
287
|
+
field :price
|
|
288
|
+
field :description
|
|
289
|
+
transient_field :temp_data
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Get all field names
|
|
293
|
+
Product.fields #=> [:name, :price, :description, :temp_data]
|
|
294
|
+
|
|
295
|
+
# Get field types
|
|
296
|
+
Product.field_types #=> { name: FieldType, price: FieldType, ... }
|
|
297
|
+
|
|
298
|
+
# Get persistent vs transient fields
|
|
299
|
+
Product.persistent_fields #=> [:name, :price, :description]
|
|
300
|
+
Product.transient_fields #=> [:temp_data]
|
|
301
|
+
|
|
302
|
+
# Check field properties
|
|
303
|
+
product = Product.new
|
|
304
|
+
field_type = Product.field_types[:temp_data]
|
|
305
|
+
field_type.persistent? #=> false
|
|
306
|
+
field_type.transient? #=> true
|
|
307
|
+
field_type.category #=> :transient
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Field Categories and Filtering
|
|
311
|
+
|
|
312
|
+
Field types can specify categories for grouping and filtering:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
class SearchableFieldType < Familia::FieldType
|
|
316
|
+
def category
|
|
317
|
+
:searchable
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
class Product < Familia::Horreum
|
|
322
|
+
def self.searchable_field(name, **options)
|
|
323
|
+
field_type = SearchableFieldType.new(name, **options)
|
|
324
|
+
register_field_type(field_type)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
searchable_field :name
|
|
328
|
+
searchable_field :description
|
|
329
|
+
field :internal_id
|
|
330
|
+
|
|
331
|
+
def self.searchable_fields
|
|
332
|
+
field_types.select { |_, ft| ft.category == :searchable }.keys
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
Product.searchable_fields #=> [:name, :description]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Fast Methods and Database Operations
|
|
340
|
+
|
|
341
|
+
### Fast Method Behavior
|
|
342
|
+
|
|
343
|
+
Fast methods (ending with `!`) provide immediate database persistence without requiring a separate `save` call:
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
class UserProfile < Familia::Horreum
|
|
347
|
+
identifier_field :user_id
|
|
348
|
+
field :user_id, :name, :email, :last_login_at
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
profile = UserProfile.new(user_id: 'u123')
|
|
352
|
+
profile.name!("John Doe") # Immediately persists to database
|
|
353
|
+
profile.email!("john@example.com") # No save() needed
|
|
354
|
+
|
|
355
|
+
# Reading with fast method returns current database value
|
|
356
|
+
current_name = profile.name! # Reads from database
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Custom Fast Method Behavior
|
|
360
|
+
|
|
361
|
+
Override fast method behavior for specialized use cases:
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
class AuditedFieldType < Familia::FieldType
|
|
365
|
+
def define_fast_writer(klass)
|
|
366
|
+
return unless @fast_method_name&.to_s&.end_with?('!')
|
|
367
|
+
|
|
368
|
+
field_name = @name
|
|
369
|
+
method_name = @method_name
|
|
370
|
+
fast_method_name = @fast_method_name
|
|
371
|
+
|
|
372
|
+
handle_method_conflict(klass, fast_method_name) do
|
|
373
|
+
klass.define_method fast_method_name do |*args|
|
|
374
|
+
val = args.first
|
|
375
|
+
return hget(field_name) if val.nil?
|
|
376
|
+
|
|
377
|
+
# Audit the change
|
|
378
|
+
old_value = hget(field_name)
|
|
379
|
+
timestamp = Time.now.to_i
|
|
380
|
+
|
|
381
|
+
# Log the change
|
|
382
|
+
puts "AUDIT: #{field_name} changed from #{old_value} to #{val} at #{timestamp}"
|
|
383
|
+
|
|
384
|
+
# Update instance variable
|
|
385
|
+
send(:"#{method_name}=", val) if method_name
|
|
386
|
+
|
|
387
|
+
# Persist to database
|
|
388
|
+
hset(field_name, serialize_value(val))
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
class AuditedDocument < Familia::Horreum
|
|
395
|
+
def self.audited_field(name, **options)
|
|
396
|
+
field_type = AuditedFieldType.new(name, **options)
|
|
397
|
+
register_field_type(field_type)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
identifier_field :doc_id
|
|
401
|
+
field :doc_id, :title
|
|
402
|
+
audited_field :content
|
|
403
|
+
audited_field :status
|
|
404
|
+
end
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Field Options and Configuration
|
|
408
|
+
|
|
409
|
+
### Available Options
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
field :name,
|
|
413
|
+
as: :display_name, # Custom method name
|
|
414
|
+
fast_method: :name_now!, # Custom fast method name
|
|
415
|
+
fast_method: false, # Disable fast method
|
|
416
|
+
on_conflict: :skip, # Conflict resolution strategy
|
|
417
|
+
loggable: false # Exclude from serialization
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Conflict Resolution Strategies
|
|
421
|
+
|
|
422
|
+
- `:raise` - Raise error if method exists (default)
|
|
423
|
+
- `:skip` - Skip field definition if method exists
|
|
424
|
+
- `:warn` - Warn but proceed with definition
|
|
425
|
+
- `:overwrite` - Silently overwrite existing method
|
|
426
|
+
|
|
427
|
+
## Integration Patterns
|
|
428
|
+
|
|
429
|
+
### Rails Integration
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
module FamiliaFields
|
|
433
|
+
extend ActiveSupport::Concern
|
|
434
|
+
|
|
435
|
+
class_methods do
|
|
436
|
+
def string_field(*names, **options)
|
|
437
|
+
names.each { |name| field(name, **options) }
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def integer_field(*names, **options)
|
|
441
|
+
field_type = Class.new(Familia::FieldType) do
|
|
442
|
+
def serialize(value, _record = nil)
|
|
443
|
+
value&.to_i
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def deserialize(value, _record = nil)
|
|
447
|
+
value&.to_i
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
names.each do |name|
|
|
452
|
+
register_field_type(field_type.new(name, **options))
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def boolean_field(*names, **options)
|
|
457
|
+
field_type = Class.new(Familia::FieldType) do
|
|
458
|
+
def serialize(value, _record = nil)
|
|
459
|
+
!!value
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def deserialize(value, _record = nil)
|
|
463
|
+
case value.to_s.downcase
|
|
464
|
+
when 'true', '1', 'yes', 'on' then true
|
|
465
|
+
when 'false', '0', 'no', 'off' then false
|
|
466
|
+
else nil
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def define_getter(klass)
|
|
471
|
+
field_name = @name
|
|
472
|
+
method_name = @method_name
|
|
473
|
+
|
|
474
|
+
handle_method_conflict(klass, method_name) do
|
|
475
|
+
klass.define_method method_name do
|
|
476
|
+
value = instance_variable_get(:"@#{field_name}")
|
|
477
|
+
!!value
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
names.each do |name|
|
|
484
|
+
register_field_type(field_type.new(name, **options))
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
class User < Familia::Horreum
|
|
491
|
+
include FamiliaFields
|
|
492
|
+
|
|
493
|
+
identifier_field :user_id
|
|
494
|
+
string_field :user_id, :email, :name
|
|
495
|
+
integer_field :age, :login_count
|
|
496
|
+
boolean_field :active, :verified
|
|
497
|
+
end
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Validation Integration
|
|
501
|
+
|
|
502
|
+
```ruby
|
|
503
|
+
class ValidatedFieldType < Familia::FieldType
|
|
504
|
+
def initialize(name, validations: {}, **options)
|
|
505
|
+
super(name, **options)
|
|
506
|
+
@validations = validations
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def define_setter(klass)
|
|
510
|
+
field_name = @name
|
|
511
|
+
method_name = @method_name
|
|
512
|
+
validations = @validations
|
|
513
|
+
|
|
514
|
+
handle_method_conflict(klass, :"#{method_name}=") do
|
|
515
|
+
klass.define_method :"#{method_name}=" do |value|
|
|
516
|
+
# Run validations
|
|
517
|
+
validations.each do |type, constraint|
|
|
518
|
+
case type
|
|
519
|
+
when :presence
|
|
520
|
+
raise ArgumentError, "#{field_name} cannot be blank" if constraint && value.to_s.strip.empty?
|
|
521
|
+
when :length
|
|
522
|
+
if constraint.is_a?(Hash)
|
|
523
|
+
min = constraint[:minimum] || constraint[:min]
|
|
524
|
+
max = constraint[:maximum] || constraint[:max]
|
|
525
|
+
len = value.to_s.length
|
|
526
|
+
raise ArgumentError, "#{field_name} too short (minimum: #{min})" if min && len < min
|
|
527
|
+
raise ArgumentError, "#{field_name} too long (maximum: #{max})" if max && len > max
|
|
528
|
+
end
|
|
529
|
+
when :format
|
|
530
|
+
raise ArgumentError, "#{field_name} format invalid" if constraint && !value.to_s.match?(constraint)
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
instance_variable_set(:"@#{field_name}", value)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
class User < Familia::Horreum
|
|
541
|
+
def self.validated_field(name, validations: {}, **options)
|
|
542
|
+
field_type = ValidatedFieldType.new(name, validations: validations, **options)
|
|
543
|
+
register_field_type(field_type)
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
identifier_field :user_id
|
|
547
|
+
validated_field :user_id, validations: { presence: true }
|
|
548
|
+
validated_field :email, validations: {
|
|
549
|
+
presence: true,
|
|
550
|
+
format: /\A[^@\s]+@[^@\s]+\z/
|
|
551
|
+
}
|
|
552
|
+
validated_field :status, validations: {
|
|
553
|
+
presence: true,
|
|
554
|
+
format: /\A(active|inactive|pending)\z/
|
|
555
|
+
}
|
|
556
|
+
validated_field :name, validations: {
|
|
557
|
+
length: { minimum: 2, maximum: 50 }
|
|
558
|
+
}
|
|
559
|
+
end
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
## Performance Considerations
|
|
563
|
+
|
|
564
|
+
### Efficient Field Operations
|
|
565
|
+
|
|
566
|
+
```ruby
|
|
567
|
+
# Batch updates using fast methods
|
|
568
|
+
user.name!("John")
|
|
569
|
+
user.email!("john@example.com")
|
|
570
|
+
user.status!("active")
|
|
571
|
+
|
|
572
|
+
# Use transactions for multiple operations
|
|
573
|
+
redis.multi do
|
|
574
|
+
user.name!("John")
|
|
575
|
+
user.email!("john@example.com")
|
|
576
|
+
user.status!("active")
|
|
577
|
+
end
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Memory-Efficient Field Storage
|
|
581
|
+
|
|
582
|
+
```ruby
|
|
583
|
+
class CompactFieldType < Familia::FieldType
|
|
584
|
+
def serialize(value, _record = nil)
|
|
585
|
+
case value
|
|
586
|
+
when String
|
|
587
|
+
# Compress long strings
|
|
588
|
+
value.length > 100 ? Zlib::Deflate.deflate(value) : value
|
|
589
|
+
when Hash
|
|
590
|
+
# Use more compact JSON representation
|
|
591
|
+
value.to_json
|
|
592
|
+
when Array
|
|
593
|
+
# Join simple arrays
|
|
594
|
+
value.all? { |v| v.is_a?(String) } ? value.join('|') : value.to_json
|
|
595
|
+
else
|
|
596
|
+
value
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def deserialize(value, _record = nil)
|
|
601
|
+
return value unless value.is_a?(String)
|
|
602
|
+
|
|
603
|
+
# Try to decompress
|
|
604
|
+
if value.start_with?("\x78\x9C") # zlib magic bytes
|
|
605
|
+
Zlib::Inflate.inflate(value)
|
|
606
|
+
elsif value.start_with?('{', '[')
|
|
607
|
+
JSON.parse(value)
|
|
608
|
+
elsif value.include?('|')
|
|
609
|
+
value.split('|')
|
|
610
|
+
else
|
|
611
|
+
value
|
|
612
|
+
end
|
|
613
|
+
rescue JSON::ParserError, Zlib::Error
|
|
614
|
+
value # Return original if parsing fails
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
## Testing Field Types
|
|
620
|
+
|
|
621
|
+
### RSpec Testing
|
|
622
|
+
|
|
623
|
+
```ruby
|
|
624
|
+
describe TimestampFieldType do
|
|
625
|
+
let(:field_type) { TimestampFieldType.new(:created_at) }
|
|
626
|
+
let(:test_class) do
|
|
627
|
+
Class.new do
|
|
628
|
+
def self.name; 'TestClass'; end
|
|
629
|
+
include Familia::Horreum
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
before do
|
|
634
|
+
field_type.install(test_class)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
it "converts various time formats" do
|
|
638
|
+
instance = test_class.new
|
|
639
|
+
instance.created_at = "2024-01-01 12:00:00 UTC"
|
|
640
|
+
expect(instance.created_at).to be_a(Time)
|
|
641
|
+
expect(instance.created_at.to_s).to include("2024-01-01 12:00:00")
|
|
642
|
+
|
|
643
|
+
instance.created_at = Time.now
|
|
644
|
+
expect(instance.created_at).to be_a(Time)
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
it "serializes to integer" do
|
|
648
|
+
time = Time.parse("2024-01-01 12:00:00 UTC")
|
|
649
|
+
expect(field_type.serialize(time)).to eq(time.to_i)
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
it "deserializes from integer" do
|
|
653
|
+
timestamp = Time.parse("2024-01-01 12:00:00 UTC").to_i
|
|
654
|
+
result = field_type.deserialize(timestamp)
|
|
655
|
+
expect(result).to be_a(Time)
|
|
656
|
+
expect(result.to_i).to eq(timestamp)
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
## Best Practices
|
|
662
|
+
|
|
663
|
+
### 1. Choose Appropriate Field Types
|
|
664
|
+
|
|
665
|
+
```ruby
|
|
666
|
+
class User < Familia::Horreum
|
|
667
|
+
feature :transient_fields
|
|
668
|
+
feature :encrypted_fields
|
|
669
|
+
|
|
670
|
+
field :name # Regular field for non-sensitive data
|
|
671
|
+
field :metadata # JSON data can be stored as regular field
|
|
672
|
+
transient_field :temp_token # Sensitive temporary data
|
|
673
|
+
encrypted_field :api_key # Sensitive persistent data
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Use specialized field types for domain-specific data
|
|
677
|
+
class GeoLocation < Familia::Horreum
|
|
678
|
+
coordinate_field :latitude # Custom validation for coordinates
|
|
679
|
+
coordinate_field :longitude
|
|
680
|
+
end
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### 2. Handle Method Conflicts Gracefully
|
|
684
|
+
|
|
685
|
+
```ruby
|
|
686
|
+
class SafeFieldDefinition < Familia::Horreum
|
|
687
|
+
# Always use skip strategy for potentially conflicting names
|
|
688
|
+
def self.safe_field(name, **options)
|
|
689
|
+
field(name, on_conflict: :skip, **options)
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### 3. Optimize for Common Use Cases
|
|
695
|
+
|
|
696
|
+
```ruby
|
|
697
|
+
class BaseModel < Familia::Horreum
|
|
698
|
+
def self.timestamps
|
|
699
|
+
timestamp_field :created_at
|
|
700
|
+
timestamp_field :updated_at
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def self.soft_delete
|
|
704
|
+
boolean_field :deleted
|
|
705
|
+
timestamp_field :deleted_at
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
class User < BaseModel
|
|
710
|
+
timestamps
|
|
711
|
+
soft_delete
|
|
712
|
+
|
|
713
|
+
field :name, :email
|
|
714
|
+
end
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### 4. Use Field Groups for Organization
|
|
718
|
+
|
|
719
|
+
```ruby
|
|
720
|
+
class User < Familia::Horreum
|
|
721
|
+
field_group :identity do
|
|
722
|
+
field :user_id
|
|
723
|
+
field :email
|
|
724
|
+
field :username
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
field_group :profile do
|
|
728
|
+
field :first_name
|
|
729
|
+
field :last_name
|
|
730
|
+
field :avatar_url
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
field_group :preferences do
|
|
734
|
+
field :theme
|
|
735
|
+
field :language
|
|
736
|
+
field :timezone
|
|
737
|
+
end
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# Access grouped fields
|
|
741
|
+
User.field_groups[:identity] #=> [:user_id, :email, :username]
|
|
742
|
+
User.field_groups[:profile] #=> [:first_name, :last_name, :avatar_url]
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
## API Reference
|
|
746
|
+
|
|
747
|
+
### FieldType Class
|
|
748
|
+
|
|
749
|
+
```ruby
|
|
750
|
+
# Constructor
|
|
751
|
+
FieldType.new(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, loggable: true, **options)
|
|
752
|
+
|
|
753
|
+
# Key methods
|
|
754
|
+
field_type.install(klass) # Install on class
|
|
755
|
+
field_type.define_getter(klass) # Define getter method
|
|
756
|
+
field_type.define_setter(klass) # Define setter method
|
|
757
|
+
field_type.define_fast_writer(klass) # Define fast writer method
|
|
758
|
+
field_type.serialize(value, record) # Serialize for storage
|
|
759
|
+
field_type.deserialize(value, record) # Deserialize from storage
|
|
760
|
+
field_type.persistent? # Check if persisted
|
|
761
|
+
field_type.category # Get field category
|
|
762
|
+
field_type.generated_methods # Get all generated method names
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
### Class Methods
|
|
766
|
+
|
|
767
|
+
```ruby
|
|
768
|
+
# Field definition
|
|
769
|
+
field(name, **options) # Define a field
|
|
770
|
+
register_field_type(field_type) # Register custom field type
|
|
771
|
+
|
|
772
|
+
# Introspection
|
|
773
|
+
fields # Get all field names
|
|
774
|
+
field_types # Get all field types
|
|
775
|
+
persistent_fields # Get persistent field names
|
|
776
|
+
transient_fields # Get transient field names
|
|
777
|
+
field_method_map # Get field name to method mappings
|
|
778
|
+
```
|