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,122 @@
1
+ # lib/familia/refinements/dear_json.rb
2
+
3
+ require 'familia/json_serializer'
4
+
5
+ module Familia
6
+ module Refinements
7
+ # DearJson provides standard JSON methods for core Ruby classes using
8
+ # Familia's secure JsonSerializer (OJ in strict mode).
9
+ #
10
+ # This refinement allows developers to use the standard Ruby JSON interface
11
+ # (as_json, to_json) on Hash and Array objects while ensuring all JSON
12
+ # serialization goes through Familia's controlled, secure serialization.
13
+ #
14
+ # @example Basic usage with refinement
15
+ # using Familia::Refinements::DearJson
16
+ #
17
+ # data = { user: user.as_json, tags: user.tags.as_json }
18
+ # json = data.to_json # Uses Familia::JsonSerializer.dump
19
+ #
20
+ # mixed_array = [user, user.tags, { meta: 'info' }]
21
+ # json = mixed_array.to_json # Handles mixed Familia/core objects
22
+ #
23
+ # @example Without refinement (manual approach)
24
+ # data = { user: user.as_json, tags: user.tags.as_json }
25
+ # json = Familia::JsonSerializer.dump(data)
26
+ #
27
+ # Security Benefits:
28
+ # - All JSON serialization uses OJ strict mode
29
+ # - Prevents accidental exposure of sensitive objects
30
+ # - Maintains Familia's security-first approach
31
+ # - Provides familiar Ruby JSON interface
32
+ #
33
+ module DearJsonHashMethods
34
+ # Convert hash to JSON string using Familia's secure JsonSerializer.
35
+ # This method preprocesses the hash to handle Familia objects properly
36
+ # by calling as_json on any objects that support it.
37
+ #
38
+ # @param options [Hash] Optional parameters (currently unused, for compatibility)
39
+ # @return [String] JSON string representation
40
+ #
41
+ def to_json(options = nil)
42
+ # Preprocess the hash to handle Familia objects
43
+ processed_hash = transform_values do |value|
44
+ if value.respond_to?(:as_json)
45
+ value.as_json(options)
46
+ else
47
+ value
48
+ end
49
+ end
50
+
51
+ Familia::JsonSerializer.dump(processed_hash)
52
+ end
53
+
54
+ # Convert hash to JSON-serializable representation.
55
+ # This method recursively calls as_json on nested values to ensure
56
+ # Familia objects are properly serialized in nested structures.
57
+ #
58
+ # @param options [Hash] Optional parameters (currently unused)
59
+ # @return [Hash] A new hash with all values converted via as_json
60
+ #
61
+ def as_json(options = nil)
62
+ # Create a new hash, calling as_json on each value.
63
+ transform_values do |value|
64
+ if value.respond_to?(:as_json)
65
+ value.as_json(options)
66
+ else
67
+ value
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ module DearJsonArrayMethods
74
+ # Convert array to JSON string using Familia's secure JsonSerializer.
75
+ # This method preprocesses the array to handle Familia objects properly
76
+ # by calling as_json on any objects that support it.
77
+ #
78
+ # @param options [Hash] Optional parameters (currently unused, for compatibility)
79
+ # @return [String] JSON string representation
80
+ #
81
+ def to_json(options = nil)
82
+ # Preprocess the array to handle Familia objects
83
+ processed_array = map do |item|
84
+ if item.respond_to?(:as_json)
85
+ item.as_json(options)
86
+ else
87
+ item
88
+ end
89
+ end
90
+
91
+ Familia::JsonSerializer.dump(processed_array)
92
+ end
93
+
94
+ # Convert array to JSON-serializable representation.
95
+ # This method recursively calls as_json on nested elements to ensure
96
+ # Familia objects are properly serialized in nested structures.
97
+ #
98
+ # @param options [Hash] Optional parameters (currently unused)
99
+ # @return [Array] A new array with all elements converted via as_json
100
+ #
101
+ def as_json(options = nil)
102
+ # Create a new array, calling as_json on each element.
103
+ map do |item|
104
+ if item.respond_to?(:as_json)
105
+ item.as_json(options)
106
+ else
107
+ item
108
+ end
109
+ end
110
+ end
111
+ end
112
+ module DearJson
113
+ refine Hash do
114
+ import_methods DearJsonHashMethods
115
+ end
116
+
117
+ refine Array do
118
+ import_methods DearJsonArrayMethods
119
+ end
120
+ end
121
+ end
122
+ end
@@ -13,7 +13,7 @@ FAMILIA_TRACE = ENV.fetch('FAMILIA_TRACE', 'false').downcase
13
13
  # '1', 'true', or 'yes' (case-insensitive).
