familia 2.0.0.pre14 → 2.0.0.pre16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/code-quality.yml +138 -0
  3. data/.github/workflows/code-smellage.yml +145 -0
  4. data/.github/workflows/docs.yml +31 -8
  5. data/.gitignore +1 -1
  6. data/.pre-commit-config.yaml +7 -1
  7. data/.reek.yml +98 -0
  8. data/.rubocop.yml +48 -10
  9. data/.talismanrc +9 -0
  10. data/.yardopts +18 -13
  11. data/CHANGELOG.rst +66 -6
  12. data/CLAUDE.md +1 -1
  13. data/Gemfile +6 -5
  14. data/Gemfile.lock +99 -23
  15. data/LICENSE.txt +1 -1
  16. data/README.md +285 -85
  17. data/changelog.d/README.md +2 -2
  18. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  19. data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
  20. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  21. data/docs/archive/README.md +3 -2
  22. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  23. data/docs/conf.py +29 -0
  24. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  25. data/docs/guides/feature-encrypted-fields.md +785 -0
  26. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  27. data/docs/guides/feature-external-identifiers.md +637 -0
  28. data/docs/guides/feature-object-identifiers.md +435 -0
  29. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  30. data/docs/guides/feature-relationships-methods.md +684 -0
  31. data/docs/guides/feature-relationships.md +200 -0
  32. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  33. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  34. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  35. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  36. data/docs/guides/index.md +176 -0
  37. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  38. data/docs/migrating/v2.0.0-pre.md +1 -1
  39. data/docs/migrating/v2.0.0-pre11.md +4 -4
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre13.md +1 -1
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +623 -19
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +6 -6
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +49 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/commands.rb +53 -51
  65. data/lib/familia/data_type/serialization.rb +108 -107
  66. data/lib/familia/data_type/types/counter.rb +1 -1
  67. data/lib/familia/data_type/types/hashkey.rb +13 -10
  68. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  69. data/lib/familia/data_type/types/lock.rb +3 -2
  70. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  71. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  72. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  73. data/lib/familia/data_type.rb +75 -47
  74. data/lib/familia/distinguisher.rb +85 -0
  75. data/lib/familia/encryption/encrypted_data.rb +15 -24
  76. data/lib/familia/encryption/manager.rb +6 -4
  77. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  78. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  79. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  80. data/lib/familia/encryption/request_cache.rb +7 -7
  81. data/lib/familia/encryption.rb +2 -3
  82. data/lib/familia/errors.rb +9 -3
  83. data/lib/familia/{autoloader.rb → features/autoloader.rb} +49 -23
  84. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  85. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  86. data/lib/familia/features/encrypted_fields.rb +68 -66
  87. data/lib/familia/features/expiration/extensions.rb +61 -0
  88. data/lib/familia/features/expiration.rb +35 -87
  89. data/lib/familia/features/external_identifier.rb +11 -12
  90. data/lib/familia/features/object_identifier.rb +58 -20
  91. data/lib/familia/features/quantization.rb +17 -22
  92. data/lib/familia/features/relationships/README.md +97 -0
  93. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  94. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  95. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  96. data/lib/familia/features/relationships/indexing.rb +176 -256
  97. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  98. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  99. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  100. data/lib/familia/features/relationships/participation.rb +656 -0
  101. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  102. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  103. data/lib/familia/features/relationships.rb +69 -271
  104. data/lib/familia/features/safe_dump.rb +127 -132
  105. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  106. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  107. data/lib/familia/features/transient_fields.rb +5 -5
  108. data/lib/familia/features.rb +21 -21
  109. data/lib/familia/field_type.rb +24 -4
  110. data/lib/familia/horreum/core/connection.rb +229 -26
  111. data/lib/familia/horreum/core/database_commands.rb +27 -17
  112. data/lib/familia/horreum/core/serialization.rb +40 -20
  113. data/lib/familia/horreum/core/utils.rb +2 -1
  114. data/lib/familia/horreum/shared/settings.rb +2 -1
  115. data/lib/familia/horreum/subclass/definition.rb +33 -45
  116. data/lib/familia/horreum/subclass/management.rb +72 -24
  117. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  118. data/lib/familia/horreum.rb +196 -114
  119. data/lib/familia/json_serializer.rb +0 -1
  120. data/lib/familia/logging.rb +11 -114
  121. data/lib/familia/refinements/dear_json.rb +122 -0
  122. data/lib/familia/refinements/logger_trace.rb +20 -17
  123. data/lib/familia/refinements/stylize_words.rb +65 -0
  124. data/lib/familia/refinements/time_literals.rb +60 -52
  125. data/lib/familia/refinements.rb +2 -1
  126. data/lib/familia/secure_identifier.rb +60 -28
  127. data/lib/familia/settings.rb +83 -7
  128. data/lib/familia/utils.rb +5 -87
  129. data/lib/familia/verifiable_identifier.rb +4 -4
  130. data/lib/familia/version.rb +1 -1
  131. data/lib/familia.rb +72 -15
  132. data/lib/middleware/database_middleware.rb +56 -14
  133. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  134. data/try/configuration/scenarios_try.rb +1 -1
  135. data/try/connection/fiber_context_preservation_try.rb +250 -0
  136. data/try/connection/handler_constraints_try.rb +59 -0
  137. data/try/connection/operation_mode_guards_try.rb +208 -0
  138. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  139. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  140. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  141. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  142. data/try/connection/transaction_mode_strict_try.rb +98 -0
  143. data/try/connection/transaction_mode_warn_try.rb +131 -0
  144. data/try/connection/transaction_modes_try.rb +249 -0
  145. data/try/core/autoloader_try.rb +129 -11
  146. data/try/core/connection_try.rb +7 -7
  147. data/try/core/conventional_inheritance_try.rb +130 -0
  148. data/try/core/create_method_try.rb +15 -23
  149. data/try/core/database_consistency_try.rb +10 -10
  150. data/try/core/errors_try.rb +8 -11
  151. data/try/core/familia_extended_try.rb +2 -2
  152. data/try/core/familia_members_methods_try.rb +76 -0
  153. data/try/core/isolated_dbclient_try.rb +165 -0
  154. data/try/core/middleware_try.rb +16 -16
  155. data/try/core/persistence_operations_try.rb +4 -4
  156. data/try/core/pools_try.rb +42 -26
  157. data/try/core/secure_identifier_try.rb +28 -24
  158. data/try/core/time_utils_try.rb +10 -10
  159. data/try/core/tools_try.rb +1 -1
  160. data/try/core/utils_try.rb +2 -2
  161. data/try/data_types/boolean_try.rb +4 -4
  162. data/try/data_types/datatype_base_try.rb +0 -2
  163. data/try/data_types/list_try.rb +10 -10
  164. data/try/data_types/sorted_set_try.rb +5 -5
  165. data/try/data_types/string_try.rb +12 -12
  166. data/try/data_types/unsortedset_try.rb +33 -0
  167. data/try/debugging/cache_behavior_tracer.rb +7 -7
  168. data/try/debugging/debug_aad_process.rb +1 -1
  169. data/try/debugging/debug_concealed_internal.rb +1 -1
  170. data/try/debugging/debug_cross_context.rb +1 -1
  171. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  172. data/try/debugging/encryption_method_tracer.rb +10 -10
  173. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  174. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  175. data/try/encryption/config_persistence_try.rb +2 -2
  176. data/try/encryption/encryption_core_try.rb +19 -19
  177. data/try/encryption/instance_variable_scope_try.rb +1 -1
  178. data/try/encryption/module_loading_try.rb +2 -2
  179. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  180. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  181. data/try/encryption/secure_memory_handling_try.rb +1 -1
  182. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  183. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  184. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  185. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  186. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  187. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  188. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  189. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  190. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  191. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  192. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  193. data/try/features/feature_dependencies_try.rb +3 -3
  194. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  195. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  196. data/try/features/quantization/quantization_try.rb +1 -1
  197. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  198. data/try/features/relationships/indexing_try.rb +433 -0
  199. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  200. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  201. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  202. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  203. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  204. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  205. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  206. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  207. data/try/features/relationships/relationships_performance_try.rb +20 -20
  208. data/try/features/relationships/relationships_try.rb +27 -38
  209. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  210. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  211. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  212. data/try/helpers/test_cleanup.rb +86 -0
  213. data/try/helpers/test_helpers.rb +3 -3
  214. data/try/horreum/base_try.rb +3 -2
  215. data/try/horreum/commands_try.rb +1 -1
  216. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  217. data/try/horreum/initialization_try.rb +11 -7
  218. data/try/horreum/relations_try.rb +21 -13
  219. data/try/horreum/serialization_try.rb +12 -11
  220. data/try/integration/cross_component_try.rb +3 -3
  221. data/try/memory/memory_basic_test.rb +1 -1
  222. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  223. data/try/models/customer_safe_dump_try.rb +1 -1
  224. data/try/models/customer_try.rb +8 -10
  225. data/try/models/datatype_base_try.rb +3 -3
  226. data/try/models/familia_object_try.rb +9 -8
  227. data/try/performance/benchmarks_try.rb +2 -2
  228. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  229. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  230. data/try/prototypes/atomic_saves_v4.rb +1 -1
  231. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  232. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  234. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  235. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  236. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  237. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  238. data/try/prototypes/pooling/pool_siege.rb +11 -11
  239. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  240. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  241. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  242. data/try/refinements/logger_trace_methods_try.rb +44 -0
  243. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  244. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  245. metadata +77 -45
  246. data/.rubocop_todo.yml +0 -208
  247. data/docs/connection_pooling.md +0 -192
  248. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  249. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  250. data/docs/guides/Feature-System-Autoloading.md +0 -228
  251. data/docs/guides/Home.md +0 -116
  252. data/docs/guides/Relationships-Guide.md +0 -737
  253. data/docs/guides/relationships-methods.md +0 -266
  254. data/docs/reference/auditing_database_commands.rb +0 -228
  255. data/examples/permissions.rb +0 -240
  256. data/lib/familia/features/autoloadable.rb +0 -113
  257. data/lib/familia/features/relationships/cascading.rb +0 -437
  258. data/lib/familia/features/relationships/membership.rb +0 -497
  259. data/lib/familia/features/relationships/permission_management.rb +0 -264
  260. data/lib/familia/features/relationships/querying.rb +0 -615
  261. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  262. data/lib/familia/features/relationships/tracking.rb +0 -418
  263. data/lib/familia/refinements/snake_case.rb +0 -40
  264. data/lib/familia/validation/command_recorder.rb +0 -336
  265. data/lib/familia/validation/expectations.rb +0 -519
  266. data/lib/familia/validation/validation_helpers.rb +0 -443
  267. data/lib/familia/validation/validator.rb +0 -412
  268. data/lib/familia/validation.rb +0 -140
  269. data/try/data_types/set_try.rb +0 -33
  270. data/try/features/autoloadable/autoloadable_try.rb +0 -61
  271. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  272. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
  273. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  274. data/try/validation/command_validation_try.rb.disabled +0 -207
  275. data/try/validation/performance_validation_try.rb.disabled +0 -324
  276. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -0,0 +1,7 @@
