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,158 +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
-
45
- Familia::Base.add_feature self, :safe_dump
46
-
47
- using Familia::Refinements::SnakeCase
48
-
49
- @dump_method = :to_json
50
- @load_method = :from_json
51
-
52
- def self.included(base)
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
+ }
88
77
 
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)
78
+ @safe_dump_field_map[field_name] = field_value
79
+ end
80
+
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
153
  end
157
154
  end
158
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)
@@ -1,6 +1,7 @@
1
1
  # lib/familia/features/transient_fields.rb
2
2
 
3
3
  require_relative 'transient_fields/redacted_string'
4
+ require_relative 'transient_fields/transient_field_type'
4
5
 
5
6
  module Familia
6
7
  module Features
@@ -104,18 +105,17 @@ module Familia
104
105
  # (HashiCorp Vault, AWS Secrets Manager) or languages with secure memory handling.
105
106
  #
106
107
  module TransientFields
107
-
108
- Familia::Base.add_feature self, :transient_fields, depends_on: nil
108
+ Familia::Base.add_feature self, :transient_fields, depends_on: nil, field_group: :transient_fields
109
109
 
110
110
  def self.included(base)
111
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
112
- base.extend ClassMethods
111
+ Familia.trace :LOADED, self, base if Familia.debug?
112
+ base.extend ModelClassMethods
113
113
 
114
114
  # Initialize transient fields tracking
115
115
  base.instance_variable_set(:@transient_fields, []) unless base.instance_variable_defined?(:@transient_fields)
116
116
  end
117
117
 
118
- module ClassMethods
118
+ module ModelClassMethods
119
119
  # Define a transient field that automatically wraps values in RedactedString
120
120
  #
121
121
  # Transient fields are not persisted to Redis/Valkey and exist only in memory.
@@ -144,8 +144,12 @@ module Familia
144
144
  @transient_fields ||= []
145
145
  @transient_fields << name unless @transient_fields.include?(name)
146
146
 
147
+ # Add to field_groups if the group exists
148
+ if field_groups&.key?(:transient_fields)
149
+ field_groups[:transient_fields] << name
150
+ end
151
+
147
152
  # Use the field type system for proper integration
148
- require_relative 'transient_fields/transient_field_type'
149
153
  field_type = TransientFieldType.new(name, as: as, **kwargs.merge(fast_method: false))
150
154
  register_field_type(field_type)
151
155
  end
@@ -223,7 +227,6 @@ module Familia
223
227
  end
224
228
  end
225
229
  end
226
-
227
230
  end
228
231
  end
229
232
  end
@@ -4,7 +4,7 @@
4
4
  require_relative 'features/autoloader'
5
5
 
6
6
  module Familia
7
- FeatureDefinition = Data.define(:name, :depends_on)
7
+ FeatureDefinition = Data.define(:name, :depends_on, :field_group)
8
8
 
9
9
  # Familia::Features
10
10
  #
@@ -120,9 +120,7 @@ module Familia
120
120
  # If there's a value provided check that it's a valid feature
121
121
  feature_name = feature_name.to_sym
122
122
  feature_module = Familia::Base.find_feature(feature_name, self)
123
- unless feature_module
124
- raise Familia::Problem, "Unsupported feature: #{feature_name}"
125
- end
123
+ raise Familia::Problem, "Unsupported feature: #{feature_name}" unless feature_module
126
124
 
127
125
  # If the feature is already available, do nothing but log about it
128
126
  if features_enabled.member?(feature_name)
@@ -130,7 +128,7 @@ module Familia
130
128
  return
131
129
  end
132
130
 
133
- 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?
134
132
 
135
133
  # Check dependencies and raise error if missing
136
134
  feature_def = Familia::Base.feature_definitions[feature_name]
@@ -149,23 +147,21 @@ module Familia
149
147
  calling_location = caller_locations(1, 1)&.first
150
148
  options[:calling_location] = calling_location&.path
151
149
 
152
- # Add feature options if the class supports them (Horreum classes)
153
- if respond_to?(:add_feature_options)
154
- add_feature_options(feature_name, **options)
150
+ # Initialize field group if feature declares one
151
+ if feature_def&.field_group && respond_to?(:field_group)
152
+ field_group(feature_def.field_group)
155
153
  end
156
154
 
155
+ # Add feature options if the class supports them (Horreum classes)
156
+ add_feature_options(feature_name, **options) if respond_to?(:add_feature_options)
157
+
157
158
  # Extend the Familia::Base subclass (e.g. Customer) with the feature module
158
159
  include feature_module
159
160
 
160
- # Trigger post-inclusion autoloading for features that support it
161
- if feature_module.respond_to?(:post_inclusion_autoload)
162
- feature_module.post_inclusion_autoload(self, feature_name, options)
163
- end
164
-
165
161
  # NOTE: Do we want to extend Familia::DataType here? That would make it
166
162
  # possible to call safe_dump on relations fields (e.g. list, zset, hashkey).
167
163
  #
168
- # The challenge is that DataType classes (List, Set, etc.) are shared across
164
+ # The challenge is that DataType classes (List, UnsortedSet, etc.) are shared across
169
165
  # all Horreum models. If Customer extends DataType with safe_dump, then
170
166
  # Session's lists would also have it. Not ideal. If that's all we wanted
171
167
  # then we can do that by looping through every DataType class here.
@@ -116,6 +116,8 @@ 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
+
119
121
  end
120
122
  end
121
123
  end
@@ -145,7 +147,7 @@ module Familia
145
147
 
146
148
  begin
147
149
  # Trace the operation if debugging is enabled
148
- Familia.trace :FAST_WRITER, dbclient, "#{field_name}: #{val.inspect}", caller(1..1) if Familia.debug?
150
+ Familia.trace :FAST_WRITER, nil, "#{field_name}: #{val.inspect}" if Familia.debug?
149
151
 
150
152
  # Convert value for database storage
151
153
  prepared = serialize_value(val)
@@ -190,7 +192,7 @@ module Familia
190
192
  # The default implementation passes values through unchanged.
191
193
  #
192
194
  # @param value [Object] The value to serialize
193
- # @param record [Object] The record instance (for context)
195
+ # @param _record [Object] The record instance (for context)
194
196
  # @return [Object] The serialized value
195
197
  #
196
198
  def serialize(value, _record = nil)
@@ -203,7 +205,7 @@ module Familia
203
205
  # The default implementation passes values through unchanged.
204
206
  #
205
207
  # @param value [Object] The value to deserialize
206
- # @param record [Object] The record instance (for context)
208
+ # @param _record [Object] The record instance (for context)
207
209
  # @return [Object] The deserialized value
208
210
  #
209
211
  def deserialize(value, _record = nil)
@@ -228,7 +230,7 @@ module Familia
228
230
  "method_name=#{@method_name}",
229
231
  "fast_method_name=#{@fast_method_name}",
230
232
  "on_conflict=#{@on_conflict}",
231
- "category=#{category}"
233
+ "category=#{category}",
232
234
  ]
233
235
  "#<#{self.class.name} #{attributes.join(' ')}>"
234
236
  end