14
14
  #
15
15
  # @example Enabling trace logging
16
- # # Set environment variable
16
+ # # UnsortedSet environment variable
17
17
  # ENV['FAMILIA_TRACE'] = 'true'
18
18
  #
19
19
  # # In your Ruby code
@@ -25,8 +25,25 @@ FAMILIA_TRACE = ENV.fetch('FAMILIA_TRACE', 'false').downcase
25
25
  #
26
26
  module Familia
27
27
  module Refinements
28
-
29
28
  # Familia::Refinements::LoggerTrace
29
+ module LoggerTraceMethods
30
+ ##
31
+ # Logs a message at the TRACE level.
32
+ #
33
+ # @param progname [String] The program name to include in the log message
34
+ # @yield A block that evaluates to the message to log
35
+ # @return [true] Always returns true
36
+ #
37
+ # @example Logging a trace message
38
+ # logger.trace("MyApp") { "Detailed trace information" }
39
+ def trace(progname = nil, &)
40
+ Fiber[:severity_letter] = 'T'
41
+ add(Familia::Refinements::LoggerTrace::TRACE, nil, progname, &)
42
+ ensure
43
+ Fiber[:severity_letter] = nil
44
+ end
45
+ end
46
+
30
47
  module LoggerTrace
31
48
  unless defined?(ENABLED)
32
49
  # Indicates whether trace logging is enabled
@@ -36,21 +53,7 @@ module Familia
36
53
  end
37
54
 
38
55
  refine Logger do
39
- ##
40
- # Logs a message at the TRACE level.
41
- #
42
- # @param progname [String] The program name to include in the log message
43
- # @yield A block that evaluates to the message to log
44
- # @return [true] Always returns true
45
- #
46
- # @example Logging a trace message
47
- # logger.trace("MyApp") { "Detailed trace information" }
48
- def trace(progname = nil, &block)
49
- Thread.current[:severity_letter] = 'T'
50
- add(Familia::Refinements::LoggerTrace::TRACE, nil, progname, &block)
51
- ensure
52
- Thread.current[:severity_letter] = nil
53
- end
56
+ import_methods LoggerTraceMethods
54
57
  end
55
58
  end
56
59
  end
@@ -0,0 +1,65 @@
1
+ # lib/familia/refinements/stylize_words.rb
2
+
3
+ module Familia
4
+ module Refinements
5
+ # Core string transformation methods that can be tested directly
6
+ module StylizeWordsMethods
7
+ # 'Models::Participants' -> 'Participants'
8
+ def demodularize
9
+ split('::').last
10
+ end
11
+
12
+ # Convert to snake_case from PascalCase/camelCase
13
+ def snake_case
14
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\\1_\\2')
15
+ .gsub(/([a-z\\d])([A-Z])/, '\\1_\\2')
16
+ .downcase
17
+ end
18
+
19
+ # Convert from plural to singular form using basic English rules
20
+ def singularize
21
+ word = to_s
22
+ if word.end_with?('ies')
23
+ "#{word[0..-4]}y"
24
+ elsif word.end_with?('es') && word.length > 3
25
+ word[0..-3]
26
+ elsif word.end_with?('s') && word.length > 1
27
+ word[0..-2]
28
+ else
29
+ word
30
+ end
31
+ end
32
+
33
+ # Convert to camelCase
34
+ def camelize
35
+ _ize(:lower)
36
+ end
37
+
38
+ # Convert to PascalCase
39
+ def pascalize
40
+ _ize(:upper)
41
+ end
42
+
43
+ private
44
+
45
+ def _ize(first_letter)
46
+ case first_letter
47
+ when :lower
48
+ parts = split(/[_-]/)
49
+ parts.first.downcase + parts[1..].map(&:capitalize).join
50
+ when :upper
51
+ split(/[_-]/).map(&:capitalize).join
52
+ else
53
+ raise ArgumentError, "Unknown stylization in first_letter: #{first_letter}"
54
+ end
55
+ end
56
+ end
57
+
58
+ # Refinement that delegates to the testable methods
59
+ module StylizeWords
60
+ refine String do
61
+ import_methods StylizeWordsMethods
62
+ end
63
+ end
64
+ end
65
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Familia
4
4
  module Refinements
