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
@@ -1,160 +1,155 @@
1
1
  # lib/familia/features/safe_dump.rb
2
2
 
3
-
4
- # rubocop:disable ThreadSafety/ClassInstanceVariable
5
3
  #
6
4
  # Class instance variables are used here for feature configuration
7
5
  # (e.g., @dump_method, @load_method). These are set once and not mutated
8
6
  # at runtime, so thread safety is not a concern for this feature.
9
7
  #
10
- module Familia::Features
11
- # SafeDump is a mixin that allows models to define a list of fields that are
12
- # safe to dump. This is useful for serializing objects to JSON or other
13
- # formats where you want to ensure that only certain fields are exposed.
14
- #
15
- # To use SafeDump, include it in your model and use the DSL methods to define
16
- # safe dump fields. The fields can be either symbols or hashes. If a field is
17
- # a symbol, the method with the same name will be called on the object to
18
- # retrieve the value. If the field is a hash, the key is the field name and
19
- # the value is a lambda that will be called with the object as an argument.
20
- # The hash syntax allows you to:
21
- # * define a field name that is different from the method name
22
- # * define a field that requires some computation on-the-fly
23
- # * define a field that is not a method on the object
24
- #
25
- # Example:
26
- #
27
- # feature :safe_dump
28
- #
29
- # safe_dump_field :objid
30
- # safe_dump_field :updated
31
- # safe_dump_field :created
32
- # safe_dump_field :active, ->(obj) { obj.active? }
33
- #
34
- # Alternatively, you can define multiple fields at once:
35
- #
36
- # safe_dump_fields :objid, :updated, :created,
37
- # { active: ->(obj) { obj.active? } }
38
- #
39
- # Internally, all fields are normalized to the hash syntax and stored in
40
- # @safe_dump_field_map. `SafeDump.safe_dump_fields` returns only the list
41
- # of symbols in the order they were defined.
42
- #
43
- module SafeDump
44
- include Familia::Features::Autoloadable
45
- using Familia::Refinements::SnakeCase
46
-
47
- @dump_method = :to_json
48
- @load_method = :from_json
49
-
50
- def self.included(base)
51
- # Call the Autoloadable module's included method for post-inclusion setup
52
- super
53
-
54
- Familia.trace(:LOADED, self, base, caller(1..1)) if Familia.debug?
55
- base.extend ClassMethods
8
+ module Familia
9
+ module Features
10
+ # SafeDump is a mixin that allows models to define a list of fields that are
11
+ # safe to dump. This is useful for serializing objects to JSON or other
12
+ # formats where you want to ensure that only certain fields are exposed.
13
+ #
14
+ # To use SafeDump, include it in your model and use the DSL methods to define
15
+ # safe dump fields. The fields can be either symbols or hashes. If a field is
16
+ # a symbol, the method with the same name will be called on the object to
17
+ # retrieve the value. If the field is a hash, the key is the field name and
18
+ # the value is a lambda that will be called with the object as an argument.
19
+ # The hash syntax allows you to:
20
+ # * define a field name that is different from the method name
21
+ # * define a field that requires some computation on-the-fly
22
+ # * define a field that is not a method on the object
23
+ #
24
+ # Example:
25
+ #
26
+ # feature :safe_dump
27
+ #
28
+ # safe_dump_field :objid
29
+ # safe_dump_field :updated
30
+ # safe_dump_field :created
31
+ # safe_dump_field :active, ->(obj) { obj.active? }
32
+ #
33
+ # Alternatively, you can define multiple fields at once:
34
+ #
35
+ # safe_dump_fields :objid, :updated, :created,
36
+ # { active: ->(obj) { obj.active? } }
37
+ #
38
+ #
39
+ # Internally, all fields are normalized to the hash syntax and stored in
40
+ # `@safe_dump_field_map`. SafeDump.safe_dump_fields returns only the list
41
+ # of symbols in the order they were defined.
42
+ #
43
+ module SafeDump
44
+ Familia::Base.add_feature self, :safe_dump
56
45
 
57
- # Initialize the safe dump field map
58
- base.instance_variable_set(:@safe_dump_field_map, {})
59
- end
46
+ using Familia::Refinements::StylizeWords
60
47
 
61
- # SafeDump::ClassMethods
62
- #
63
- # These methods become available on the model class
64
- module ClassMethods
65
- # Define a single safe dump field
66
- # @param field_name [Symbol] The name of the field
67
- # @param callable [Proc, nil] Optional callable to transform the value
68
- def safe_dump_field(field_name, callable = nil)
69
- @safe_dump_field_map ||= {}
48
+ @dump_method = :to_json
49
+ @load_method = :from_json
70
50
 