1
+ # examples/autoloader/mega_customer/features/deprecated_fields.rb
2
+
3
+ # Extend the MegaCustomer class to organize all of the deprecated fields into one place
4
+ class MegaCustomer < Familia::Horreum
5
+ field :favourite_colour
6
+ field :nickname
7
+ end
@@ -1,6 +1,6 @@
1
1
  # examples/autoloader/mega_customer/safe_dump_fields.rb
2
2
 
3
3
  # Extend the MegaCustomer class to add safe dump fields
4
- class MegaCustomer
4
+ class MegaCustomer < Familia::Horreum
5
5
  safe_dump_fields :custid, :username, :created_at, :updated_at
6
6
  end
@@ -3,6 +3,8 @@
3
3
  require_relative '../../lib/familia'
4
4
 
5
5
  class MegaCustomer < Familia::Horreum
6
+ include Familia::Features::Autoloader
7
+
6
8
  field :custid
7
9
  field :username
8
10
  field :email
@@ -13,5 +15,5 @@ class MegaCustomer < Familia::Horreum
13
15
  field :updated_at
14
16
 
15
17
  feature :safe_dump
16
- # feature :deprecated_fields
18
+ feature :deprecated_fields
17
19
  end
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # examples/encrypted_fields.rb
4
+ #
5
+ # Demonstrates the EncryptedFields feature for protecting sensitive data.
6
+ # This feature provides transparent encryption/decryption of sensitive fields
7
+ # using strong cryptographic algorithms with field-specific key derivation.
8
+
9
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
10
+ require 'familia'
11
+
12
+ # Configure connection
13
+ Familia.uri = 'redis://localhost:6379/15'
14
+
15
+ puts '=== Encrypted Fields Feature Examples ==='
16
+ puts
17
+
18
+ # Configure encryption keys for examples
19
+ Familia.configure do |config|
20
+ config.encryption_keys = {
21
+ v1: 'dGVzdGtleWZvcmV4YW1wbGVzMTIzNDU2Nzg5MA==', # Base64 encoded 32 bytes
22
+ v2: 'bmV3ZXJrZXlmb3JleGFtcGxlczEyMzQ1Njc4OTA=', # Base64 encoded 32 bytes
23
+ }
24
+ config.current_key_version = :v2
25
+ config.encryption_personalization = 'FamiliaExamples'
26
+ end
27
+
28
+ # Validate configuration before proceeding
29
+ begin
30
+ Familia::Encryption.validate_configuration!
31
+ puts '✓ Encryption configuration validated'
32
+ puts " Algorithm: #{Familia::Encryption.status[:default_algorithm]}"
33
+ puts " Available algorithms: #{Familia::Encryption.status[:available_algorithms].join(', ')}"
34
+ puts " Key versions: #{Familia::Encryption.status[:key_versions].join(', ')}"
35
+ puts
36
+ rescue Familia::EncryptionError => e
37
+ puts "✗ Encryption configuration error: #{e.message}"
38
+ exit 1
39
+ end
40
+
41
+ # Example 1: Basic encrypted fields
42
+ class SecureUser < Familia::Horreum
43
+ feature :encrypted_fields
44
+
45
+ identifier_field :email
46
+ field :email # stored as plaintext in the database
47
+ field :name
48
+ encrypted_field :ssn # store as an encrypted string in the database
49
+ encrypted_field :credit_card
50
+ encrypted_field :notes
51
+ field :created_at
52
+ end
53
+
54
+ puts 'Example 1: Basic encrypted fields'
55
+ user = SecureUser.new(
56
+ email: 'alice@example.com',
57
+ name: 'Alice Windows',
58
+ ssn: '123-45-6789',
59
+ credit_card: '4111-1111-1111-1111',
60
+ notes: 'VIP customer with special handling',
61
+ created_at: Familia.now.to_i
62
+ )
63
+
64
+ user.save
65
+ puts '✓ User saved with encrypted fields'
66
+
67
+ # Demonstrate transparent access
68
+ puts "Name (plaintext): #{user.name}"
69
+ puts "SSN (encrypted): #{user.ssn.class} -> #{user.ssn.reveal}"
70
+ puts "Credit card: #{user.credit_card.reveal}"
71
+ puts "Notes: #{user.notes.reveal}"
72
+
73
+ # Show how ConcealedString protects data in logs
74
+ puts "SSN to_s (safe for logging): #{user.ssn}"
75
+ puts "SSN inspect (safe for logging): #{user.ssn.inspect}"
76
+ puts
77
+
78
+ # Example 2: Encrypted fields with Additional Authenticated Data (AAD)
79
+ class SecureDocument < Familia::Horreum
80
+ feature :encrypted_fields
81
+
82
+ identifier_field :doc_id
83
+ field :doc_id
84
+ field :title # Plaintext
85
+ field :owner_id # Plaintext
86
+ field :classification # Plaintext
87
+ encrypted_field :content, aad_fields: %i[doc_id owner_id classification]
88
+ encrypted_field :summary # No AAD
89
+ field :created_at # Plaintext
90
+ end
91
+
92
+ puts 'Example 2: Encrypted fields with Additional Authenticated Data'
93
+ doc = SecureDocument.new(
94
+ doc_id: 'DOC-2024-001',
95
+ title: 'Strategic Plan',
96
+ owner_id: 'user123',
97
+ classification: 'confidential',
98
+ content: 'This document contains sensitive strategic information...',
99
+ summary: 'Strategic planning document for Q1 2024',
100
+ created_at: Familia.now.to_i
101
+ )
102
+
103
+ doc.save
104
+ puts '✓ Document saved with AAD-protected content'
105
+
106
+ # AAD ensures content can only be decrypted with matching metadata
107
+ puts "Title: #{doc.title}"
108
+ puts "Content (with AAD protection): #{doc.content.reveal}"
109
+ puts "Summary (no AAD): #{doc.summary.reveal}"
110
+ puts
111
+
112
+ # Example 3: Performance optimization with request caching
113
+ class VaultEntry < Familia::Horreum
114
+ feature :encrypted_fields
115
+
116
+ identifier_field :entry_id
117
+ field :entry_id
118
+ encrypted_field :api_key
119
+ encrypted_field :secret_token
120
+ encrypted_field :private_key
121
+ encrypted_field :webhook_url
122
+ end
123
+
124
+ puts 'Example 3: Performance optimization with request caching'
125
+ entries = []
126
+
127
+ # Without caching - each field derives keys independently
128
+ start_time = Time.now
129
+ 5.times do |i|
130
+ private_key_pem = <<~PEM
131
+ -----BEGIN PRIVATE KEY-----
132
+ FAKE_EXAMPLE_KEY_FOR_TESTING_ONLY
133
+ FAKE_EXAMPLE_KEY_FOR_TESTING_ONLY
134
+ -----END PRIVATE KEY-----
135
+ PEM
136
+
137
+ entry = VaultEntry.new(
138
+ entry_id: "entry_#{i}",
139
+ api_key: "sk_test_key_#{i}",
140
+ secret_token: "token_#{i}_secret",
141
+ private_key: private_key_pem.strip,
142
+ webhook_url: "https://api.example.com/webhook/#{i}"
143
+ )
144
+ entry.save
145
+ entries << entry
146
+ end
147
+ no_cache_time = Time.now - start_time
148
+
149
+ # With caching - reuses derived keys within the block
150
+ start_time = Time.now
151
+ Familia::Encryption.with_request_cache do
152
+ 5.times do |i|
153
+ private_key_pem = <<~PEM
154
+ -----BEGIN PRIVATE KEY-----
155
+ FAKE_EXAMPLE_KEY_FOR_TESTING_ONLY
156
+ FAKE_EXAMPLE_KEY_FOR_TESTING_ONLY
157
+ -----END PRIVATE KEY-----
158
+ PEM
159
+
160
+ entry = VaultEntry.new(
161
+ entry_id: "cached_entry_#{i}",
162
+ api_key: "sk_test_key_cached_#{i}",
163
+ secret_token: "token_cached_#{i}_secret",
164
+ private_key: private_key_pem.strip,
165
+ webhook_url: "https://api.example.com/webhook/cached/#{i}"
166
+ )
167
+ entry.save
168
+ entries << entry
169
+ end
170
+ end
171
+ cached_time = Time.now - start_time
172
+
173
+ puts "Encryption without caching: #{(no_cache_time * 1000).round(2)}ms"
174
+ puts "Encryption with caching: #{(cached_time * 1000).round(2)}ms"
175
+ puts "Performance improvement: #{((no_cache_time - cached_time) / no_cache_time * 100).round(1)}%"
176
+ puts
177
+
178
+ # Example 4: Key rotation simulation
179
+ class RotationTest < Familia::Horreum
180
+ feature :encrypted_fields
181
+
182
+ identifier_field :test_id
183
+ field :test_id
184
+ encrypted_field :sensitive_data
185
+ end
186
+
187
+ puts 'Example 4: Key rotation demonstration'
188
+ rotation_obj = RotationTest.new(
189
+ test_id: 'rotation_test',
190
+ sensitive_data: 'Original sensitive data encrypted with v2 key'
191
+ )
192
+ rotation_obj.save
193
+
194
+ puts "Original data encrypted with key version: #{Familia.config.current_key_version}"
195
+ puts "Data: #{rotation_obj.sensitive_data.reveal}"
196
+
197
+ # Check encryption status before rotation
198
+ status_before = rotation_obj.encrypted_fields_status
199
+ puts "Encryption status before rotation: #{status_before}"
200
+
201
+ # Simulate key rotation - switch to v1 for demonstration
202
+ Familia.config.current_key_version = :v1
203
+
204
+ # Re-encrypt with new current key
205
+ rotation_obj.sensitive_data = 'Updated data encrypted with v1 key'
206
+ rotation_obj.re_encrypt_fields!
207
+ rotation_obj.save
208
+
209
+ puts "After rotation to key version: #{Familia.config.current_key_version}"
210
+ puts "Data: #{rotation_obj.sensitive_data.reveal}"
211
+
212
+ # Check encryption status after rotation
213
+ status_after = rotation_obj.encrypted_fields_status
214
+ puts "Encryption status after rotation: #{status_after}"
215
+
216
+ # Switch back to v2
217
+ Familia.config.current_key_version = :v2
218
+ puts
219
+
220
+ # Example 5: Memory safety and cleanup
221
+ class MemoryTest < Familia::Horreum
222
+ feature :encrypted_fields
223
+
224
+ identifier_field :mem_id
225
+ field :mem_id
226
+ encrypted_field :secret_one
227
+ encrypted_field :secret_two
228
+ encrypted_field :secret_three
229
+ end
230
+
231
+ puts 'Example 5: Memory safety and cleanup'
232
+ mem_obj = MemoryTest.new(
233
+ mem_id: 'memory_test',
234
+ secret_one: 'First secret value',
235
+ secret_two: 'Second secret value',
236
+ secret_three: 'Third secret value'
237
+ )
238
+
239
+ puts "Has encrypted data: #{mem_obj.encrypted_data?}"
240
+ puts "Fields cleared: #{mem_obj.encrypted_fields_cleared?}"
241
+
242
+ # Access some fields to load them into memory
243
+ puts "Secret one: #{mem_obj.secret_one.reveal}"
244
+ puts "Secret two: #{mem_obj.secret_two.reveal}"
245
+
246
+ # Clear specific field
247
+ mem_obj.secret_one.clear!
248
+ puts "Secret one cleared: #{mem_obj.secret_one.cleared?}"
249
+
250
+ # Clear all encrypted fields
251
+ mem_obj.clear_encrypted_fields!
252
+ puts "All fields cleared: #{mem_obj.encrypted_fields_cleared?}"
253
+ puts
254
+
255
+ # Example 6: Error handling and validation
256
+ puts 'Example 6: Error handling and validation'
257
+
258
+ # Test with invalid configuration
259
+ begin
260
+ old_keys = Familia.config.encryption_keys
261
+ Familia.config.encryption_keys = {}
262
+
263
+ invalid_obj = SecureUser.new(email: 'test@example.com', ssn: 'test')
264
+ invalid_obj.save
265
+ rescue Familia::EncryptionError => e
266
+ puts "✓ Caught expected error with invalid config: #{e.message}"
267
+ ensure
268
+ Familia.config.encryption_keys = old_keys
269
+ end
270
+
271
+ # Test with missing key version
272
+ begin
273
+ test_obj = SecureUser.new(email: 'version_test@example.com', ssn: '987-65-4321')
274
+ test_obj.save
275
+
276
+ # Simulate missing key version in config
277
+ old_keys = Familia.config.encryption_keys.dup
278
+ Familia.config.encryption_keys = { v3: old_keys[:v2] }
279
+
280
+ # This should fail when trying to decrypt
281
+ test_obj.ssn.reveal
282
+ rescue Familia::EncryptionError => e
283
+ puts "✓ Caught expected error with missing key version: #{e.message}"
284
+ ensure
285
+ Familia.config.encryption_keys = old_keys
286
+ end
287
+ puts
288
+
289
+ # Example 7: Benchmarking encryption performance
290
+ puts 'Example 7: Encryption performance benchmarks'
291
+ if defined?(Familia::Encryption) && Familia::Encryption.respond_to?(:benchmark)
292
+ benchmark_results = Familia::Encryption.benchmark(iterations: 100)
293
+
294
+ puts 'Encryption benchmark results (100 iterations):'
295
+ benchmark_results.each do |algorithm, stats|
296
+ puts " #{algorithm}:"
297
+ puts " Time: #{(stats[:time] * 1000).round(2)}ms total"
298
+ puts " Operations/sec: #{stats[:ops_per_sec]}"
299
+ puts " Priority: #{stats[:priority]}"
300
+ end
301
+ else
302
+ puts 'Benchmarking not available in this version'
303
+ end
304
+ puts
305
+
306
+ # Example 8: Integration with safe dump feature
307
+ class SecureProfile < Familia::Horreum
308
+ feature :encrypted_fields
309
+ feature :safe_dump
310
+
311
+ identifier_field :profile_id
312
+ field :profile_id
313
+ field :username # Safe to expose
314
+ field :email # Safe to expose
315
+ encrypted_field :phone # Encrypted but can be safely exposed
316
+ encrypted_field :ssn # Encrypted and should NOT be exposed
317
+ encrypted_field :bank_account # Encrypted and should NOT be exposed
318
+ field :created_at # Safe to expose
319
+
320
+ # Define safe dump fields - encrypted fields are handled automatically
321
+ safe_dump_field :profile_id
322
+ safe_dump_field :username
323
+ safe_dump_field :email
324
+ safe_dump_field :phone_display, lambda { |profile|
325
+ phone = profile.phone.reveal
326
+ phone ? "#{phone[0..2]}-***-#{phone[-4..]}" : nil
327
+ }
328
+ safe_dump_field :created_at
329
+ end
330
+
331
+ puts 'Example 8: Integration with SafeDump feature'
332
+ profile = SecureProfile.new(
333
+ profile_id: 'profile_123',
334
+ username: 'alice_windows',
335
+ email: 'alice@example.com',
336
+ phone: '555-123-4567',
337
+ ssn: '123-45-6789',
338
+ bank_account: '9876543210',
339
+ created_at: Familia.now.to_i
340
+ )
341
+ profile.save
342
+
343
+ puts 'Profile safe dump (encrypted fields handled automatically):'
344
+ puts JSON.pretty_generate(profile.safe_dump)
345
+ puts 'Notice: SSN and bank account are automatically excluded'
346
+ puts 'Phone number is included but masked for display'
347
+ puts
348
+
349
+ # Clean up examples
350
+ puts '=== Cleaning up test data ==='
351
+ [SecureUser, SecureDocument, VaultEntry, RotationTest, MemoryTest, SecureProfile].each do |klass|
352
+ keys = klass.dbclient.keys("#{klass.name.downcase.gsub('::', '_')}:*")
353
+ klass.dbclient.del(*keys) unless keys.empty?
354
+ puts "✓ Cleaned #{klass.name} (#{keys.length} keys)"
355
+ rescue StandardError => e
356
+ puts "✗ Error cleaning #{klass.name}: #{e.message}"
357
+ end
358
+
359
+ # Clear any request cache
360
+ begin
361
+ Familia::Encryption.clear_request_cache!
362
+ rescue StandardError => e
363
+ puts "⚠ Warning: Failed to clear encryption request cache: #{e.message} (#{e.backtrace.join("\n")})"
364
+ end
365
+
366
+ puts
367
+ puts '=== Summary ==='
368
+ puts 'This example demonstrated:'
369
+ puts '• Basic encrypted field usage with transparent access'
370
+ puts '• Additional Authenticated Data (AAD) for tamper detection'
371
+ puts '• Performance optimization with request-level caching'
372
+ puts '• Key rotation procedures and status monitoring'
373
+ puts '• Memory safety with ConcealedString and cleanup methods'
374
+ puts '• Error handling for configuration and key version issues'
375
+ puts '• Encryption performance benchmarking'
376
+ puts '• Integration with SafeDump for API-safe serialization'
377
+ puts
378
+ puts 'Encrypted Fields examples completed!'
@@ -0,0 +1,144 @@
1
+ # examples/json_usage_patterns.rb
2
+ #
3
+ # This file demonstrates the JSON serialization patterns available in Familia,
4
+ # showing both the secure defaults and optional developer convenience features.
5
+
6
+ require_relative '../lib/familia'
7
+
8
+ # Example model setup
9
+ class User < Familia::Horreum
10
+ feature :encrypted_fields
11
+ identifier_field :user_id
12
+ field :user_id
13
+ field :name
14
+ field :email
15
+ encrypted_field :password_hash # This will be concealed in JSON
16
+ listkey :tags # Associates a separate ListKey field
17
+ set :permissions
18
+ end
19
+
20
+ # Configure encryption (required for encrypted_fields)
21
+ Familia.config.encryption_keys = { v1: Base64.strict_encode64('a' * 32) }
22
+ Familia.config.current_key_version = :v1
23
+
24
+ # Create and save a user
25
+ user = User.new(
26
+ user_id: 'user123',
27
+ name: 'Alice Johnson',
28
+ email: 'alice@example.com',
29
+ password_hash: 'hashed_password_secret'
30
+ )
31
+ user.save
32
+
33
+ user.tags << 'admin' << 'developer'
34
+ user.permissions << 'read' << 'write'
35
+
36
+ puts "=== Familia JSON Serialization Patterns ==="
37
+ puts
38
+
39
+ # Pattern 1: Direct object serialization (always available)
40
+ puts "1. Direct Familia Object Serialization:"
41
+ puts " user.as_json # Secure - only public fields"
42
+ p user.as_json
43
+ puts " user.to_json # Uses Familia::JsonSerializer (OJ strict mode)"
44
+ puts user.to_json
45
+ puts
46
+
47
+ puts " user.tags.as_json # DataType objects work too"
48
+ p user.tags.as_json
49
+ puts " user.tags.to_json"
50
+ puts user.tags.to_json
51
+ puts
52
+
53
+ # Pattern 2: Manual mixed serialization (current secure pattern)
54
+ puts "2. Manual Mixed Serialization (Current Pattern):"
55
+ mixed_data = {
56
+ user: user.as_json,
57
+ tags: user.tags.as_json,
58
+ permissions: user.permissions.as_json,
59
+ meta: { timestamp: Time.now.to_i }
60
+ }
61
+ puts " Manual preparation + JsonSerializer.dump:"
62
+ puts Familia::JsonSerializer.dump(mixed_data)
63
+ puts
64
+
65
+ # Pattern 3: Opt-in refinement for Hash/Array (new convenience feature)
66
+ puts "3. Opt-in Refinement Pattern (Developer Convenience):"
67
+ puts " # Add this line to enable refinements in your file:"
68
+ puts " using Familia::Refinements::DearJson"
69
+ puts
70
+
71
+ # Demonstrate the refinement
72
+ require_relative '../lib/familia/refinements/dear_json'
73
+ using Familia::Refinements::DearJson
74
+
75
+ mixed_hash = {
76
+ user: user, # Familia object (will call as_json)
77
+ tags: user.tags, # Familia DataType (will call as_json)
78
+ meta: { timestamp: Time.now.to_i } # Plain hash (passes through)
79
+ }
80
+
81
+ mixed_array = [
82
+ user, # Familia object
83
+ user.tags, # Familia DataType
84
+ { type: 'example' }, # Plain hash
85
+ 'metadata' # Plain string
86
+ ]
87
+
88
+ puts " # Now Hash and Array have secure to_json using Familia::JsonSerializer"
89
+ puts " mixed_hash.to_json:"
90
+ puts mixed_hash.to_json
91
+ puts
92
+
93
+ puts " mixed_array.to_json:"
94
+ puts mixed_array.to_json
95
+ puts
96
+
97
+ # Pattern 4: Security demonstration
98
+ puts "4. Security Features (Always Active):"
99
+ puts " # Encrypted fields are automatically concealed"
100
+ puts " # This is what user.password_hash looks like:"
101
+ puts " user.password_hash.class # => #{user.password_hash.class}"
102
+ puts " user.password_hash.to_s # => #{user.password_hash.to_s}"
103
+ puts
104
+
105
+ begin
106
+ user.password_hash.to_json
107
+ rescue Familia::SerializerError => e
108
+ puts " user.password_hash.to_json # => #{e.class}: #{e.message}"
109
+ end
110
+ puts
111
+
112
+ puts " # Only public fields appear in JSON (password_hash is excluded):"
113
+ puts " user.as_json.keys # => #{user.as_json.keys}"
114
+ puts
115
+
116
+ puts "5. Framework Integration Examples:"
117
+ puts " # Rails controller"
118
+ puts " def show"
119
+ puts " render json: user # Works with as_json/to_json"
120
+ puts " end"
121
+ puts
122
+ puts " # Sinatra/Roda response"
123
+ puts " get '/user/:id' do"
124
+ puts " content_type :json"
125
+ puts " user.to_json # Direct serialization"
126
+ puts " end"
127
+ puts
128
+ puts " # API response with refinement"
129
+ puts " using Familia::Refinements::DearJson"
130
+ puts " response = {"
131
+ puts " user: user,"
132
+ puts " meta: { version: '1.0' }"
133
+ puts " }"
134
+ puts " response.to_json # Handles mixed Familia/core objects"
135
+ puts
136
+
137
+ puts "=== Summary ==="
138
+ puts "✅ Security: All JSON serialization uses OJ strict mode"
139
+ puts "✅ Encrypted fields: Automatically concealed (ConcealedString protection)"
140
+ puts "✅ Public fields only: Horreum objects expose only defined fields"
141
+ puts "✅ DataType support: Lists, sets, etc. serialize their contents"
142
+ puts "✅ Developer experience: Standard Ruby JSON interface (as_json/to_json)"
143
+ puts "✅ Opt-in convenience: Refinements for Hash/Array when desired"
144
+ puts "✅ Framework compatible: Works with Rails, Sinatra, Roda, etc."
@@ -27,7 +27,7 @@ class Customer < Familia::Horreum
27
27
 
