familia 2.0.0.pre14 → 2.0.0.pre16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/code-quality.yml +138 -0
  3. data/.github/workflows/code-smellage.yml +145 -0
  4. data/.github/workflows/docs.yml +31 -8
  5. data/.gitignore +1 -1
  6. data/.pre-commit-config.yaml +7 -1
  7. data/.reek.yml +98 -0
  8. data/.rubocop.yml +48 -10
  9. data/.talismanrc +9 -0
  10. data/.yardopts +18 -13
  11. data/CHANGELOG.rst +66 -6
  12. data/CLAUDE.md +1 -1
  13. data/Gemfile +6 -5
  14. data/Gemfile.lock +99 -23
  15. data/LICENSE.txt +1 -1
  16. data/README.md +285 -85
  17. data/changelog.d/README.md +2 -2
  18. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  19. data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
  20. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  21. data/docs/archive/README.md +3 -2
  22. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  23. data/docs/conf.py +29 -0
  24. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  25. data/docs/guides/feature-encrypted-fields.md +785 -0
  26. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  27. data/docs/guides/feature-external-identifiers.md +637 -0
  28. data/docs/guides/feature-object-identifiers.md +435 -0
  29. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  30. data/docs/guides/feature-relationships-methods.md +684 -0
  31. data/docs/guides/feature-relationships.md +200 -0
  32. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  33. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  34. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  35. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  36. data/docs/guides/index.md +176 -0
  37. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  38. data/docs/migrating/v2.0.0-pre.md +1 -1
  39. data/docs/migrating/v2.0.0-pre11.md +4 -4
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre13.md +1 -1
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +623 -19
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +6 -6
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +49 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/commands.rb +53 -51
  65. data/lib/familia/data_type/serialization.rb +108 -107
  66. data/lib/familia/data_type/types/counter.rb +1 -1
  67. data/lib/familia/data_type/types/hashkey.rb +13 -10
  68. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  69. data/lib/familia/data_type/types/lock.rb +3 -2
  70. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  71. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  72. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  73. data/lib/familia/data_type.rb +75 -47
  74. data/lib/familia/distinguisher.rb +85 -0
  75. data/lib/familia/encryption/encrypted_data.rb +15 -24
  76. data/lib/familia/encryption/manager.rb +6 -4
  77. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  78. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  79. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  80. data/lib/familia/encryption/request_cache.rb +7 -7
  81. data/lib/familia/encryption.rb +2 -3
  82. data/lib/familia/errors.rb +9 -3
  83. data/lib/familia/{autoloader.rb → features/autoloader.rb} +49 -23
  84. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  85. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  86. data/lib/familia/features/encrypted_fields.rb +68 -66
  87. data/lib/familia/features/expiration/extensions.rb +61 -0
  88. data/lib/familia/features/expiration.rb +35 -87
  89. data/lib/familia/features/external_identifier.rb +11 -12
  90. data/lib/familia/features/object_identifier.rb +58 -20
  91. data/lib/familia/features/quantization.rb +17 -22
  92. data/lib/familia/features/relationships/README.md +97 -0
  93. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  94. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  95. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  96. data/lib/familia/features/relationships/indexing.rb +176 -256
  97. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  98. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  99. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  100. data/lib/familia/features/relationships/participation.rb +656 -0
  101. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  102. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  103. data/lib/familia/features/relationships.rb +69 -271
  104. data/lib/familia/features/safe_dump.rb +127 -132
  105. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  106. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  107. data/lib/familia/features/transient_fields.rb +5 -5
  108. data/lib/familia/features.rb +21 -21
  109. data/lib/familia/field_type.rb +24 -4
  110. data/lib/familia/horreum/core/connection.rb +229 -26
  111. data/lib/familia/horreum/core/database_commands.rb +27 -17
  112. data/lib/familia/horreum/core/serialization.rb +40 -20
  113. data/lib/familia/horreum/core/utils.rb +2 -1
  114. data/lib/familia/horreum/shared/settings.rb +2 -1
  115. data/lib/familia/horreum/subclass/definition.rb +33 -45
  116. data/lib/familia/horreum/subclass/management.rb +72 -24
  117. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  118. data/lib/familia/horreum.rb +196 -114
  119. data/lib/familia/json_serializer.rb +0 -1
  120. data/lib/familia/logging.rb +11 -114
  121. data/lib/familia/refinements/dear_json.rb +122 -0
  122. data/lib/familia/refinements/logger_trace.rb +20 -17
  123. data/lib/familia/refinements/stylize_words.rb +65 -0
  124. data/lib/familia/refinements/time_literals.rb +60 -52
  125. data/lib/familia/refinements.rb +2 -1
  126. data/lib/familia/secure_identifier.rb +60 -28
  127. data/lib/familia/settings.rb +83 -7
  128. data/lib/familia/utils.rb +5 -87
  129. data/lib/familia/verifiable_identifier.rb +4 -4
  130. data/lib/familia/version.rb +1 -1
  131. data/lib/familia.rb +72 -15
  132. data/lib/middleware/database_middleware.rb +56 -14
  133. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  134. data/try/configuration/scenarios_try.rb +1 -1
  135. data/try/connection/fiber_context_preservation_try.rb +250 -0
  136. data/try/connection/handler_constraints_try.rb +59 -0
  137. data/try/connection/operation_mode_guards_try.rb +208 -0
  138. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  139. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  140. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  141. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  142. data/try/connection/transaction_mode_strict_try.rb +98 -0
  143. data/try/connection/transaction_mode_warn_try.rb +131 -0
  144. data/try/connection/transaction_modes_try.rb +249 -0
  145. data/try/core/autoloader_try.rb +129 -11
  146. data/try/core/connection_try.rb +7 -7
  147. data/try/core/conventional_inheritance_try.rb +130 -0
  148. data/try/core/create_method_try.rb +15 -23
  149. data/try/core/database_consistency_try.rb +10 -10
  150. data/try/core/errors_try.rb +8 -11
  151. data/try/core/familia_extended_try.rb +2 -2
  152. data/try/core/familia_members_methods_try.rb +76 -0
  153. data/try/core/isolated_dbclient_try.rb +165 -0
  154. data/try/core/middleware_try.rb +16 -16
  155. data/try/core/persistence_operations_try.rb +4 -4
  156. data/try/core/pools_try.rb +42 -26
  157. data/try/core/secure_identifier_try.rb +28 -24
  158. data/try/core/time_utils_try.rb +10 -10
  159. data/try/core/tools_try.rb +1 -1
  160. data/try/core/utils_try.rb +2 -2
  161. data/try/data_types/boolean_try.rb +4 -4
  162. data/try/data_types/datatype_base_try.rb +0 -2
  163. data/try/data_types/list_try.rb +10 -10
  164. data/try/data_types/sorted_set_try.rb +5 -5
  165. data/try/data_types/string_try.rb +12 -12
  166. data/try/data_types/unsortedset_try.rb +33 -0
  167. data/try/debugging/cache_behavior_tracer.rb +7 -7
  168. data/try/debugging/debug_aad_process.rb +1 -1
  169. data/try/debugging/debug_concealed_internal.rb +1 -1
  170. data/try/debugging/debug_cross_context.rb +1 -1
  171. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  172. data/try/debugging/encryption_method_tracer.rb +10 -10
  173. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  174. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  175. data/try/encryption/config_persistence_try.rb +2 -2
  176. data/try/encryption/encryption_core_try.rb +19 -19
  177. data/try/encryption/instance_variable_scope_try.rb +1 -1
  178. data/try/encryption/module_loading_try.rb +2 -2
  179. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  180. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  181. data/try/encryption/secure_memory_handling_try.rb +1 -1
  182. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  183. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  184. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  185. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  186. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  187. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  188. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  189. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  190. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  191. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  192. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  193. data/try/features/feature_dependencies_try.rb +3 -3
  194. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  195. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  196. data/try/features/quantization/quantization_try.rb +1 -1
  197. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  198. data/try/features/relationships/indexing_try.rb +433 -0
  199. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  200. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  201. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  202. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  203. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  204. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  205. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  206. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  207. data/try/features/relationships/relationships_performance_try.rb +20 -20
  208. data/try/features/relationships/relationships_try.rb +27 -38
  209. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  210. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  211. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  212. data/try/helpers/test_cleanup.rb +86 -0
  213. data/try/helpers/test_helpers.rb +3 -3
  214. data/try/horreum/base_try.rb +3 -2
  215. data/try/horreum/commands_try.rb +1 -1
  216. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  217. data/try/horreum/initialization_try.rb +11 -7
  218. data/try/horreum/relations_try.rb +21 -13
  219. data/try/horreum/serialization_try.rb +12 -11
  220. data/try/integration/cross_component_try.rb +3 -3
  221. data/try/memory/memory_basic_test.rb +1 -1
  222. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  223. data/try/models/customer_safe_dump_try.rb +1 -1
  224. data/try/models/customer_try.rb +8 -10
  225. data/try/models/datatype_base_try.rb +3 -3
  226. data/try/models/familia_object_try.rb +9 -8
  227. data/try/performance/benchmarks_try.rb +2 -2
  228. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  229. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  230. data/try/prototypes/atomic_saves_v4.rb +1 -1
  231. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  232. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  234. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  235. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  236. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  237. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  238. data/try/prototypes/pooling/pool_siege.rb +11 -11
  239. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  240. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  241. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  242. data/try/refinements/logger_trace_methods_try.rb +44 -0
  243. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  244. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  245. metadata +77 -45
  246. data/.rubocop_todo.yml +0 -208
  247. data/docs/connection_pooling.md +0 -192
  248. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  249. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  250. data/docs/guides/Feature-System-Autoloading.md +0 -228
  251. data/docs/guides/Home.md +0 -116
  252. data/docs/guides/Relationships-Guide.md +0 -737
  253. data/docs/guides/relationships-methods.md +0 -266
  254. data/docs/reference/auditing_database_commands.rb +0 -228
  255. data/examples/permissions.rb +0 -240
  256. data/lib/familia/features/autoloadable.rb +0 -113
  257. data/lib/familia/features/relationships/cascading.rb +0 -437
  258. data/lib/familia/features/relationships/membership.rb +0 -497
  259. data/lib/familia/features/relationships/permission_management.rb +0 -264
  260. data/lib/familia/features/relationships/querying.rb +0 -615
  261. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  262. data/lib/familia/features/relationships/tracking.rb +0 -418
  263. data/lib/familia/refinements/snake_case.rb +0 -40
  264. data/lib/familia/validation/command_recorder.rb +0 -336
  265. data/lib/familia/validation/expectations.rb +0 -519
  266. data/lib/familia/validation/validation_helpers.rb +0 -443
  267. data/lib/familia/validation/validator.rb +0 -412
  268. data/lib/familia/validation.rb +0 -140
  269. data/try/data_types/set_try.rb +0 -33
  270. data/try/features/autoloadable/autoloadable_try.rb +0 -61
  271. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  272. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
  273. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  274. data/try/validation/command_validation_try.rb.disabled +0 -207
  275. data/try/validation/performance_validation_try.rb.disabled +0 -324
  276. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -0,0 +1,160 @@