71
- field_name = field_name.to_sym
72
- field_value = callable || lambda { |obj|
73
- if obj.respond_to?(:[]) && obj[field_name]
74
- obj[field_name] # Familia::DataType classes
75
- elsif obj.respond_to?(field_name)
76
- obj.send(field_name) # Regular method calls
77
- end
78
- }
51
+ def self.included(base)
52
+ Familia.trace(:LOADED, self, base) if Familia.debug?
53
+ base.extend ModelClassMethods
79
54
 
80
- @safe_dump_field_map[field_name] = field_value
55
+ # Initialize the safe dump field map
56
+ base.instance_variable_set(:@safe_dump_field_map, {})
81
57
  end
82
58
 
83
- # Define multiple safe dump fields at once
84
- # @param fields [Array] Mixed array of symbols and hashes
85
- def safe_dump_fields(*fields)
86
- # If no arguments, return field names (getter behavior)
87
- return safe_dump_field_names if fields.empty?
59
+ # SafeDump::ModelClassMethods
60
+ #
61
+ # These methods become available on the model class
62
+ module ModelClassMethods
63
+ # Define a single safe dump field
64
+ # @param field_name [Symbol] The name of the field
65
+ # @param callable [Proc, nil] Optional callable to transform the value
66
+ def safe_dump_field(field_name, callable = nil)
67
+ @safe_dump_field_map ||= {}
68
+
69
+ field_name = field_name.to_sym
70
+ field_value = callable || lambda { |obj|
71
+ if obj.respond_to?(:[]) && obj[field_name]
72
+ obj[field_name] # Familia::DataType classes
73
+ elsif obj.respond_to?(field_name)
74
+ obj.send(field_name) # Regular method calls
75
+ end
76
+ }
77
+
78
+ @safe_dump_field_map[field_name] = field_value
79
+ end
88
80
 
89
- # Otherwise, define fields (setter behavior)
90
- fields.each do |field|
91
- if field.is_a?(Symbol)
92
- safe_dump_field(field)
93
- elsif field.is_a?(Hash)
94
- field.each do |name, callable|
95
- safe_dump_field(name, callable)
81
+ # Define multiple safe dump fields at once
82
+ # @param fields [Array] Mixed array of symbols and hashes
83
+ def safe_dump_fields(*fields)
84
+ # If no arguments, return field names (getter behavior)
85
+ return safe_dump_field_names if fields.empty?
86
+
87
+ # Otherwise, define fields (setter behavior)
88
+ fields.each do |field|
89
+ if field.is_a?(Symbol)
90
+ safe_dump_field(field)
91
+ elsif field.is_a?(Hash)
92
+ field.each do |name, callable|
93
+ safe_dump_field(name, callable)
94
+ end
96
95
  end
97
96
  end
98
97
  end
99
- end
100
98
 
101
- # Returns an array of safe dump field names in the order they were defined
102
- def safe_dump_field_names
103
- (@safe_dump_field_map || {}).keys
104
- end
99
+ # Returns an array of safe dump field names in the order they were defined
100
+ def safe_dump_field_names
101
+ (@safe_dump_field_map || {}).keys
102
+ end
105
103
 
106
- # Returns the field map used for dumping
107
- def safe_dump_field_map
108
- @safe_dump_field_map || {}
109
- end
104
+ # Returns the field map used for dumping
105
+ def safe_dump_field_map
106
+ @safe_dump_field_map || {}
107
+ end
110
108
 
111
- # Legacy method for setting safe dump fields (for backward compatibility)
112
- def set_safe_dump_fields(*fields)
113
- safe_dump_fields(*fields)
109
+ # Legacy method for setting safe dump fields (for backward compatibility)
110
+ def set_safe_dump_fields(*fields)
111
+ safe_dump_fields(*fields)
112
+ end
114
113
  end
115
- end
116
114
 
