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
@@ -1,497 +0,0 @@
1
- # lib/familia/features/relationships/membership.rb
2
-
3
- module Familia
4
- module Features
5
- module Relationships
6
- # Membership module for member_of relationships
7
- # Provides collision-free method naming by including collection names
8
- module Membership
9
- # Class-level membership configurations
10
- def self.included(base)
11
- base.extend ClassMethods
12
- base.include InstanceMethods
13
- super
14
- end
15
-
16
- module ClassMethods
17
- # Define a member_of relationship
18
- #
19
- # @param owner_class [Class] The class that owns the collection
20
- # @param collection_name [Symbol] Name of the collection on the owner
21
- # @param score [Symbol, Proc, nil] How to calculate the score for sorted sets
22
- # @param type [Symbol] Type of Redis collection (:sorted_set, :set, :list)
23
- #
24
- # @example Basic membership
25
- # member_of Customer, :domains
26
- #
27
- # @example Membership with scoring
28
- # member_of Team, :projects, score: -> { permission_encode(Time.now, permission_level) }
29
- def member_of(owner_class, collection_name, score: nil, type: :sorted_set)
30
- owner_class_name = owner_class.is_a?(Class) ? owner_class.name : owner_class.to_s.camelize
31
-
32
- # Store metadata for this membership relationship
33
- membership_relationships << {
34
- owner_class: owner_class,
35
- owner_class_name: owner_class_name,
36
- collection_name: collection_name,
37
- score: score,
38
- type: type
39
- }
40
-
41
- # Generate instance methods with collision-free naming
42
- owner_class_name_lower = owner_class_name.downcase
43
-
44
- # Method to add this object to the owner's collection
45
- # e.g., domain.add_to_customer_domains(customer)
46
- define_method("add_to_#{owner_class_name_lower}_#{collection_name}") do |owner_instance, score = nil|
47
- collection = owner_instance.send(collection_name)
48
- collection.add(identifier, score: score)
49
- end
50
-
51
- # Method to remove this object from the owner's collection
52
- # e.g., domain.remove_from_customer_domains(customer)
53
- define_method("remove_from_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
54
- collection = owner_instance.send(collection_name)
55
- collection.remove(identifier)
56
- end
57
-
58
- # Method to check if this object is in the owner's collection
59
- # e.g., domain.in_customer_domains?(customer)
60
- define_method("in_#{owner_class_name_lower}_#{collection_name}?") do |owner_instance|
61
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
62
-
63
- # TODO: We should be able to reduce this to a single method call on the DataType class
64
- # instance, like we do for remove above (why: each the HashKey, SortedSet, UnsortedSet,
65
- # and List classes have a `remove` method that implements the correct behaviour).
66
- case type
67
- when :sorted_set
68
- !dbclient.zscore(collection_key, identifier).nil?
69
- when :set
70
- dbclient.sismember(collection_key, identifier)
71
- when :list
72
- dbclient.lpos(collection_key, identifier) != nil
73
- end
74
- end
75
-
76
- # Method to get score in the owner's collection (for sorted sets)
77
- # e.g., domain.score_in_customer_domains(customer)
78
- if type == :sorted_set
79
- define_method("score_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
80
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
81
- dbclient.zscore(collection_key, identifier)
82
- end
83
- end
84
-
85
- # Method to get position in the owner's collection (for lists)
86
- # e.g., domain.position_in_customer_domain_list(customer)
87
- return unless type == :list
88
-
89
- define_method("position_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
90
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
91
- position = dbclient.lpos(collection_key, identifier)
92
- position
93
- end
94
- end
95
-
96
- # Get all membership relationships for this class
97
- def membership_relationships
98
- @membership_relationships ||= []
99
- end
100
-
101
- private
102
-
103
- # Generate collision-free instance methods for membership
104
- def generate_membership_instance_methods(owner_class_name, collection_name, _score_calculator, type)
105
- owner_class_name_lower = owner_class_name.downcase
106
-
107
- # Method to add this object to the owner's collection
108
- # e.g., domain.add_to_customer_domains(customer)
109
- define_method("add_to_#{owner_class_name_lower}_#{collection_name}") do |owner_instance, score = nil|
110
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
111
-
112
- # TODO: We should be able to reduce this to a single method call on the DataType class
113
- # instance, like we do for remove above (why: each the HashKey, SortedSet, UnsortedSet,
114
- # and List classes have a `remove` method that implements the correct behaviour).
115
- case type
116
- when :sorted_set
117
- # Find the owner class from the stored config
118
- membership_config = self.class.membership_relationships.find do |config|
119
- config[:owner_class_name] == owner_class_name && config[:collection_name] == collection_name
120
- end
121
- owner_class = membership_config[:owner_class] if membership_config
122
- score ||= calculate_membership_score(owner_class, collection_name)
123
- dbclient.zadd(collection_key, score, identifier)
124
- when :set
125
- dbclient.sadd(collection_key, identifier)
126
- when :list
127
- dbclient.lpush(collection_key, identifier)
128
- end
129
- end
130
-
131
- # Method to remove this object from the owner's collection
132
- # e.g., domain.remove_from_customer_domains(customer)
133
- define_method("remove_from_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
134
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
135
-
136
- # TODO: We should be able to reduce this to a single method call on the DataType class
137
- # instance, like we do for remove above (why: each the HashKey, SortedSet, UnsortedSet,
138
- # and List classes have a `remove` method that implements the correct behaviour).
139
- case type
140
- when :sorted_set
141
- dbclient.zrem(collection_key, identifier)
142
- when :set
143
- dbclient.srem(collection_key, identifier)
144
- when :list
145
- dbclient.lrem(collection_key, 0, identifier) # Remove all occurrences
146
- end
147
- end
148
-
149
- # Method to check if this object is in the owner's collection
150
- # e.g., domain.in_customer_domains?(customer)
151
- define_method("in_#{owner_class_name_lower}_#{collection_name}?") do |owner_instance|
152
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
153
-
154
- # TODO: We should be able to reduce this to a single method call on the DataType class
155
- # instance, like we do for remove above (why: each the HashKey, SortedSet, UnsortedSet,
156
- # and List classes have a `remove` method that implements the correct behaviour).
157
- case type
158
- when :sorted_set
159
- dbclient.zscore(collection_key, identifier) != nil
160
- when :set
161
- dbclient.sismember(collection_key, identifier)
162
- when :list
163
- dbclient.lpos(collection_key, identifier) != nil
164
- end
165
- end
166
-
167
- # For sorted sets, add methods to get and update scores
168
- if type == :sorted_set
169
- # Method to get score in the owner's collection
170
- # e.g., domain.score_in_customer_domains(customer)
171
- define_method("score_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
172
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
173
- dbclient.zscore(collection_key, identifier)
174
- end
175
-
176
- # Method to update score in the owner's collection
177
- # e.g., domain.update_score_in_customer_domains(customer, new_score)
178
- define_method("update_score_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance, new_score|
179
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
180
- dbclient.zadd(collection_key, new_score, identifier, xx: true) # Only update existing
181
- end
182
-
183
- # Method to get rank in the owner's collection
184
- # e.g., domain.rank_in_customer_domains(customer)
185
- define_method("rank_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance, reverse: false|
186
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
187
- if reverse
188
- dbclient.zrevrank(collection_key, identifier)
189
- else
190
- dbclient.zrank(collection_key, identifier)
191
- end
192
- end
193
- end
194
-
195
- # For lists, add position-related methods
196
- if type == :list
197
- # Method to get position in the owner's list
198
- # e.g., domain.position_in_customer_domains(customer)
199
- define_method("position_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance|
200
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
201
- dbclient.lpos(collection_key, identifier)
202
- end
203
-
204
- # Method to move to specific position in the owner's list
205
- # e.g., domain.move_in_customer_domains(customer, new_position)
206
- define_method("move_in_#{owner_class_name_lower}_#{collection_name}") do |owner_instance, new_position|
207
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
208
-
209
- # Remove and re-insert at new position
210
- dbclient.multi do |tx|
211
- tx.lrem(collection_key, 1, identifier)
212
-
213
- if new_position.zero?
214
- tx.lpush(collection_key, identifier)
215
- elsif new_position == -1
216
- tx.rpush(collection_key, identifier)
217
- else
218
- # For arbitrary positions, we need to use a more complex approach
219
- # This is simplified - proper implementation would handle edge cases
220
- tx.linsert(collection_key, 'BEFORE', dbclient.lindex(collection_key, new_position), identifier)
221
- end
222
- end
223
- end
224
- end
225
-
226
- # Method to get all owners that contain this object in the specified collection
227
- # e.g., domain.all_customer_domains_owners
228
- define_method("all_#{owner_class_name_lower}_#{collection_name}_owners") do
229
- owners = []
230
- pattern = "#{owner_class_name_lower}:*:#{collection_name}"
231
-
232
- dbclient.scan_each(match: pattern) do |key|
233
- owner_id = key.split(':')[1]
234
-
235
- # Check if this object is in this collection
236
- is_member = case type
237
- when :sorted_set
238
- dbclient.zscore(key, identifier) != nil
239
- when :set
240
- dbclient.sismember(key, identifier)
241
- when :list
242
- dbclient.lpos(key, identifier) != nil
243
- end
244
-
245
- if is_member
246
- # Try to instantiate the owner object
247
- begin
248
- owners << owner_class.new(identifier: owner_id)
249
- rescue NameError
250
- # Owner class not available, just store the ID
251
- owners << { class: owner_class_name, id: owner_id }
252
- end
253
- end
254
- end
255
-
256
- owners
257
- end
258
-
259
- # Batch method to add to multiple owners' collections at once
260
- # e.g., domain.add_to_multiple_customer_domains([customer1, customer2])
261
- define_method("add_to_multiple_#{owner_class_name_lower}_#{collection_name}") do |owner_instances, score = nil|
262
- return if owner_instances.empty?
263
-
264
- dbclient.pipelined do |pipeline|
265
- owner_instances.each do |owner_instance|
266
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
267
-
268
- case type
269
- when :sorted_set
270
- # Find the owner class from the stored config
271
- membership_config = self.class.membership_relationships.find do |config|
272
- config[:owner_class_name] == owner_class_name && config[:collection_name] == collection_name
273
- end
274
- owner_class = membership_config[:owner_class] if membership_config
275
- calculated_score = score || calculate_membership_score(owner_class, collection_name)
276
- pipeline.zadd(collection_key, calculated_score, identifier)
277
- when :set
278
- pipeline.sadd(collection_key, identifier)
279
- when :list
280
- pipeline.lpush(collection_key, identifier)
281
- end
282
- end
283
- end
284
- end
285
-
286
- # Batch method to remove from multiple owners' collections at once
287
- # e.g., domain.remove_from_multiple_customer_domains([customer1, customer2])
288
- define_method("remove_from_multiple_#{owner_class_name_lower}_#{collection_name}") do |owner_instances|
289
- return if owner_instances.empty?
290
-
291
- dbclient.pipelined do |pipeline|
292
- owner_instances.each do |owner_instance|
293
- collection_key = "#{owner_class_name_lower}:#{owner_instance.identifier}:#{collection_name}"
294
-
295
- case type
296
- when :sorted_set
297
- pipeline.zrem(collection_key, identifier)
298
- when :set
299
- pipeline.srem(collection_key, identifier)
300
- when :list
301
- pipeline.lrem(collection_key, 0, identifier)
302
- end
303
- end
304
- end
305
- end
306
- end
307
- end
308
-
309
- # Instance methods for objects with membership relationships
310
- module InstanceMethods
311
- # Calculate the appropriate score for a membership relationship
312
- #
313
- # @param owner_class [Class] The owner class (e.g., Customer)
314
- # @param collection_name [Symbol] The collection name (e.g., :domains)
315
- # @return [Float] Calculated score
316
- def calculate_membership_score(owner_class, collection_name)
317
- # Find the membership configuration
318
- membership_config = self.class.membership_relationships.find do |config|
319
- config[:owner_class] == owner_class && config[:collection_name] == collection_name
320
- end
321
-
322
- return default_score unless membership_config
323
-
324
- score_calculator = membership_config[:score]
325
-
326
- # Extract the score calculation logic to reduce complexity
327
- calculated_score = extract_score_from_calculator(score_calculator)
328
- calculated_score || default_score
329
- end
330
-
331
- private
332
-
333
- def extract_score_from_calculator(score_calculator)
334
- case score_calculator
335
- when Symbol
336
- extract_score_from_symbol(score_calculator)
337
- when Proc
338
- extract_score_from_proc(score_calculator)
339
- when Numeric
340
- score_calculator.to_f
341
- end
342
- end
343
-
344
- def extract_score_from_symbol(symbol)
345
- return nil unless respond_to?(symbol)
346
-
347
- value = send(symbol)
348
- if value.respond_to?(:to_f)
349
- value.to_f
350
- elsif value.respond_to?(:to_i)
351
- encode_score(value, 0)
352
- end
353
- end
354
-
355
- def extract_score_from_proc(proc)
356
- result = instance_exec(&proc)
357
- return nil if result.nil?
358
-
359
- result.respond_to?(:to_f) ? result.to_f : nil
360
- end
361
-
362
- def default_score
363
- respond_to?(:current_score) ? current_score : Time.now.to_f
364
- end
365
-
366
- # Update membership in all collections atomically
367
- def update_all_memberships(_action = :add)
368
- nil unless self.class.respond_to?(:membership_relationships)
369
-
370
- # This is a simplified version - in practice, you'd need to know
371
- # which specific owner instances this object should be a member of
372
- # For now, we'll skip the automatic update and rely on explicit calls
373
- end
374
-
375
- # Remove from all membership collections (used during destroy)
376
- def remove_from_all_memberships
377
- return unless self.class.respond_to?(:membership_relationships)
378
-
379
- self.class.membership_relationships.each do |config|
380
- owner_class_name = config[:owner_class_name]
381
- collection_name = config[:collection_name]
382
- type = config[:type]
383
-
384
- # Find all collections this object is a member of
385
- pattern = "#{owner_class_name.downcase}:*:#{collection_name}"
386
-
387
- dbclient.scan_each(match: pattern) do |key|
388
- case type
389
- when :sorted_set
390
- dbclient.zrem(key, identifier)
391
- when :set
392
- dbclient.srem(key, identifier)
393
- when :list
394
- dbclient.lrem(key, 0, identifier)
395
- end
396
- end
397
- end
398
- end
399
-
400
- # Get all memberships this object has
401
- #
402
- # @return [Array<Hash>] Array of membership information
403
- def membership_collections
404
- return [] unless self.class.respond_to?(:membership_relationships)
405
-
406
- memberships = []
407
-
408
- self.class.membership_relationships.each do |config|
409
- owner_class_name = config[:owner_class_name]
410
- collection_name = config[:collection_name]
411
- type = config[:type]
412
-
413
- # Find all collections this object is a member of
414
- pattern = "#{owner_class_name.downcase}:*:#{collection_name}"
415
-
416
- dbclient.scan_each(match: pattern) do |key|
417
- is_member = case type
418
- when :sorted_set
419
- score = dbclient.zscore(key, identifier)
420
- next unless score
421
-
422
- { score: score, decoded_score: decode_score(score) }
423
- when :set
424
- next unless dbclient.sismember(key, identifier)
425
-
426
- {}
427
- when :list
428
- position = dbclient.lpos(key, identifier)
429
- next unless position
430
-
431
- { position: position }
432
- end
433
-
434
- if is_member
435
- owner_id = key.split(':')[1]
436
- memberships << {
437
- owner_class: owner_class_name,
438
- owner_id: owner_id,
439
- collection_name: collection_name,
440
- type: type,
441
- key: key
442
- }.merge(is_member)
443
- end
444
- end
445
- end
446
-
447
- memberships
448
- end
449
-
450
- # Transfer membership from one owner to another
451
- #
452
- # @param from_owner [Object] Source owner instance
453
- # @param to_owner [Object] Target owner instance
454
- # @param collection_name [Symbol] Collection to transfer membership in
455
- def transfer_membership(from_owner, to_owner, collection_name)
456
- # Find the membership configuration
457
- config = self.class.membership_relationships.find do |rel|
458
- rel[:collection_name] == collection_name &&
459
- (rel[:owner_class] == from_owner.class || rel[:owner_class_name] == from_owner.class.name)
460
- end
461
-
462
- return false unless config
463
-
464
- owner_class_name = config[:owner_class_name].downcase
465
- type = config[:type]
466
-
467
- from_key = "#{owner_class_name}:#{from_owner.identifier}:#{collection_name}"
468
- to_key = "#{owner_class_name}:#{to_owner.identifier}:#{collection_name}"
469
-
470
- dbclient.multi do |tx|
471
- case type
472
- when :sorted_set
473
- score = dbclient.zscore(from_key, identifier)
474
- if score
475
- tx.zrem(from_key, identifier)
476
- tx.zadd(to_key, score, identifier)
477
- end
478
- when :set
479
- if dbclient.sismember(from_key, identifier)
480
- tx.srem(from_key, identifier)
481
- tx.sadd(to_key, identifier)
482
- end
483
- when :list
484
- if dbclient.lpos(from_key, identifier)
485
- tx.lrem(from_key, 1, identifier)
486
- tx.lpush(to_key, identifier)
487
- end
488
- end
489
- end
490
-
491
- true
492
- end
493
- end
494
- end
495
- end
496
- end
497
- end