5
-
6
5
  # Familia::Refinements::TimeLiterals
7
6
  #
8
7
  # This module provides a set of refinements for `Numeric` and `String` to
@@ -74,7 +73,22 @@ module Familia
74
73
  'μs' => :microseconds,
75
74
  }.freeze
76
75
 
77
- refine Numeric do
76
+ # Shared conversion logic
77
+ def self.convert_to_seconds(value, unit)
78
+ case UNIT_METHODS.fetch(unit.to_s.downcase, nil)
79
+ when :milliseconds then value * PER_MILLISECOND
80
+ when :microseconds then value * PER_MICROSECOND
81
+ when :minutes then value * PER_MINUTE
82
+ when :hours then value * PER_HOUR
83
+ when :days then value * PER_DAY
84
+ when :weeks then value * PER_WEEK
85
+ when :months then value * PER_MONTH
86
+ when :years then value * PER_YEAR
87
+ else value
88
+ end
89
+ end
90
+
91
+ module NumericMethods
78
92
  def microseconds = seconds * PER_MICROSECOND
79
93
  def milliseconds = seconds * PER_MILLISECOND
80
94
  def seconds = self
@@ -86,19 +100,19 @@ module Familia
86
100
  def years = seconds * PER_YEAR
87
101
 
88
102
  # Aliases with singular forms
89
- alias_method :microsecond, :microseconds
90
- alias_method :millisecond, :milliseconds
91
- alias_method :second, :seconds
92
- alias_method :minute, :minutes
93
- alias_method :hour, :hours
94
- alias_method :day, :days
95
- alias_method :week, :weeks
96
- alias_method :month, :months
97
- alias_method :year, :years
103
+ def microsecond = microseconds
104
+ def millisecond = milliseconds
105
+ def second = seconds
106
+ def minute = minutes
107
+ def hour = hours
108
+ def day = days
109
+ def week = weeks
110
+ def month = months
111
+ def year = years
98
112
 
99
113
  # Shortest aliases
100
- alias_method :ms, :milliseconds
101
- alias_method s, :microseconds
114
+ def ms = milliseconds
115
+ def μs = microseconds
102
116
 
103
117
  # Seconds -> other time units
104
118
  def in_years = seconds / PER_YEAR
@@ -109,12 +123,11 @@ module Familia
109
123
  def in_minutes = seconds / PER_MINUTE
110
124
  def in_milliseconds = seconds / PER_MILLISECOND
111
125
  def in_microseconds = seconds / PER_MICROSECOND
112
- # For semantic purposes
113
- def in_seconds = seconds
126
+ def in_seconds = seconds # for semantic purposes
114
127
 
115
128
  # Time manipulation
116
- def ago = Time.now.utc - seconds
117
- def from_now = Time.now.utc + seconds
129
+ def ago = Familia.now - seconds
130
+ def from_now = Familia.now + seconds
118
131
  def before(time) = time - seconds
119
132
  def after(time) = time + seconds
120
133
  def in_time = Time.at(seconds).utc
@@ -124,22 +137,10 @@ module Familia
124
137
 
125
138
  # Converts seconds to specified time unit
126
139
  #
127
- # @param u [String, Symbol] Unit to convert to
140
+ # @param unit [String, Symbol] Unit to convert to
128
141
  # @return [Float] Converted time value