117
- # Returns a hash of safe fields and their values. This method
118
- # calls the callables defined in the safe_dump_field_map with
119
- # the instance object as an argument.
120
- #
121
- # The return values are not cached, so if you call this method
122
- # multiple times, the callables will be called each time.
123
- #
124
- # Example:
125
- #
126
- # class Customer < Familia::HashKey
127
- # include SafeDump
128
- # @safe_dump_fields = [
129
- # :name,
130
- # { :active => ->(cust) { cust.active? } }
131
- # ]
132
- #
133
- # def active?
134
- # true # or false
135
- # end
136
- #
137
- # cust = Customer.new :name => 'Lucy'
138
- # cust.safe_dump
139
- # #=> { :name => 'Lucy', :active => true }
140
- #
141
- def safe_dump
142
- self.class.safe_dump_field_map.transform_values do |callable|
143
- transformed_value = callable.call(self)
144
-
145
- # If the value is a relative ancestor of SafeDump we can
146
- # call safe_dump on it, otherwise we'll just return the value as-is.
147
- if transformed_value.is_a?(SafeDump)
148
- transformed_value.safe_dump
149
- else
150
- transformed_value
115
+ # Returns a hash of safe fields and their values. This method
116
+ # calls the callables defined in the safe_dump_field_map with
117
+ # the instance object as an argument.
118
+ #
119
+ # The return values are not cached, so if you call this method
120
+ # multiple times, the callables will be called each time.
121
+ #
122
+ # Example:
123
+ #
124
+ # class Customer < Familia::HashKey
125
+ # include SafeDump
126
+ # @safe_dump_fields = [
127
+ # :name,
128
+ # { :active => ->(cust) { cust.active? } }
129
+ # ]
130
+ #
131
+ # def active?
132
+ # true # or false
133
+ # end
134
+ #
135
+ # cust = Customer.new :name => 'Lucy'
136
+ # cust.safe_dump
137
+ # #=> { :name => 'Lucy', :active => true }
138
+ #
139
+ def safe_dump
140
+ self.class.safe_dump_field_map.transform_values do |callable|
141
+ transformed_value = callable.call(self)
142
+
143
+ # If the value is a relative ancestor of SafeDump we can
144
+ # call safe_dump on it, otherwise we'll just return the value as-is.
145
+ if transformed_value.is_a?(SafeDump)
146
+ transformed_value.safe_dump
147
+ else
148
+ transformed_value
149
+ end
151
150
  end
152
151
  end
153
152
  end
154
-
155
- extend ClassMethods
156
-
157
- Familia::Base.add_feature self, :safe_dump
158
153
  end
159
154
  end
160
155
  # rubocop:enable ThreadSafety/ClassInstanceVariable
@@ -37,10 +37,10 @@
37
37
  #
38
38
  # 2. Every .dup, .to_s, +, interpolation, or method call may create hidden
39
39
  # copies:
40
- # s = "secret"
41
- # t = s.dup # New object, same content — now two copies
42
- # u = s + "123" # New string — third copy
43
- # "#{t}" # Interpolation — fourth copy
40
+ # `s = "secret"`
41
+ # `t = s.dup` # New object, same content — now two copies
42
+ # `u = s + "123"` # New string — third copy
43
+ # `"#{t}"` # Interpolation — fourth copy
44
44
  # These copies are *not* controlled by RedactedString and may persist.
45
45
  #
46
46
  # 3. String Freezing & Immutability
@@ -95,8 +95,8 @@ class RedactedString
95
95
  # Example:
96
96
  # token.expose do |plain|
97
97
  # # Good: use directly without copying
98
- # HTTP.post('/api', headers: { 'X-Token' => plain })
99
- # # Avoid: plain.dup, "prefix#{plain}", plain[0..-1], etc.
98
+ # `HTTP.post('/api', headers: { 'X-Token' => plain })`
99
+ # # Avoid: `plain.dup`, `"prefix#{plain}"`, `plain[0..-1]`, etc.
100
100
  # end
101
101
  # # Value is still accessible after block
102
102
  # token.clear! # Explicitly clear when done
@@ -76,7 +76,7 @@ module Familia
76
76
  # Transient fields should not have fast writers since they're not
77
77
  # persisted to the database.
78
78
  #
79
- # @param klass [Class] The class to define the method on
79
+ # @param _klass [Class] The class to define the method on
80
80
  #
81
81
  def define_fast_writer(_klass)
82
82
  # No fast writer for transient fields since they're not persisted
@@ -111,8 +111,8 @@ module Familia
111
111
  # This method should not be called since transient fields are not
112
112
  # persisted, but we provide it for completeness.
113
113
  #
114
- # @param value [Object] The value to serialize
115
- # @param record [Object] The record instance
114
+ # @param _value [Object] The value to serialize
115
+ # @param _record [Object] The record instance
116
116
  # @return [nil] Always nil since transient fields are not serialized
117
117
  #
118
118
  def serialize(_value, _record = nil)
@@ -126,8 +126,8 @@ module Familia
126
126
  # This method should not be called since transient fields are not
127
127
  # persisted, but we provide it for completeness.
128
128
  #
