familia 2.0.0.pre15 → 2.0.0.pre17

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