familia 2.0.0.pre19 → 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 +177 -112
- data/CLAUDE.md +28 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +20 -17
- 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 +161 -117
- 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-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 +4 -3
- 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 +2 -0
- data/lib/familia/connection/handlers.rb +2 -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 +2 -0
- data/lib/familia/connection/operations.rb +2 -0
- data/lib/familia/connection/pipelined_core.rb +2 -0
- data/lib/familia/connection/transaction_core.rb +68 -0
- data/lib/familia/connection.rb +18 -3
- data/lib/familia/data_type/class_methods.rb +3 -1
- data/lib/familia/data_type/connection.rb +2 -0
- data/lib/familia/data_type/database_commands.rb +2 -0
- data/lib/familia/data_type/serialization.rb +6 -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 +7 -5
- 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 +2 -0
- 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 +3 -1
- data/lib/familia/features/expiration.rb +12 -4
- 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 +138 -9
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +89 -21
- data/lib/familia/features/relationships/indexing.rb +3 -0
- data/lib/familia/features/relationships/indexing_relationship.rb +3 -1
- 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 +3 -1
- data/lib/familia/horreum/connection.rb +17 -1
- data/lib/familia/horreum/database_commands.rb +2 -0
- data/lib/familia/horreum/definition.rb +16 -6
- data/lib/familia/horreum/management.rb +212 -42
- data/lib/familia/horreum/persistence.rb +176 -108
- data/lib/familia/horreum/related_fields.rb +2 -0
- data/lib/familia/horreum/serialization.rb +23 -4
- data/lib/familia/horreum/settings.rb +2 -0
- data/lib/familia/horreum/utils.rb +2 -0
- data/lib/familia/horreum.rb +15 -1
- 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 +92 -32
- 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 +2 -0
- 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 +295 -170
- 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 +2 -0
- 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 +4 -0
- 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 +2 -0
- 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 +2 -0
- 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 +4 -0
- 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 +4 -0
- data/try/integration/connection/pipeline_fallback_integration_try.rb +3 -0
- 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 +4 -0
- data/try/integration/cross_component_try.rb +4 -0
- data/try/integration/data_types/datatype_pipelines_try.rb +4 -0
- data/try/integration/data_types/datatype_transactions_try.rb +4 -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 +4 -0
- 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 +4 -0
- data/try/integration/persistence_operations_try.rb +4 -0
- 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 +4 -0
- 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 +4 -0
- 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 +5 -1
- data/try/unit/horreum/automatic_index_validation_try.rb +2 -0
- data/try/unit/horreum/base_try.rb +4 -0
- data/try/unit/horreum/class_methods_try.rb +4 -0
- 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 +4 -0
- 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 +4 -0
- data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
- data/try/unit/horreum/serialization_try.rb +4 -0
- data/try/unit/horreum/settings_try.rb +4 -0
- data/try/unit/horreum/unique_index_edge_cases_try.rb +4 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +2 -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 +72 -17
- data/.github/workflows/code-quality.yml +0 -138
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +0 -91
- data/changelog.d/20251011_203905_delano_next.rst +0 -30
- data/changelog.d/20251011_212633_delano_next.rst +0 -13
- data/changelog.d/20251011_221253_delano_next.rst +0 -26
- 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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# lib/familia/features/relationships/participation/participant_methods.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require_relative '../collection_operations'
|
|
4
6
|
|
|
@@ -8,37 +10,48 @@ module Familia
|
|
|
8
10
|
# Methods added to PARTICIPANT classes (the ones calling participates_in)
|
|
9
11
|
# These methods allow participant instances to manage their membership in target collections
|
|
10
12
|
#
|
|
11
|
-
# Example: When Domain calls `participates_in
|
|
12
|
-
# Domain instances get methods to check/manage their presence in
|
|
13
|
+
# Example: When Domain calls `participates_in Employee, :domains`
|
|
14
|
+
# Domain instances get methods to check/manage their presence in Employee collections
|
|
13
15
|
module ParticipantMethods
|
|
14
16
|
using Familia::Refinements::StylizeWords
|
|
15
17
|
extend CollectionOperations
|
|
16
18
|
|
|
17
19
|
# Visual Guide for methods added to PARTICIPANT instances:
|
|
18
20
|
# =========================================================
|
|
19
|
-
# When Domain calls: participates_in
|
|
21
|
+
# When Domain calls: participates_in Employee, :domains
|
|
20
22
|
#
|
|
21
23
|
# Domain instances (PARTICIPANT) get these methods:
|
|
22
|
-
# ├──
|
|
23
|
-
# ├──
|
|
24
|
-
# ├──
|
|
25
|
-
# ├──
|
|
26
|
-
# └──
|
|
24
|
+
# ├── in_employee_domains?(employee) # Check if I'm in this employee's domains
|
|
25
|
+
# ├── add_to_employee_domains(employee, score) # Add myself to employee's domains
|
|
26
|
+
# ├── remove_from_employee_domains(employee) # Remove myself from employee's domains
|
|
27
|
+
# ├── score_in_employee_domains(employee) # Get my score (sorted_set only)
|
|
28
|
+
# └── position_in_employee_domains(employee) # Get my position (list only)
|
|
27
29
|
#
|
|
28
30
|
# Note: To update scores, use the DataType API directly:
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
+
# employee.domains.add(domain.identifier, new_score, xx: true)
|
|
32
|
+
#
|
|
31
33
|
module Builder
|
|
32
34
|
extend CollectionOperations
|
|
33
35
|
|
|
34
36
|
# Build all participant methods for a participation relationship
|
|
37
|
+
#
|
|
35
38
|
# @param participant_class [Class] The class receiving these methods (e.g., Domain)
|
|
36
|
-
# @param
|
|
39
|
+
# @param target_class [Class, String] Target class object or 'class' for class-level participation (e.g., Employee or 'class')
|
|
37
40
|
# @param collection_name [Symbol] Name of the collection (e.g., :domains)
|
|
38
41
|
# @param type [Symbol] Collection type (:sorted_set, :set, :list)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
# @param as [Symbol, nil] Optional custom name for relationship methods (e.g., :employees)
|
|
43
|
+
#
|
|
44
|
+
def self.build(participant_class, target_class, collection_name, type, as)
|
|
45
|
+
# Determine target name based on participation context:
|
|
46
|
+
# - Instance-level: target_class is a Class object (e.g., Team) → use config_name ("project_team")
|
|
47
|
+
# - Class-level: target_class is the string 'class' (from class_participates_in) → use as-is
|
|
48
|
+
# The string 'class' is passed from TargetMethods.build_class_add_method when calling
|
|
49
|
+
# calculate_participation_score('class', collection_name) for class-level scoring
|
|
50
|
+
target_name = if target_class.is_a?(String)
|
|
51
|
+
target_class # 'class' for class-level participation
|
|
52
|
+
else
|
|
53
|
+
target_class.config_name # snake_case class name for instance-level
|
|
54
|
+
end
|
|
42
55
|
|
|
43
56
|
# Core participant methods
|
|
44
57
|
build_membership_check(participant_class, target_name, collection_name, type)
|
|
@@ -52,6 +65,110 @@ module Familia
|
|
|
52
65
|
when :list
|
|
53
66
|
build_position_method(participant_class, target_name, collection_name)
|
|
54
67
|
end
|
|
68
|
+
|
|
69
|
+
# Build reverse collection methods on PARTICIPANT class for instance-level participation
|
|
70
|
+
# Skip for class-level participation because:
|
|
71
|
+
# - Class-level uses class_participates_in (e.g., User.all_users)
|
|
72
|
+
# - Bidirectional methods don't make sense: an individual User can't have "all_users"
|
|
73
|
+
# - Class-level collections are accessed directly on the class (User.all_users)
|
|
74
|
+
return if target_class.is_a?(String) # 'class' indicates class-level participation
|
|
75
|
+
|
|
76
|
+
# If `as` is specified, create a custom method for just this collection
|
|
77
|
+
# Otherwise, add to the default pluralized method that unions all collections
|
|
78
|
+
if as
|
|
79
|
+
# Custom method for just this specific collection
|
|
80
|
+
build_reverse_collection_methods(participant_class, target_class, as, [collection_name])
|
|
81
|
+
else
|
|
82
|
+
# Default pluralized method - will include ALL collections for this target
|
|
83
|
+
build_reverse_collection_methods(participant_class, target_class, nil, nil)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Generate reverse collection methods on participant class for bidirectional access
|
|
88
|
+
#
|
|
89
|
+
# Creates methods like:
|
|
90
|
+
# - user.team_instances (returns Array of Team instances)
|
|
91
|
+
# - user.team_ids (returns Array of IDs)
|
|
92
|
+
# - user.team? (returns Boolean)
|
|
93
|
+
# - user.team_count (returns Integer)
|
|
94
|
+
#
|
|
95
|
+
# @param participant_class [Class] The participant class (e.g., User)
|
|
96
|
+
# @param target_class [Class] The target class (e.g., Team)
|
|
97
|
+
# @param custom_name [Symbol, nil] Custom method name override (base name without suffix)
|
|
98
|
+
# @param collection_names [Array<Symbol>, nil] Specific collections to include (nil = all)
|
|
99
|
+
#
|
|
100
|
+
def self.build_reverse_collection_methods(participant_class, target_class, custom_name = nil, collection_names = nil)
|
|
101
|
+
# Determine base method name - either custom or target class config_name
|
|
102
|
+
# e.g., "project_team" or "contracting_org"
|
|
103
|
+
base_name = if custom_name
|
|
104
|
+
custom_name.to_s
|
|
105
|
+
else
|
|
106
|
+
# Use config_name as-is (e.g., "project_team")
|
|
107
|
+
target_class.config_name
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Store collection names as string array for matching
|
|
111
|
+
collections_filter = collection_names&.map(&:to_s)
|
|
112
|
+
|
|
113
|
+
# Generate the main collection method (e.g., user.project_team_instances)
|
|
114
|
+
#
|
|
115
|
+
# Loads actual objects - verifies Redis key existence via load_multi.
|
|
116
|
+
# No caching - load_multi is efficient enough and avoids stale data.
|
|
117
|
+
#
|
|
118
|
+
# @note Error Handling: This method lets database errors bubble up to the
|
|
119
|
+
# application layer, consistent with Familia's error handling pattern.
|
|
120
|
+
# Potential failures include:
|
|
121
|
+
# - Familia::NotConnected - Redis connection unavailable
|
|
122
|
+
# - Redis::TimeoutError - Operation timed out
|
|
123
|
+
# - Redis::ConnectionError - Network/connection issues
|
|
124
|
+
#
|
|
125
|
+
# For production environments, consider wrapping calls in application-level
|
|
126
|
+
# error handling:
|
|
127
|
+
#
|
|
128
|
+
# @example Application-level error handling
|
|
129
|
+
# begin
|
|
130
|
+
# teams = user.project_team_instances
|
|
131
|
+
# rescue Familia::PersistenceError => e
|
|
132
|
+
# # Handle database failure (log, fallback, retry, etc.)
|
|
133
|
+
# Rails.logger.error("Failed to load teams: #{e.message}")
|
|
134
|
+
# [] # Return empty array or other fallback
|
|
135
|
+
# end
|
|
136
|
+
#
|
|
137
|
+
participant_class.define_method("#{base_name}_instances") do
|
|
138
|
+
ids = participating_ids_for_target(target_class, collections_filter)
|
|
139
|
+
# Use load_multi for Horreum objects (stored as Redis hashes)
|
|
140
|
+
target_class.load_multi(ids).compact
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Generate the IDs-only method (e.g., user.project_team_ids)
|
|
144
|
+
#
|
|
145
|
+
# Shallow - returns IDs from participation index without verifying key existence.
|
|
146
|
+
#
|
|
147
|
+
# @note Database errors (connection, timeout) will bubble up to caller.
|
|
148
|
+
#
|
|
149
|
+
participant_class.define_method("#{base_name}_ids") do
|
|
150
|
+
participating_ids_for_target(target_class, collections_filter)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Generate the boolean check method (e.g., user.project_team?)
|
|
154
|
+
#
|
|
155
|
+
# Shallow check - verifies participation index membership, not Redis key existence.
|
|
156
|
+
#
|
|
157
|
+
# @note Database errors (connection, timeout) will bubble up to caller.
|
|
158
|
+
#
|
|
159
|
+
participant_class.define_method("#{base_name}?") do
|
|
160
|
+
participating_in_target?(target_class, collections_filter)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Generate the count method (e.g., user.project_team_count)
|
|
164
|
+
#
|
|
165
|
+
# Shallow - counts IDs from participation index without verifying key existence.
|
|
166
|
+
#
|
|
167
|
+
# @note Database errors (connection, timeout) will bubble up to caller.
|
|
168
|
+
#
|
|
169
|
+
participant_class.define_method("#{base_name}_count") do
|
|
170
|
+
participating_ids_for_target(target_class, collections_filter).size
|
|
171
|
+
end
|
|
55
172
|
end
|
|
56
173
|
|
|
57
174
|
# Build method to check membership in target's collection
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
## Indexes vs. Participations: Critical Distinction
|
|
2
|
+
|
|
3
|
+
**Indexes are derived data** - they can be rebuilt from source:
|
|
4
|
+
```ruby
|
|
5
|
+
# Indexes derive from object fields and can be reconstructed
|
|
6
|
+
User.rebuild_email_lookup # Rebuilds from all User instances
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**Participations are primary data** - they represent business decisions:
|
|
10
|
+
```ruby
|
|
11
|
+
# Participations are intentional relationships that must be explicitly created
|
|
12
|
+
@team.add_members_instance(@user) # Human/business decision about team membership
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Why this matters for rebuilding:**
|
|
16
|
+
|
|
17
|
+
| Aspect | Indexes | Participations |
|
|
18
|
+
|--------|---------|----------------|
|
|
19
|
+
| **Source of truth** | Object field values | Business logic/user actions |
|
|
20
|
+
| **Can rebuild?** | ✅ Yes - iterate instances | ❌ No - requires domain knowledge |
|
|
21
|
+
| **Fix when wrong** | Run rebuild method | Re-apply business logic |
|
|
22
|
+
| **Nature** | Computed/derived | Intentional/chosen |
|
|
23
|
+
|
|
24
|
+
**Examples:**
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
# ✅ INDEXES - Can rebuild because source exists
|
|
28
|
+
User.rebuild_email_lookup # Rebuilds from User.email field values
|
|
29
|
+
company.rebuild_badge_index # Rebuilds from Employee.badge_number values
|
|
30
|
+
|
|
31
|
+
# ❌ PARTICIPATIONS - Cannot rebuild without knowing intent
|
|
32
|
+
@team.members # Which users should be members? (business decision)
|
|
33
|
+
@org.employees # Who works here? (HR/business logic)
|
|
34
|
+
@project.contributors # Who contributed? (tracked externally)
|
|
35
|
+
|
|
36
|
+
# To fix participation data, reapply the business logic:
|
|
37
|
+
correct_members.each { |user| @team.add_members_instance(user) }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**When indexes fail**, run the rebuild method.
|
|
41
|
+
**When participations are wrong**, understand why they're wrong and reapply your application's business rules.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# lib/familia/features/relationships/participation/target_methods.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require_relative '../collection_operations'
|
|
4
6
|
|
|
@@ -72,10 +74,9 @@ module Familia
|
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
# Build method to add an item to the collection
|
|
75
|
-
# Creates: customer.
|
|
77
|
+
# Creates: customer.add_domains_instance(domain, score)
|
|
76
78
|
def self.build_add_item(target_class, collection_name, type)
|
|
77
|
-
|
|
78
|
-
method_name = "add_#{singular_name}"
|
|
79
|
+
method_name = "add_#{collection_name}_instance"
|
|
79
80
|
|
|
80
81
|
target_class.define_method(method_name) do |item, score = nil|
|
|
81
82
|
collection = send(collection_name)
|
|
@@ -105,10 +106,9 @@ module Familia
|
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
# Build method to remove an item from the collection
|
|
108
|
-
# Creates: customer.
|
|
109
|
+
# Creates: customer.remove_domains_instance(domain)
|
|
109
110
|
def self.build_remove_item(target_class, collection_name, type)
|
|
110
|
-
|
|
111
|
-
method_name = "remove_#{singular_name}"
|
|
111
|
+
method_name = "remove_#{collection_name}_instance"
|
|
112
112
|
|
|
113
113
|
target_class.define_method(method_name) do |item|
|
|
114
114
|
collection = send(collection_name)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# lib/familia/features/relationships/participation.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require_relative 'participation_relationship'
|
|
6
|
+
require_relative 'participation_membership'
|
|
4
7
|
require_relative 'collection_operations'
|
|
5
8
|
require_relative 'participation/participant_methods'
|
|
6
9
|
require_relative 'participation/target_methods'
|
|
@@ -160,10 +163,10 @@ module Familia
|
|
|
160
163
|
type: :sorted_set, bidirectional: true)
|
|
161
164
|
# Store metadata for this participation relationship
|
|
162
165
|
participation_relationships << ParticipationRelationship.new(
|
|
163
|
-
|
|
166
|
+
_original_target: self, # For class-level, original and resolved are the same
|
|
167
|
+
target_class: self, # The class itself
|
|
164
168
|
collection_name: collection_name,
|
|
165
169
|
score: score,
|
|
166
|
-
|
|
167
170
|
type: type,
|
|
168
171
|
bidirectional: bidirectional,
|
|
169
172
|
)
|
|
@@ -176,7 +179,10 @@ module Familia
|
|
|
176
179
|
# e.g., user.in_class_all_users?, user.add_to_class_all_users
|
|
177
180
|
return unless bidirectional
|
|
178
181
|
|
|
179
|
-
|
|
182
|
+
# Pass the string 'class' as target to distinguish class-level from instance-level
|
|
183
|
+
# This prevents generating reverse collection methods (user can't have "all_users")
|
|
184
|
+
# See ParticipantMethods::Builder.build for handling of this special case
|
|
185
|
+
ParticipantMethods::Builder.build(self, 'class', collection_name, type, nil)
|
|
180
186
|
end
|
|
181
187
|
|
|
182
188
|
# Define an instance-level participation relationship between two classes.
|
|
@@ -208,12 +214,14 @@ module Familia
|
|
|
208
214
|
# all collections the instance belongs to. This enables efficient membership queries
|
|
209
215
|
# and cleanup operations without scanning all possible collections.
|
|
210
216
|
#
|
|
211
|
-
# @param
|
|
212
|
-
# - +Class+ object (e.g., +
|
|
213
|
-
# - +Symbol+ referencing class name (e.g., +:
|
|
214
|
-
# - +String+ class name (e.g., +"
|
|
215
|
-
# @param collection_name [Symbol] Name of the collection on the
|
|
216
|
-
#
|
|
217
|
+
# @param target [Class, Symbol, String] The class that owns the collection. Can be:
|
|
218
|
+
# - +Class+ object (e.g., +Employee+)
|
|
219
|
+
# - +Symbol+ referencing class name (e.g., +:employee+, +:Employee+)
|
|
220
|
+
# - +String+ class name (e.g., +"Employee"+)
|
|
221
|
+
# @param collection_name [Symbol] Name of the collection on the
|
|
222
|
+
# target class (e.g., +:domains+, +:members+)
|
|
223
|
+
# @param score [Symbol, Proc, Numeric, nil] Scoring strategy for
|
|
224
|
+
# sorted collections:
|
|
217
225
|
# - +Symbol+: Field name or method name (e.g., +:priority+, +:created_at+)
|
|
218
226
|
# - +Proc+: Dynamic calculation executed in participant instance context
|
|
219
227
|
# - +Numeric+: Static score applied to all participants
|
|
@@ -221,17 +229,24 @@ module Familia
|
|
|
221
229
|
# - +:remove+: Remove from all collections on destruction (default)
|
|
222
230
|
# - +:ignore+: Leave in collections when destroyed
|
|
223
231
|
# @param type [Symbol] Valkey/Redis collection type:
|
|
224
|
-
# - +:sorted_set+: Ordered by score, allows duplicates with
|
|
232
|
+
# - +:sorted_set+: Ordered by score, allows duplicates with
|
|
233
|
+
# different scores (default)
|
|
225
234
|
# - +:set+: Unordered unique membership
|
|
226
235
|
# - +:list+: Ordered sequence, allows duplicates
|
|
227
|
-
# @param bidirectional [Boolean] Whether to generate
|
|
236
|
+
# @param bidirectional [Boolean] Whether to generate reverse collection
|
|
237
|
+
# methods on participant class. If true, methods are generated using the
|
|
238
|
+
# name of the target class. (default: +true+)
|
|
239
|
+
# @param as [Symbol, nil] Custom name for reverse collection methods
|
|
240
|
+
# (e.g., +as: :contracting_orgs+). When provided, overrides the default
|
|
241
|
+
# method name derived from the target class.
|
|
242
|
+
#
|
|
243
|
+
# @example Basic domain-employee relationship
|
|
228
244
|
#
|
|
229
|
-
# @example Basic domain-customer relationship
|
|
230
245
|
# class Domain < Familia::Horreum
|
|
231
246
|
# field :name
|
|
232
247
|
# field :created_at
|
|
233
248
|
#
|
|
234
|
-
# participates_in
|
|
249
|
+
# participates_in Employee, :domains, score: :created_at
|
|
235
250
|
# end
|
|
236
251
|
#
|
|
237
252
|
# # Usage:
|
|
@@ -241,6 +256,7 @@ module Familia
|
|
|
241
256
|
# domain.current_participations # All collections domain belongs to
|
|
242
257
|
#
|
|
243
258
|
# @example Multi-collection participation with different types
|
|
259
|
+
#
|
|
244
260
|
# class Employee < Familia::Horreum
|
|
245
261
|
# field :hire_date
|
|
246
262
|
# field :skill_level
|
|
@@ -267,42 +283,54 @@ module Familia
|
|
|
267
283
|
# @see #class_participates_in for class-level participation
|
|
268
284
|
# @see ModelInstanceMethods#current_participations for membership queries
|
|
269
285
|
# @see ModelInstanceMethods#calculate_participation_score for scoring details
|
|
270
|
-
#
|
|
271
|
-
def participates_in(
|
|
272
|
-
|
|
273
|
-
#
|
|
274
|
-
|
|
286
|
+
#
|
|
287
|
+
def participates_in(target, collection_name, score: nil, type: :sorted_set, bidirectional: true, as: nil)
|
|
288
|
+
|
|
289
|
+
# Normalize the target class parameter
|
|
290
|
+
target_class = Familia.resolve_class(target)
|
|
291
|
+
|
|
292
|
+
# Raise helpful error if target class can't be resolved
|
|
293
|
+
if target_class.nil?
|
|
294
|
+
raise ArgumentError, <<~ERROR
|
|
295
|
+
Cannot resolve target class: #{target.inspect}
|
|
296
|
+
|
|
297
|
+
The target class '#{target}' could not be found in Familia.members.
|
|
298
|
+
This usually means:
|
|
299
|
+
1. The target class hasn't been loaded/required yet (load order issue)
|
|
300
|
+
2. The target class name is misspelled
|
|
301
|
+
3. The target class doesn't inherit from Familia::Horreum
|
|
302
|
+
|
|
303
|
+
Current registered classes: #{Familia.members.filter_map(&:name).sort.join(', ')}
|
|
304
|
+
|
|
305
|
+
Solution: Ensure #{target} is defined and loaded before #{self.name}
|
|
306
|
+
ERROR
|
|
307
|
+
end
|
|
275
308
|
|
|
276
309
|
# Store metadata for this participation relationship
|
|
277
310
|
participation_relationships << ParticipationRelationship.new(
|
|
278
|
-
|
|
311
|
+
_original_target: target, # Original value as passed (Symbol/String/Class)
|
|
312
|
+
target_class: target_class, # Resolved Class object
|
|
279
313
|
collection_name: collection_name,
|
|
280
314
|
score: score,
|
|
281
|
-
|
|
282
315
|
type: type,
|
|
283
316
|
bidirectional: bidirectional,
|
|
284
317
|
)
|
|
285
318
|
|
|
286
|
-
# Resolve target class if it's a symbol/string
|
|
287
|
-
actual_target_class = if target_class.is_a?(Class)
|
|
288
|
-
target_class
|
|
289
|
-
else
|
|
290
|
-
Familia.member_by_config_name(target_class)
|
|
291
|
-
end
|
|
292
|
-
|
|
293
319
|
# STEP 0: Add participations tracking field to PARTICIPANT class (Domain)
|
|
294
|
-
# This creates the proper key: "domain:123:participations"
|
|
320
|
+
# This creates the proper key: "domain:123:participations"
|
|
295
321
|
set :participations unless method_defined?(:participations)
|
|
296
322
|
|
|
297
|
-
# STEP 1: Add collection management methods to TARGET class (
|
|
298
|
-
#
|
|
299
|
-
TargetMethods::Builder.build(
|
|
323
|
+
# STEP 1: Add collection management methods to TARGET class (Employee)
|
|
324
|
+
# Employee gets: domains, add_domain, remove_domain, etc.
|
|
325
|
+
TargetMethods::Builder.build(target_class, collection_name, type)
|
|
300
326
|
|
|
301
|
-
# STEP 2: Add participation methods to PARTICIPANT class (Domain) - only if
|
|
302
|
-
#
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
327
|
+
# STEP 2: Add participation methods to PARTICIPANT class (Domain) - only if
|
|
328
|
+
# bidirectional. e.g. in_employee_domains?, add_to_employee_domains, etc.
|
|
329
|
+
if bidirectional
|
|
330
|
+
# `as` parameter allows custom naming for reverse collections
|
|
331
|
+
# If not provided, we'll let the builder use the pluralized target class name
|
|
332
|
+
ParticipantMethods::Builder.build(self, target_class, collection_name, type, as)
|
|
333
|
+
end
|
|
306
334
|
end
|
|
307
335
|
|
|
308
336
|
# Get all participation relationships defined for this class.
|
|
@@ -365,6 +393,8 @@ module Familia
|
|
|
365
393
|
# within scoring Procs.
|
|
366
394
|
#
|
|
367
395
|
# @param target_class [Class, Symbol, String] The target class containing the collection
|
|
396
|
+
# - For instance-level participation: Class object (e.g., +Project+, +Team+)
|
|
397
|
+
# - For class-level participation: The string +'class'+ (from +class_participates_in+)
|
|
368
398
|
# @param collection_name [Symbol] The collection name within the target class
|
|
369
399
|
# @return [Float] Calculated score for sorted set positioning, falls back to current_score
|
|
370
400
|
#
|
|
@@ -402,18 +432,9 @@ module Familia
|
|
|
402
432
|
# @see #track_participation_in for reverse index management
|
|
403
433
|
# @since 1.0.0
|
|
404
434
|
def calculate_participation_score(target_class, collection_name)
|
|
405
|
-
# Find the participation configuration
|
|
435
|
+
# Find the participation configuration using the new matches? method
|
|
406
436
|
participation_config = self.class.participation_relationships.find do |details|
|
|
407
|
-
|
|
408
|
-
config_target = details.target_class
|
|
409
|
-
config_target = config_target.name if config_target.is_a?(Class)
|
|
410
|
-
config_target = config_target.to_s
|
|
411
|
-
|
|
412
|
-
comparison_target = target_class
|
|
413
|
-
comparison_target = comparison_target.name if comparison_target.is_a?(Class)
|
|
414
|
-
comparison_target = comparison_target.to_s
|
|
415
|
-
|
|
416
|
-
config_target == comparison_target && details.collection_name == collection_name
|
|
437
|
+
details.matches?(target_class, collection_name)
|
|
417
438
|
end
|
|
418
439
|
|
|
419
440
|
return current_score unless participation_config
|
|
@@ -542,6 +563,69 @@ module Familia
|
|
|
542
563
|
# @see #track_participation_in for reverse index management
|
|
543
564
|
# @see #calculate_participation_score for scoring details
|
|
544
565
|
# @since 1.0.0
|
|
566
|
+
# Get all IDs where this instance participates for a specific target class
|
|
567
|
+
#
|
|
568
|
+
# This is a shallow check - it extracts IDs from the participation index without
|
|
569
|
+
# verifying that the target Redis keys actually exist. Use this for fast ID
|
|
570
|
+
# enumeration; use *_instances methods if you need existence verification.
|
|
571
|
+
#
|
|
572
|
+
# Optimized to iterate through keys once and use Set for efficient uniqueness,
|
|
573
|
+
# reducing string operations and object allocations.
|
|
574
|
+
#
|
|
575
|
+
# @param target_class [Class] The target class to filter by
|
|
576
|
+
# @param collection_names [Array<String>, nil] Optional collection name filter
|
|
577
|
+
# @return [Array<String>] Array of unique target instance IDs
|
|
578
|
+
def participating_ids_for_target(target_class, collection_names = nil)
|
|
579
|
+
|
|
580
|
+
# Use config_name to get the proper snake_case format (e.g., "project_team")
|
|
581
|
+
target_prefix = "#{target_class.config_name}#{Familia.delim}"
|
|
582
|
+
ids = Set.new
|
|
583
|
+
|
|
584
|
+
participations.members.each do |key|
|
|
585
|
+
next unless key.start_with?(target_prefix)
|
|
586
|
+
|
|
587
|
+
parts = key.split(Familia.delim, 3) # Split into ["targetclass", "id", "collection"]
|
|
588
|
+
id = parts[1]
|
|
589
|
+
|
|
590
|
+
# If filtering by collection names, check before adding
|
|
591
|
+
if collection_names && !collection_names.empty?
|
|
592
|
+
collection = parts[2]
|
|
593
|
+
ids << id if collection_names.include?(collection)
|
|
594
|
+
else
|
|
595
|
+
ids << id
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
ids.to_a
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# Check if this instance participates in any target of a specific class
|
|
603
|
+
#
|
|
604
|
+
# This is a shallow check - it only verifies that participation entries exist
|
|
605
|
+
# in the participation index. It does NOT verify that the target Redis keys
|
|
606
|
+
# actually exist. Use this for fast membership checks.
|
|
607
|
+
#
|
|
608
|
+
# Optimized to stop scanning as soon as a match is found.
|
|
609
|
+
#
|
|
610
|
+
# @param target_class [Class] The target class to check
|
|
611
|
+
# @param collection_names [Array<String>, nil] Optional collection name filter
|
|
612
|
+
# @return [Boolean] true if any matching participation exists
|
|
613
|
+
def participating_in_target?(target_class, collection_names = nil)
|
|
614
|
+
target_prefix = "#{target_class.config_name}#{Familia.delim}"
|
|
615
|
+
|
|
616
|
+
participations.members.any? do |key|
|
|
617
|
+
next false unless key.start_with?(target_prefix)
|
|
618
|
+
|
|
619
|
+
# If filtering by specific collections, check the collection name
|
|
620
|
+
if collection_names && !collection_names.empty?
|
|
621
|
+
collection = key.split(Familia.delim, 3)[2]
|
|
622
|
+
collection_names.include?(collection)
|
|
623
|
+
else
|
|
624
|
+
true
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
545
629
|
def current_participations
|
|
546
630
|
return [] unless self.class.respond_to?(:participation_relationships)
|
|
547
631
|
|
|
@@ -555,59 +639,61 @@ module Familia
|
|
|
555
639
|
collection_keys.each do |collection_key|
|
|
556
640
|
# Parse the collection key to extract target info
|
|
557
641
|
# Expected format: "targetclass:targetid:collectionname"
|
|
558
|
-
|
|
559
|
-
next unless
|
|
560
|
-
|
|
561
|
-
target_class_config = key_parts[0]
|
|
562
|
-
target_id = key_parts[1]
|
|
563
|
-
collection_name_from_key = key_parts[2]
|
|
642
|
+
target_class_config, target_id, collection_name_from_key = collection_key.split(Familia.delim, 3)
|
|
643
|
+
next unless target_class_config && target_id && collection_name_from_key
|
|
564
644
|
|
|
565
645
|
# Find the matching participation configuration
|
|
566
646
|
# Note: target_class_config from key is snake_case
|
|
567
647
|
config = self.class.participation_relationships.find do |cfg|
|
|
568
|
-
cfg.
|
|
648
|
+
cfg.target_class.config_name == target_class_config &&
|
|
569
649
|
cfg.collection_name.to_s == collection_name_from_key
|
|
570
650
|
end
|
|
571
651
|
|
|
572
652
|
next unless config
|
|
573
653
|
|
|
574
654
|
# Find the target instance and check membership using Horreum DataTypes
|
|
655
|
+
# config.target_class is already a resolved Class object
|
|
575
656
|
begin
|
|
576
|
-
|
|
577
|
-
target_instance = target_class.find_by_id(target_id)
|
|
657
|
+
target_instance = config.target_class.find_by_id(target_id)
|
|
578
658
|
next unless target_instance
|
|
579
659
|
|
|
580
660
|
# Use Horreum's DataType accessor to get the collection
|
|
581
661
|
collection = target_instance.send(config.collection_name)
|
|
582
662
|
|
|
583
|
-
# Check membership using DataType methods
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
collection_name: config.collection_name,
|
|
588
|
-
type: config.type,
|
|
589
|
-
}
|
|
663
|
+
# Check membership using DataType methods and build ParticipationMembership
|
|
664
|
+
score = nil
|
|
665
|
+
decoded_score = nil
|
|
666
|
+
position = nil
|
|
590
667
|
|
|
591
668
|
case config.type
|
|
592
669
|
when :sorted_set
|
|
593
670
|
score = collection.score(identifier)
|
|
594
671
|
next unless score
|
|
595
672
|
|
|
596
|
-
|
|
597
|
-
membership_data[:decoded_score] = decode_score(score) if respond_to?(:decode_score)
|
|
673
|
+
decoded_score = decode_score(score) if respond_to?(:decode_score)
|
|
598
674
|
when :set
|
|
599
675
|
is_member = collection.member?(identifier)
|
|
600
676
|
next unless is_member
|
|
601
677
|
when :list
|
|
602
678
|
position = collection.to_a.index(identifier)
|
|
603
679
|
next unless position
|
|
604
|
-
|
|
605
|
-
membership_data[:position] = position
|
|
606
680
|
end
|
|
607
681
|
|
|
608
|
-
|
|
682
|
+
# Create ParticipationMembership instance
|
|
683
|
+
# Use target_class_base to get clean class name without namespace
|
|
684
|
+
membership = ParticipationMembership.new(
|
|
685
|
+
target_class: config.target_class_base,
|
|
686
|
+
target_id: target_id,
|
|
687
|
+
collection_name: config.collection_name,
|
|
688
|
+
type: config.type,
|
|
689
|
+
score: score,
|
|
690
|
+
decoded_score: decoded_score,
|
|
691
|
+
position: position
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
memberships << membership
|
|
609
695
|
rescue StandardError => e
|
|
610
|
-
Familia.
|
|
696
|
+
Familia.debug "[#{collection_key}] Error checking membership: #{e.message}"
|
|
611
697
|
next
|
|
612
698
|
end
|
|
613
699
|
end
|