129
- # @param value [Object] The value to deserialize
130
- # @param record [Object] The record instance
129
+ # @param _value [Object] The value to deserialize
130
+ # @param _record [Object] The record instance
131
131
  # @return [nil] Always nil since transient fields are not stored
132
132
  #
133
133
  def deserialize(_value, _record = nil)
@@ -104,15 +104,17 @@ module Familia
104
104
  # (HashiCorp Vault, AWS Secrets Manager) or languages with secure memory handling.
105
105
  #
106
106
  module TransientFields
107
+ Familia::Base.add_feature self, :transient_fields, depends_on: nil
108
+
107
109
  def self.included(base)
108
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
109
- base.extend ClassMethods
110
+ Familia.trace :LOADED, self, base if Familia.debug?
111
+ base.extend ModelClassMethods
110
112
 
111
113
  # Initialize transient fields tracking
112
114
  base.instance_variable_set(:@transient_fields, []) unless base.instance_variable_defined?(:@transient_fields)
113
115
  end
114
116
 
115
- module ClassMethods
117
+ module ModelClassMethods
116
118
  # Define a transient field that automatically wraps values in RedactedString
117
119
  #
118
120
  # Transient fields are not persisted to Redis/Valkey and exist only in memory.
@@ -220,8 +222,6 @@ module Familia
220
222
  end
221
223
  end
222
224
  end
223
-
224
- Familia::Base.add_feature self, :transient_fields, depends_on: nil
225
225
  end
226
226
  end
227
227
  end
@@ -1,7 +1,7 @@
1
1
  # lib/familia/features.rb
2
2
 
3
3
  # Load the Autoloader first, then use it to load all other features
4
- require_relative 'autoloader'
4
+ require_relative 'features/autoloader'
5
5
 
6
6
  module Familia
7
7
  FeatureDefinition = Data.define(:name, :depends_on)
@@ -25,12 +25,21 @@ module Familia
25
25
  # feature options. When you enable a feature with options in different models,
26
26
  # each model stores its own separate configuration without interference.
27
27
  #
28
- # ## Project Organization with Autoloadable
28
+ # ## Project Organization with Autoloader
29
29
  #
30
- # For large projects, use {Familia::Features::Autoloadable} to automatically load
30
+ # For large projects, use {Familia::Features::Autoloader} to automatically load
31
31
  # project-specific features from a dedicated directory structure. This helps
32
32
  # organize complex models by separating features into individual files.
33
33
  #
34
+ # ### Class Reopening (Deprecated)
35
+ #
36
+ # Direct class reopening still works but generates deprecation warnings:
37
+ #
38
+ # # app/models/customer/safe_dump_extensions.rb
39
+ # class Customer
40
+ # safe_dump_fields :name, :email # Works but not recommended
41
+ # end
42
+ #
34
43
  # @example Different models with different feature options
35
44
  # class UserModel < Familia::Horreum
36
45
  # feature :object_identifier, generator: :uuid_v4
@@ -57,15 +66,15 @@ module Familia
57
66
  # # In your model file: app/models/customer.rb
58
67
  # class Customer < Familia::Horreum
59
68
  # module Features
60
- # include Familia::Features::Autoloadable
69
+ # include Familia::Features::Autoloader
61
70
  # # Automatically loads all .rb files from app/models/customer/features/
62
71
  # end
63
72
  # end
64
73
  #
65
- # @see Familia::Features::Autoloadable For automatic feature loading
74
+ # @see Familia::Features::Autoloader For automatic feature loading
66
75
  #
67
76
  module Features
68
- include Familia::Autoloader
77
+ include Familia::Features::Autoloader
69
78
 
70
79
  @features_enabled = nil
71
80
  attr_reader :features_enabled
@@ -110,10 +119,8 @@ module Familia
110
119
 
111
120
  # If there's a value provided check that it's a valid feature
112
121
  feature_name = feature_name.to_sym
113
- feature_class = Familia::Base.find_feature(feature_name, self)
114
- unless feature_class
115
- raise Familia::Problem, "Unsupported feature: #{feature_name}"
116
- end
122
+ feature_module = Familia::Base.find_feature(feature_name, self)
123
+ raise Familia::Problem, "Unsupported feature: #{feature_name}" unless feature_module
117
124
 
118
125
  # If the feature is already available, do nothing but log about it
119
126
  if features_enabled.member?(feature_name)
@@ -121,7 +128,7 @@ module Familia
121
128
  return
122
129
  end
123
130
 
124
- Familia.trace :FEATURE, nil, "#{self} includes #{feature_name.inspect}", caller(1..1) if Familia.debug?
131
+ Familia.trace :FEATURE, nil, "#{self} includes #{feature_name.inspect}" if Familia.debug?
125
132
 