129
- def in_seconds(u = nil)
130
- return self unless u
131
-
132
- case UNIT_METHODS.fetch(u.to_s.downcase, nil)
133
- when :milliseconds then self * PER_MILLISECOND
134
- when :microseconds then self * PER_MICROSECOND
135
- when :minutes then self * PER_MINUTE
136
- when :hours then self * PER_HOUR
137
- when :days then self * PER_DAY
138
- when :weeks then self * PER_WEEK
139
- when :months then self * PER_MONTH
140
- when :years then self * PER_YEAR
141
- else self
142
- end
142
+ def in_seconds(unit = nil)
143
+ unit ? TimeLiterals.convert_to_seconds(self, unit) : self
143
144
  end
144
145
 
145
146
  # Converts the number to a human-readable string representation
@@ -154,12 +155,12 @@ module Familia
154
155
  def humanize
155
156
  gte_zero = positive? || zero?
156
157
  duration = (gte_zero ? self : abs) # let's keep it positive up in here
157
- text = case (s = duration.to_i)
158
- in 0..59 then "#{s} second#{'s' if s != 1}"
159
- in 60..3599 then "#{s /= 60} minute#{'s' if s != 1}"
160
- in 3600..86_399 then "#{s /= 3600} hour#{'s' if s != 1}"
161
- else "#{s /= 86_400} day#{'s' if s != 1}"
162
- end
158
+ text = case (num = duration.to_i)
159
+ in 0..59 then "#{num} second#{'s' if num != 1}"
160
+ in 60..3599 then "#{num /= 60} minute#{'s' if num != 1}"
161
+ in 3600..86_399 then "#{num /= 3600} hour#{'s' if num != 1}"
162
+ else "#{num /= 86_400} day#{'s' if num != 1}"
163
+ end
163
164
  gte_zero ? text : "#{text} ago"
164
165
  end
165
166
 
@@ -188,7 +189,7 @@ module Familia
188
189
  # Calculates age of timestamp in specified unit from reference time
189
190
  #
190
191
  # @param unit [String, Symbol] Time unit ('days', 'hours', 'minutes', 'weeks')
191
- # @param from_time [Time, nil] Reference time (defaults to Time.now.utc)
192
+ # @param from_time [Time, nil] Reference time (defaults to Familia.now)
192
193
  # @return [Float] Age in specified unit
193
194
  # @example
194
195
  # timestamp = 2.days.ago.to_i
@@ -196,7 +197,7 @@ module Familia
196
197
  # timestamp.age_in('hours') #=> ~48.0
197
198
  # timestamp.age_in(:days, 1.day.ago) #=> ~1.0
198
199
  def age_in(unit, from_time = nil)
199
- from_time ||= Time.now.utc
200
+ from_time ||= Familia.now
200
201
  age_seconds = from_time.to_f - to_f
201
202
  case UNIT_METHODS.fetch(unit.to_s.downcase, nil)
202
203
  when :days then age_seconds / PER_DAY
@@ -211,7 +212,7 @@ module Familia
211
212
 
212
213
  # Convenience methods for `age_in(unit)` calls.
213
214
  #
214
- # @param from_time [Time, nil] Reference time (defaults to Time.now.utc)
215
+ # @param from_time [Time, nil] Reference time (defaults to Familia.now)
215
216
  # @return [Float] Age in days
216
217
  # @example
217
218
  # timestamp.days_old #=> 2.5
@@ -230,17 +231,17 @@ module Familia
230
231
  # is within the same second. Use within? to check this case.
231
232
  #
232
233
  # @example
233
- # Time.now.older_than?(1.second) #=> false
234
+ # Familia.now.older_than?(1.second) #=> false
234
235
  def older_than?(duration)
235
- self < (Time.now.utc.to_f - duration)
236
+ self < (Familia.now - duration)
236
237
  end
237
238
 
238
239
  # Checks if timestamp is newer than specified duration in the future
239
240
  #
240
241
  # @example
241
- # Time.now.newer_than?(1.second) #=> false
242
+ # Familia.now.newer_than?(1.second) #=> false
242
243
  def newer_than?(duration)