28
28
  # Define collections for tracking relationships
29
29
  set :domains # Simple set of domain IDs
30
- list :projects # Ordered list of project IDs
30
+ listkey :projects # Ordered list of project IDs
31
31
  sorted_set :activity # Activity feed with timestamps
32
32
 
33
33
  # Create indexes for fast lookups (using class_ prefix for class-level)
@@ -35,7 +35,7 @@ class Customer < Familia::Horreum
35
35
  class_indexed_by :plan, :plan_lookup # i.e. Customer.plan_lookup
36
36
 
37
37
  # Track in class-level collections (using class_ prefix for class-level)
38
- class_tracked_in :all_customers, score: :created_at # i.e. Customer.all_customers
38
+ class_participates_in :all_customers, score: :created_at # i.e. Customer.all_customers
39
39
  end
40
40
 
41
41
  class Domain < Familia::Horreum
@@ -49,11 +49,11 @@ class Domain < Familia::Horreum
49
49
  field :status
50
50
 
51
51
  # Declare membership in customer collections
52
- member_of Customer, :domains # , type: :set
52
+ participates_in Customer, :domains # , type: :set
53
53
 
54
54
  # Track domains by status (using class_ prefix for class-level)
55
- class_tracked_in :active_domains,
56
- score: -> { status == 'active' ? Time.now.to_i : 0 }
55
+ class_participates_in :active_domains,
56
+ score: -> { status == 'active' ? Familia.now.to_i : 0 }
57
57
  end