126
133
  # Check dependencies and raise error if missing
127
134
  feature_def = Familia::Base.feature_definitions[feature_name]
@@ -141,22 +148,15 @@ module Familia
141
148
  options[:calling_location] = calling_location&.path
142
149
 
143
150
  # Add feature options if the class supports them (Horreum classes)
144
- if respond_to?(:add_feature_options)
145
- add_feature_options(feature_name, **options)
146
- end
151
+ add_feature_options(feature_name, **options) if respond_to?(:add_feature_options)
147
152
 
148
153
  # Extend the Familia::Base subclass (e.g. Customer) with the feature module
149
- include feature_class
150
-
151
- # Trigger post-inclusion autoloading for features that support it
152
- if feature_class.respond_to?(:post_inclusion_autoload)
153
- feature_class.post_inclusion_autoload(self, feature_name, options)
154
- end
154
+ include feature_module
155
155
 
156
156
  # NOTE: Do we want to extend Familia::DataType here? That would make it
157
157
  # possible to call safe_dump on relations fields (e.g. list, zset, hashkey).
158
158
  #
159
- # The challenge is that DataType classes (List, Set, etc.) are shared across
159
+ # The challenge is that DataType classes (List, UnsortedSet, etc.) are shared across
160
160
  # all Horreum models. If Customer extends DataType with safe_dump, then
161
161
  # Session's lists would also have it. Not ideal. If that's all we wanted
162
162
  # then we can do that by looping through every DataType class here.
@@ -116,6 +116,26 @@ module Familia
116
116
  handle_method_conflict(klass, :"#{method_name}=") do
117
117
  klass.define_method :"#{method_name}=" do |value|
118
118
  instance_variable_set(:"@#{field_name}", value)
119
+
120
+ # If this field is the identifier and object_identifier feature is loaded,
121
+ # update objid_lookup mapping when identifier is set after objid generation
122
+ if respond_to?(:objid) &&
123
+ self.class.respond_to?(:identifier_field) &&
124
+ self.class.identifier_field == field_name &&
125
+ self.class.respond_to?(:objid_lookup)
126
+ current_objid = instance_variable_get(:@objid)
127
+ self.class.objid_lookup[current_objid] = value if current_objid && value
128
+ end
129
+
130
+ # If this field is the identifier and external_identifier feature is loaded,
131
+ # update extid_lookup mapping when identifier is set after extid generation
132
+ if respond_to?(:extid) &&
133
+ self.class.respond_to?(:identifier_field) &&
134
+ self.class.identifier_field == field_name &&
135
+ self.class.respond_to?(:extid_lookup)
136
+ current_extid = instance_variable_get(:@extid)
137
+ self.class.extid_lookup[current_extid] = value if current_extid && value
138
+ end
119
139
  end
120
140
  end
121
141
  end
@@ -145,7 +165,7 @@ module Familia
145
165
 
146
166
  begin
147
167
  # Trace the operation if debugging is enabled
148
- Familia.trace :FAST_WRITER, dbclient, "#{field_name}: #{val.inspect}", caller(1..1) if Familia.debug?
168
+ Familia.trace :FAST_WRITER, nil, "#{field_name}: #{val.inspect}" if Familia.debug?
149
169
 
150
170
  # Convert value for database storage
151
171
  prepared = serialize_value(val)
@@ -190,7 +210,7 @@ module Familia
190
210
  # The default implementation passes values through unchanged.
191
211
  #
192
212
  # @param value [Object] The value to serialize
193
- # @param record [Object] The record instance (for context)
213
+ # @param _record [Object] The record instance (for context)
194
214
  # @return [Object] The serialized value
195
215
  #
196
216
  def serialize(value, _record = nil)
@@ -203,7 +223,7 @@ module Familia
203
223
  # The default implementation passes values through unchanged.
204
224
  #
205
225
  # @param value [Object] The value to deserialize
206
- # @param record [Object] The record instance (for context)
226
+ # @param _record [Object] The record instance (for context)
207
227
  # @return [Object] The deserialized value
208
228
  #
209
229
  def deserialize(value, _record = nil)
@@ -228,7 +248,7 @@ module Familia
228
248
  "method_name=#{@method_name}",
229
249
  "fast_method_name=#{@fast_method_name}",
230
250
  "on_conflict=#{@on_conflict}",
231
- "category=#{category}"
251
+ "category=#{category}",
232
252
  ]
233
253
  "#<#{self.class.name} #{attributes.join(' ')}>"
234
254
  end