243
- self > (Time.now.utc.to_f + duration)
244
+ self > (Familia.now + duration)
244
245
  end
245
246
 
246
247
  # Checks if timestamp is within specified duration of now (past or future)
@@ -252,11 +253,11 @@ module Familia
252
253
  # 30.minutes.from_now.to_i.within?(1.hour) #=> true
253
254
  # 2.hours.ago.to_i.within?(1.hour) #=> false
254
255
  def within?(duration)
255
- (self - Time.now.utc.to_f).abs <= duration
256
+ (self - Familia.now).abs <= duration
256
257
  end
257
258
  end
258
259
 
259
- refine ::String do
260
+ module StringMethods
260
261
  # Converts string time representation to seconds
261
262
  #
262
263
  # @example
@@ -266,14 +267,21 @@ module Familia
266
267
  #
267
268
  # @return [Float, nil] Time in seconds or nil if invalid
268
269
  def in_seconds
269
- q, u = scan(/([\d.]+)([a-zA-Zμs]+)?/).flatten
270
- return nil unless q
270
+ quantity, unit = scan(/([\d.]+)([a-zA-Zμs]+)?/).flatten
271
+ return nil unless quantity
271
272
 
272
- q = q.to_f
273
- u ||= 's'
274
- q.in_seconds(u)
273
+ quantity = quantity.to_f
274
+ unit ||= 's'
275
+ TimeLiterals.convert_to_seconds(quantity, unit)
275
276
  end
276
277
  end
278
+
279
+ refine ::Numeric do
280
+ import_methods NumericMethods
281
+ end
282
+ refine ::String do
283
+ import_methods StringMethods
284
+ end
277
285
  end
278
286
  end
279
287
  end
@@ -1,5 +1,6 @@
1
1
  # lib/familia/refinements.rb
2
2
 
3
+ require_relative 'refinements/dear_json'
3
4
  require_relative 'refinements/logger_trace'
4
- require_relative 'refinements/snake_case'
5
+ require_relative 'refinements/stylize_words'
5
6
  require_relative 'refinements/time_literals'
@@ -5,41 +5,58 @@ require 'securerandom'
5
5
  # Provides a suite of tools for generating and manipulating cryptographically
6
6
  # secure identifiers in various formats and lengths.
7
7
  module Familia
8
+ # Cryptographically secure random identifiers.
9
+ #
10
+ # Strength tiers
11
+ # --------------
12
+ # 256-bit : cryptographic secrets, session tokens, API keys
13
+ # 128-bit : business/user IDs, product SKUs, non-secret resources
14
+ # 64-bit : request tracing, log correlation, ephemeral tags
15
+ #
16
+ # All methods use `SecureRandom`; collisions are probabilistic and
17
+ # scale with the number of generated values, not time.
18
+ #
8
19
  module SecureIdentifier
9
- # Generates a 256-bit cryptographically secure hexadecimal identifier.
20
+ # 256-bit identifier the "full-strength" version.
10
21
  #
11
- # @return [String] A 64-character hex string representing 256 bits of entropy.
12
- # @security Provides ~10^77 possible values, far exceeding UUIDv4's 128 bits.
13
- def generate_hex_id
14
- SecureRandom.hex(32)
15
- end
16
-
17
- # Generates a 64-bit cryptographically secure hexadecimal trace identifier.
22
+ # Safe for:
23
+ # * cryptographic secrets, session tokens, API keys
24
+ # * any identifier that must resist brute-force or intentional guessing
18
25
  #
19
- # @return [String] A 16-character hex string representing 64 bits of entropy.
20
- # @note 64 bits provides ~18 quintillion values, sufficient for request tracing.
21
- def generate_hex_trace_id
22
- SecureRandom.hex(8)
26
+ # @param base [Integer] 2–36, defaults to 36 for URL-safe chars
27
+ # @return [String] identifier in specified base, zero-padded to minimum length for 256 bits
28
+ def generate_id(base = 36)
29
+ _generate_secure_id(bits: 256, base: base)
23
30
  end