58
58
 
59
59
  class Project < Familia::Horreum
@@ -66,7 +66,7 @@ class Project < Familia::Horreum
66
66
  field :priority
67
67
 
68
68
  # Member of customer projects list (ordered)
69
- member_of Customer, :projects, type: :list
69
+ participates_in Customer, :projects, type: :list
70
70
  end
71
71
 
72
72
  puts '=== 1. Basic Object Creation ==='
@@ -110,7 +110,7 @@ puts '=== 2. Establishing Relationships ==='
110
110
  customer.save # Automatically adds to email_lookup, plan_lookup, and all_customers
111
111
  puts '✓ Customer automatically added to indexes and tracking on save'
112
112
 
113
- # Establish member_of relationships using clean << operator syntax
113
+ # Establish participates_in relationships using clean << operator syntax
114
114
  customer.domains << domain1 # Clean Ruby-like syntax
115
115
  customer.domains << domain2 # Same as domain1.add_to_customer_domains(customer)
116
116
  customer.projects << project # Same as project.add_to_customer_projects(customer)
@@ -149,7 +149,7 @@ puts " Customer has #{customer.projects.size} projects"
149
149
  puts " Domain IDs: #{customer.domains.members}"
150
150
  puts " Project IDs: #{customer.projects.members}"
151
151
 