1
+ # lib/familia/features/relationships/participation/participant_methods.rb
2
+
3
+ require_relative '../collection_operations'
4
+
5
+ module Familia
6
+ module Features
7
+ module Relationships
8
+ # Methods added to PARTICIPANT classes (the ones calling participates_in)
9
+ # These methods allow participant instances to manage their membership in target collections
10
+ #
11
+ # Example: When Domain calls `participates_in Customer, :domains`
12
+ # Domain instances get methods to check/manage their presence in Customer collections
13
+ module ParticipantMethods
14
+ using Familia::Refinements::StylizeWords
15
+ extend CollectionOperations
16
+
17
+ # Visual Guide for methods added to PARTICIPANT instances:
18
+ # =========================================================
19
+ # When Domain calls: participates_in Customer, :domains
20
+ #
21
+ # Domain instances (PARTICIPANT) get these methods:
22
+ # ├── in_customer_domains?(customer) # Check if I'm in this customer's domains
23
+ # ├── add_to_customer_domains(customer, score) # Add myself to customer's domains
24
+ # ├── remove_from_customer_domains(customer) # Remove myself from customer's domains
25
+ # ├── score_in_customer_domains(customer) # Get my score (sorted_set only)
26
+ # ├── update_score_in_customer_domains(customer) # Update my score (sorted_set only)
27
+ # └── position_in_customer_domains(customer) # Get my position (list only)
28
+
29
+ module Builder
30
+ extend CollectionOperations
31
+
32
+ # Build all participant methods for a participation relationship
33
+ # @param participant_class [Class] The class receiving these methods (e.g., Domain)
34
+ # @param target_class_name [String] Name of the target class (e.g., "Customer")
35
+ # @param collection_name [Symbol] Name of the collection (e.g., :domains)
36
+ # @param type [Symbol] Collection type (:sorted_set, :set, :list)
37
+ def self.build(participant_class, target_class_name, collection_name, type)
38
+ # Convert to snake_case once for consistency (target_class_name is PascalCase)
39
+ target_name = target_class_name.to_s.snake_case
40
+
41
+ # Core participant methods
42
+ build_membership_check(participant_class, target_name, collection_name, type)
43
+ build_add_to_target(participant_class, target_name, collection_name, type)
44
+ build_remove_from_target(participant_class, target_name, collection_name, type)
45
+
46
+ # Type-specific methods
47
+ case type
48
+ when :sorted_set
49
+ build_score_methods(participant_class, target_name, collection_name)
50
+ when :list
51
+ build_position_method(participant_class, target_name, collection_name)
52
+ end
53
+ end
54
+
55
+ # Build method to check membership in target's collection
56
+ # Creates: domain.in_customer_domains?(customer)
57
+ def self.build_membership_check(participant_class, target_name, collection_name, _type)
58
+ method_name = "in_#{target_name}_#{collection_name}?"
59
+
60
+ participant_class.define_method(method_name) do |target_instance|
61
+ return false unless target_instance&.identifier
62
+
63
+ # Use Horreum's DataType accessor instead of manual creation
64
+ collection = target_instance.send(collection_name)
65
+ ParticipantMethods::Builder.member_of_collection?(collection, self)
66
+ end
67
+ end
68
+
69
+ # Build method to add self to target's collection
70
+ # Creates: domain.add_to_customer_domains(customer, score)
71
+ def self.build_add_to_target(participant_class, target_name, collection_name, type)
72
+ method_name = "add_to_#{target_name}_#{collection_name}"
73
+
74
+ participant_class.define_method(method_name) do |target_instance, score = nil|
75
+ return unless target_instance&.identifier
76
+
77
+ # Use Horreum's DataType accessor instead of manual creation
78
+ collection = target_instance.send(collection_name)
79
+
80
+ # Calculate score if needed and not provided
81
+ if type == :sorted_set && score.nil?
82
+ score = calculate_participation_score(target_instance.class, collection_name)
83
+ end
84
+
85
+ # Use transaction for atomicity between collection add and reverse index tracking
86
+ # All operations use Horreum's DataType methods (not direct Redis calls)
87
+ target_instance.transaction do |_tx|
88
+ # Add to collection using DataType method (ZADD/SADD/RPUSH)
89
+ ParticipantMethods::Builder.add_to_collection(
90
+ collection,
91
+ self,
92
+ score: score,
93
+ type: type,
94
+ target_class: target_instance.class,
95
+ collection_name: collection_name,
96
+ )
97
+
98
+ # Track participation for efficient cleanup using DataType method (SADD)
99
+ track_participation_in(collection.dbkey) if respond_to?(:track_participation_in)
100
+ end
101
+ end
102
+ end
103
+
104
+ # Build method to remove self from target's collection
105
+ # Creates: domain.remove_from_customer_domains(customer)
106
+ def self.build_remove_from_target(participant_class, target_name, collection_name, type)
107
+ method_name = "remove_from_#{target_name}_#{collection_name}"
108
+
109
+ participant_class.define_method(method_name) do |target_instance|
110
+ return unless target_instance&.identifier
111
+
112
+ # Use Horreum's DataType accessor instead of manual creation
113
+ collection = target_instance.send(collection_name)
114
+
115
+ # Use transaction for atomicity between collection remove and reverse index untracking
116
+ # All operations use Horreum's DataType methods (not direct Redis calls)
117
+ target_instance.transaction do |_tx|
118
+ # Remove from collection using DataType method (ZREM/SREM/LREM)
119
+ ParticipantMethods::Builder.remove_from_collection(collection, self, type: type)
120
+
121
+ # Remove from participation tracking using DataType method (SREM)
122
+ untrack_participation_in(collection.dbkey) if respond_to?(:untrack_participation_in)
123
+ end
124
+ end
125
+ end
126
+
127
+ # Build score-related methods for sorted sets
128
+ # Creates: domain.score_in_customer_domains(customer)
129
+ # domain.update_score_in_customer_domains(customer, new_score)
130
+ def self.build_score_methods(participant_class, target_name, collection_name)
131
+ # Get score method
132
+ score_method = "score_in_#{target_name}_#{collection_name}"
133
+ participant_class.define_method(score_method) do |target_instance|
134
+ return nil unless target_instance&.identifier
135
+
136
+ # Use Horreum's DataType accessor instead of manual key construction
137
+ collection = target_instance.send(collection_name)
138
+ collection.score(identifier)
139
+ end
140
+ end
141
+
142
+ # Build position method for lists
143
+ # Creates: domain.position_in_customer_domains(customer)
144
+ def self.build_position_method(participant_class, target_name, collection_name)
145
+ method_name = "position_in_#{target_name}_#{collection_name}"
146
+
147
+ participant_class.define_method(method_name) do |target_instance|
148
+ return nil unless target_instance&.identifier
149
+
150
+ # Use Horreum's DataType accessor instead of manual key construction
151
+ collection = target_instance.send(collection_name)
152
+ # Use DataType method to find position (index in list)
153
+ collection.to_a.index(identifier)
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,225 @@
1
+ # lib/familia/features/relationships/participation/target_methods.rb
2
+
3
+ require_relative '../collection_operations'
4
+
5
+ module Familia
6
+ module Features
7
+ module Relationships
8
+ # Methods added to TARGET classes (the ones specified in participates_in)
9
+ # These methods allow target instances to manage their collections of participants
10
+ #
11
+ # Example: When Domain calls `participates_in Customer, :domains`
12
+ # Customer instances get methods to manage their domains collection
13
+ module TargetMethods
14
+ using Familia::Refinements::StylizeWords
15
+ extend CollectionOperations
16
+
17
+ # Visual Guide for methods added to TARGET instances:
18
+ # ====================================================
19
+ # When Domain calls: participates_in Customer, :domains
20
+ #
21
+ # Customer instances (TARGET) get these methods:
22
+ # ├── domains # Get the domains collection
23
+ # ├── add_domain(domain, score) # Add a domain to my collection
24
+ # ├── remove_domain(domain) # Remove a domain from my collection
25
+ # ├── add_domains([...]) # Bulk add domains
26
+ # └── domains_with_permission(level) # Query with score filtering (sorted_set only)
27
+
28
+ module Builder
29
+ extend CollectionOperations
30
+
31
+ # Build all target methods for a participation relationship
32
+ # @param target_class [Class] The class receiving these methods (e.g., Customer)
33
+ # @param collection_name [Symbol] Name of the collection (e.g., :domains)
34
+ # @param type [Symbol] Collection type (:sorted_set, :set, :list)
35
+ def self.build(target_class, collection_name, type)
36
+ # FIRST: Ensure the DataType field is defined on the target class
37
+ TargetMethods::Builder.ensure_collection_field(target_class, collection_name, type)
38
+
39
+ # Core target methods
40
+ build_collection_getter(target_class, collection_name, type)
41
+ build_add_item(target_class, collection_name, type)
42
+ build_remove_item(target_class, collection_name, type)
43
+ build_bulk_add(target_class, collection_name, type)
44
+
45
+ # Type-specific methods
46
+ return unless type == :sorted_set
47
+
48
+ build_permission_query(target_class, collection_name)
49
+ end
50
+
51
+ # Build class-level collection methods (for class_participates_in)
52
+ # @param target_class [Class] The class receiving these methods
53
+ # @param collection_name [Symbol] Name of the collection
54
+ # @param type [Symbol] Collection type
55
+ def self.build_class_level(target_class, collection_name, type)
56
+ # FIRST: Ensure the class-level DataType field is defined
57
+ target_class.send("class_#{type}", collection_name)
58
+
59
+ # Class-level collection getter (e.g., User.all_users)
60
+ build_class_collection_getter(target_class, collection_name, type)
61
+ build_class_add_method(target_class, collection_name, type)
62
+ build_class_remove_method(target_class, collection_name)
63
+ end
64
+
65
+ # Build method to get the collection
66
+ # Creates: customer.domains
67
+ def self.build_collection_getter(target_class, collection_name, type)
68
+ # No need to define the method - Horreum automatically creates it
69
+ # when we call ensure_collection_field above. This method is
70
+ # kept for backwards compatibility but now does nothing.
71
+ # The field definition (sorted_set :domains) creates the accessor automatically.
72
+ end
73
+
74
+ # Build method to add an item to the collection
75
+ # Creates: customer.add_domain(domain, score)
76
+ def self.build_add_item(target_class, collection_name, type)
77
+ singular_name = collection_name.to_s.singularize
78
+ method_name = "add_#{singular_name}"
79
+
80
+ target_class.define_method(method_name) do |item, score = nil|
81
+ collection = send(collection_name)
82
+
83
+ # Calculate score if needed and not provided
84
+ if type == :sorted_set && score.nil? && item.respond_to?(:calculate_participation_score)
85
+ score = item.calculate_participation_score(self.class, collection_name)
86
+ end
87
+
88
+ # Use transaction for atomicity between collection add and reverse index tracking
89
+ # All operations use Horreum's DataType methods (not direct Redis calls)
90
+ transaction do |_tx|
91
+ # Add to collection using DataType method (ZADD/SADD/RPUSH)
92
+ TargetMethods::Builder.add_to_collection(
93
+ collection,
94
+ item,
95
+ score: score,
96
+ type: type,
97
+ target_class: self.class,
98
+ collection_name: collection_name,
99
+ )
100
+
101
+ # Track participation in reverse index using DataType method (SADD)
102
+ item.track_participation_in(collection.dbkey) if item.respond_to?(:track_participation_in)
103
+ end
104
+ end
105
+ end
106
+
107
+ # Build method to remove an item from the collection
108
+ # Creates: customer.remove_domain(domain)
109
+ def self.build_remove_item(target_class, collection_name, type)
110
+ singular_name = collection_name.to_s.singularize
111
+ method_name = "remove_#{singular_name}"
112
+
113
+ target_class.define_method(method_name) do |item|
114
+ collection = send(collection_name)
115
+
116
+ # Use transaction for atomicity between collection remove and reverse index untracking
117
+ # All operations use Horreum's DataType methods (not direct Redis calls)
118
+ transaction do |_tx|
119
+ # Remove from collection using DataType method (ZREM/SREM/LREM)
120
+ TargetMethods::Builder.remove_from_collection(collection, item, type: type)
121
+
122
+ # Remove from participation tracking using DataType method (SREM)
123
+ item.untrack_participation_in(collection.dbkey) if item.respond_to?(:untrack_participation_in)
124
+ end
125
+ end
126
+ end
127
+
128
+ # Build method for bulk adding items
129
+ # Creates: customer.add_domains([domain1, domain2, ...])
130
+ def self.build_bulk_add(target_class, collection_name, type)
131
+ method_name = "add_#{collection_name}"
132
+
133
+ target_class.define_method(method_name) do |items|
134
+ return if items.empty?
135
+
136
+ collection = send(collection_name)
137
+
138
+ # Use transaction for atomicity across all bulk additions and reverse index tracking
139
+ # All operations use Horreum's DataType methods (not direct Redis calls)
140
+ transaction do |_tx|
141
+ # Bulk add to collection using DataType methods (multiple ZADD/SADD/RPUSH)
142
+ TargetMethods::Builder.bulk_add_to_collection(collection, items, type: type, target_class: self.class,
143
+ collection_name: collection_name)
144
+
145
+ # Track all participations using DataType methods (multiple SADD)
146
+ items.each do |item|
147
+ item.track_participation_in(collection.dbkey) if item.respond_to?(:track_participation_in)
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ # Build permission query for sorted sets
154
+ # Creates: customer.domains_with_permission(min_level)
155
+ def self.build_permission_query(target_class, collection_name)
156
+ method_name = "#{collection_name}_with_permission"
157
+
158
+ target_class.define_method(method_name) do |min_permission = :read|
159
+ collection = send(collection_name)
160
+
161
+ # Assumes ScoreEncoding module is available
162
+ if defined?(ScoreEncoding)
163
+ permission_score = ScoreEncoding.permission_encode(0, min_permission)
164
+ collection.zrangebyscore(permission_score, '+inf', with_scores: true)
165
+ else
166
+ # Fallback to all members if ScoreEncoding not available
167
+ collection.members(with_scores: true)
168
+ end
169
+ end
170
+ end
171
+
172
+ # Build class-level collection getter
173
+ # Creates: User.all_users (class method)
174
+ def self.build_class_collection_getter(target_class, collection_name, type)
175
+ # No need to define the method - Horreum automatically creates it
176
+ # when we call class_#{type} above. This method is kept for
177
+ # backwards compatibility but now does nothing.
178
+ # The field definition (class_sorted_set :all_users) creates the accessor automatically.
179
+ end
180
+
181
+ # Build class-level add method
182
+ # Creates: User.add_to_all_users(user, score)
183
+ def self.build_class_add_method(target_class, collection_name, type)
184
+ method_name = "add_to_#{collection_name}"
185
+
186
+ target_class.define_singleton_method(method_name) do |item, score = nil|
187
+ collection = send(collection_name.to_s)
188
+
189
+ # Calculate score if needed
190
+ if type == :sorted_set && score.nil?
191
+ score = if item.respond_to?(:calculate_participation_score)
192
+ item.calculate_participation_score('class', collection_name)
193
+ elsif item.respond_to?(:current_score)
194
+ item.current_score
195
+ else
196
+ Familia.now.to_f
197
+ end
198
+ end
199
+
200
+ TargetMethods::Builder.add_to_collection(
201
+ collection,
202
+ item,
203
+ score: score,
204
+ type: type,
205
+ target_class: self.class,
206
+ collection_name: collection_name,
207
+ )
208
+ end
209
+ end
210
+
211
+ # Build class-level remove method
212
+ # Creates: User.remove_from_all_users(user)
213
+ def self.build_class_remove_method(target_class, collection_name)
214
+ method_name = "remove_from_#{collection_name}"
215
+
216
+ target_class.define_singleton_method(method_name) do |item|
217
+ collection = send(collection_name.to_s)
218
+ TargetMethods::Builder.remove_from_collection(collection, item)
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end