24
31
 
25
- # Generates a cryptographically secure identifier, encoded in the specified base.
26
- # By default, this creates a compact, URL-safe base-36 string.
32
+ # 128-bit identifier the "lite" version.
27
33
  #
28
- # @param base [Integer] The base for encoding the output string (2-36, default: 36).
29
- # @return [String] A secure identifier.
30
- def generate_id(base = 36)
31
- target_length = SecureIdentifier.min_length_for_bits(256, base)
32
- generate_hex_id.to_i(16).to_s(base).rjust(target_length, '0')
34
+ # Safe for:
35
+ # * ~ 10¹⁵ generated values (collision risk < 10⁻⁹)
36
+ # * business/user IDs, product SKUs, non-secret resources
37
+ #
38
+ # NOT safe for:
39
+ # * security tokens that must resist intentional guessing
40
+ #
41
+ # @param base [Integer] 2–36, defaults to 36 for URL-safe chars
42
+ # @return [String] identifier in specified base, zero-padded to minimum length for 128 bits
43
+ def generate_lite_id(base = 36)
44
+ _generate_secure_id(bits: 128, base: base)
33
45
  end
34
46
 
35
- # Generates a short, secure trace identifier, encoded in the specified base.
36
- # Suitable for tracing, logging, and other ephemeral use cases.
47
+ # 64-bit identifier the "trace" version.
48
+ #
49
+ # Safe for:
50
+ # * request tracing, log correlation, ephemeral tags
51
+ # * up to ~ 10⁹ values (collision risk < 10⁻⁶)
37
52
  #
38
- # @param base [Integer] The base for encoding the output string (2-36, default: 36).
39
- # @return [String] A secure short identifier.
53
+ # NOT safe for:
54
+ # * long-lived identifiers or security contexts
55
+ #
56
+ # @param base [Integer] 2–36, defaults to 36 for URL-safe chars
57
+ # @return [String] identifier in specified base, zero-padded to minimum length for 64 bits
40
58
  def generate_trace_id(base = 36)
41
- target_length = SecureIdentifier.min_length_for_bits(64, base)
42
- generate_hex_trace_id.to_i(16).to_s(base).rjust(target_length, '0')
59
+ _generate_secure_id(bits: 64, base: base)
43
60
  end
44
61
 
45
62
  # Creates a deterministic 64-bit trace identifier from a longer hex ID.
@@ -68,9 +85,7 @@ module Familia
68
85
  target_length = SecureIdentifier.min_length_for_bits(bits, base)
69
86
  input_bits = hex_id.length * 4
70
87
 
71
- unless hex_id.match?(/\A[0-9a-fA-F]+\z/)
72
- raise ArgumentError, "Invalid hexadecimal string: #{hex_id}"
73
- end
88
+ raise ArgumentError, "Invalid hexadecimal string: #{hex_id}" unless hex_id.match?(/\A[0-9a-fA-F]+\z/)
74
89
 
75
90
  if input_bits < bits
76
91
  raise ArgumentError, "Input bits (#{input_bits}) cannot be less than desired output bits (#{bits})."
@@ -84,6 +99,23 @@ module Familia
84
99
  # Calculates the minimum string length required to represent a given number of
85
100
  # bits in a specific numeric base.
86
101
  #
102
+ private
103
+
104
+ # Generates a secure identifier with specified bit length and base.
105
+ #
106
+ # @private
107
+ #
108
+ # @param bits [Integer] The number of bits of entropy (64, 128, or 256).
109
+ # @param base [Integer] The numeric base (2-36).
110
+ # @return [String] The generated identifier.
111
+ def _generate_secure_id(bits:, base:)
112
+ hex_id = SecureRandom.hex(bits / 8)
113
+ return hex_id if base == 16
114
+
115
+ len = SecureIdentifier.min_length_for_bits(bits, base)
116
+ hex_id.to_i(16).to_s(base).rjust(len, '0')
117
+ end
118
+
87
119
  # @private
88
120
  #
89
121
  # @param bits [Integer] The number of bits of entropy.