152
- # Test tracked_in collections
152
+ # Test participates_in collections
153
153
  all_customers_count = Customer.values.size
154
154
  puts "\nClass-level tracking:"
155
155
  puts " Total customers in system: #{all_customers_count}"
@@ -161,7 +161,7 @@ puts
161
161
  puts '=== 4. Range Queries ==='
162
162
 
163
163
  # Get recent customers (last 24 hours)
164
- yesterday = (Time.now - (24 * 3600)).to_i # 24 hours ago
164
+ yesterday = (Familia.now - (24 * 3600)).to_i # 24 hours ago
165
165
  recent_customers = Customer.values.rangebyscore(yesterday, '+inf')
166
166
  puts "Recent customers (last 24h): #{recent_customers.size}"
167
167
 
@@ -178,7 +178,7 @@ puts '=== 6. Relationship Cleanup ==='
178
178
  # Remove relationships
179
179
  puts 'Cleaning up relationships...'
180
180
 
181
- # Remove from member_of relationships
181
+ # Remove from participates_in relationships
182
182
  domain1.remove_from_customer_domains(customer)
183
183
  puts "✓ Removed #{domain1.name} from customer domains"
184
184
 
@@ -195,10 +195,10 @@ puts
195
195
  puts '=== Example Complete! ==='
196
196
  puts
197
197
  puts 'Key takeaways:'
198
- puts '• class_tracked_in: Automatic class-level collections updated on save'
198
+ puts '• class_participates_in: Automatic class-level collections updated on save'
199
199
  puts '• class_indexed_by: Automatic class-level indexes updated on save'
200
- puts '• member_of: Use << operator for clean Ruby-like collection syntax'
201
- puts '• indexed_by with parent:: Use for relationship-scoped indexes'
200
+ puts '• participates_in: Use << operator for clean Ruby-like collection syntax'
201
+ puts '• indexed_by with context:: Use for relationship-scoped indexes'
202
202
  puts '• Save operations: Automatically update indexes and class-level tracking'
203
203
  puts '• << operator: Works naturally with all collection types (sets, lists, sorted sets)'
204
204
  puts
@@ -38,10 +38,10 @@ puts 'Example 1: Basic SafeDump'
38
38
  user = User.new(
39
39
  email: 'alice@example.com',
40
40
  first_name: 'Alice',
41
- last_name: 'Smith',
41
+ last_name: 'Windows',
42
42
  password_hash: 'secret123',
43
43
  ssn: '123-45-6789',
44
- created_at: Time.now.to_i
44
+ created_at: Familia.now.to_i
45
45
  )
46
46
 
47
47
  puts "Full object data: #{user.to_h}"
@@ -70,7 +70,7 @@ class Product < Familia::Horreum
70
70
 
71
71
  # Computed fields using callables
72
72
  safe_dump_field :price, ->(product) { "$#{format('%.2f', product.price_cents.to_i / 100.0)}" }
73
- safe_dump_field :in_stock, ->(product) { product.inventory_count.to_i > 0 }
73
+ safe_dump_field :in_stock, ->(product) { product.inventory_count.to_i.positive? }
74
74
  safe_dump_field :display_name, ->(product) { "#{product.name} (#{product.sku})" }
75
75
  end
76
76
 
@@ -82,7 +82,7 @@ product = Product.new(
82
82
  cost_cents: 800, # $8.00 - sensitive, not exposed
83
83
  inventory_count: 25,
84
84
  category: 'widgets',
85
- created_at: Time.now.to_i
85
+ created_at: Familia.now.to_i
86
86
  )
87
87
 
88
88
  puts "Full object data: #{product.to_h}"
@@ -140,8 +140,8 @@ order = Order.new(
140
140
  payment_method: 'credit_card',
141
141
  credit_card_number: '4111-1111-1111-1111', # Never expose this!
142
142
  processing_notes: 'Rush order - expedite shipping',
143
- created_at: Time.now.to_i - 86_400, # Yesterday
144
- shipped_at: Time.now.to_i - 3600 # 1 hour ago
143
+ created_at: Familia.now.to_i - 86_400, # Yesterday
144
+ shipped_at: Familia.now.to_i - 3600 # 1 hour ago
145
145
  )
146
146
 
147
147
  puts "Full object data: #{order.to_h}"