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
@@ -13,7 +13,6 @@ module Familia
13
13
  # parsed = Familia::JsonSerializer.parse(json, symbolize_names: true)
14
14
  #
15
15
  module JsonSerializer
16
-
17
16
  class << self
18
17
  # Parse JSON string into Ruby objects
19
18
  #
@@ -10,6 +10,7 @@ module Familia
10
10
  severity_letter = severity[0] # Get the first letter of the severity
11
11
  pid = Process.pid
12
12
  thread_id = Thread.current.object_id
13
+ fiber_id = Fiber.current.object_id
13
14
  full_path, line = caller(5..5).first.split(':')[0..1]
14
15
  parent_path = Pathname.new(full_path).ascend.find { |p| p.basename.to_s == 'familia' }
15
16
  relative_path = full_path.sub(parent_path.to_s, 'familia')
@@ -19,9 +20,9 @@ module Familia
19
20
  # the default. The thread local variable is set in the trace
20
21
  # method in the Familia::Refinements::LoggerTrace module. The name of the
21
22
  # variable `severity_letter` is arbitrary and could be anything.
22
- severity_letter = Thread.current[:severity_letter] || severity_letter
23
+ severity_letter = Fiber[:severity_letter] || severity_letter
23
24
 
24
- "#{severity_letter}, #{utc_datetime} #{pid} #{thread_id}: #{msg} [#{relative_path}:#{line}]\n"
25
+ "#{severity_letter}, #{utc_datetime} #{pid} #{thread_id}/#{fiber_id}: #{msg} [#{relative_path}:#{line}]\n"
25
26
  end
26
27
 
27
28
  # The Logging module provides a set of methods and constants for logging messages
@@ -103,9 +104,7 @@ module Familia
103
104
  attr_reader :logger
104
105
 
105
106
  # Gives our logger the ability to use our trace method.
106
- if Familia::Refinements::LoggerTrace::ENABLED
107
- using Familia::Refinements::LoggerTrace
108
- end
107
+ using Familia::Refinements::LoggerTrace if Familia::Refinements::LoggerTrace::ENABLED
109
108
 
110
109
  def info(*msg)
111
110
  @logger.info(*msg)
@@ -129,16 +128,12 @@ module Familia
129
128
  #
130
129
  # @param label [Symbol] A label for the trace message (e.g., :EXPAND,
131
130
  # :FROMREDIS, :LOAD, :EXISTS).
132
- # @param dbclient [Redis, Redis::Future, nil] The Database instance or
133
- # Future being used.
131
+ # @param instance_id
134
132
  # @param ident [String] An identifier or key related to the operation being
135
133
  # traced.
136
- # @param context [Array<String>, String, nil] The calling context, typically
137
- # obtained from `caller` or `caller.first`. Default is nil.
134
+ # @param extra_context [Array<String>, String, nil] Any extra details to include.
138
135
  #
139
- # @example
140
- # Familia.trace :LOAD, Familia.dbclient(uri), objkey, caller(1..1) if
141
- # Familia.debug?
136
+ # @example Familia.trace :LOAD, Familia.dbclient(uri), objkey if Familia.debug?
142
137
  #
143
138
  # @return [nil]
144
139
  #
@@ -147,110 +142,12 @@ module Familia
147
142
  # pipelined and multi blocks), or nil (when the database connection isn't
148
143
  # relevant).
149
144
  #
150
- def trace(label, dbclient, ident, context = nil)
145
+ def trace(label, instance_id = nil, ident = nil, extra_context = nil)
151
146
  return unless Familia::Refinements::LoggerTrace::ENABLED
152
147
 
153
- # Usually dbclient is a Database object, but it could be
154
- # a Redis::Future which is what is used inside of pipelined
155
- # and multi blocks. In some contexts it's nil where the
156
- # database connection isn't relevant.
157
- instance_id = if dbclient
158
- case dbclient
159
- when Redis
160
- dbclient.id.respond_to?(:to_s) ? dbclient.id.to_s : dbclient.class.name
161
- when Redis::Future
162
- 'Redis::Future'
163
- else
164
- dbclient.class.name
165
- end
166
- end
167
-
168
- codeline = if context
169
- context = [context].flatten
170
- context.reject! { |line| line =~ %r{lib/familia} }
171
- context.first
172
- end
173
-
174
- @logger.trace format('[%s] %s -> %s <- at %s', label, instance_id, ident, codeline)
148
+ # Let the other values show nothing when nil, but make it known for the focused value
149
+ ident_str = (ident.nil? ? '<nil>' : ident).to_s
150
+ @logger.trace format('[%s] %s -> %s <-%s', label, instance_id, ident_str, extra_context)
175
151
  end
176
152
  end
177
153
  end
178
-
179
-
180
- __END__
181
-
182
-
183
- ### Example 1: Basic Logging
184
- ```ruby
185
- require 'logger'
186
-
187
- logger = Logger.new($stdout)
188
- logger.info("This is an info message")
189
- logger.warn("This is a warning message")
190
- logger.error("This is an error message")
191
- ```
192
-
193
- ### Example 2: Setting Log Level
194
- ```ruby
195
- require 'logger'
196
-
197
- logger = Logger.new($stdout)
198
- logger.level = Logger::WARN
199
-
200
- logger.debug("This is a debug message") # Will not be logged
201
- logger.info("This is an info message") # Will not be logged
202
- logger.warn("This is a warning message")
203
- logger.error("This is an error message")
204
- ```
205
-
206
- ### Example 3: Customizing Log Format
207
- ```ruby
208
- require 'logger'
209
-
210
- logger = Logger.new($stdout)
211
- logger.formatter = proc do |severity, datetime, progname, msg|
212
- "#{datetime}: #{severity} - #{msg}\n"
213
- end
214
-
215
- logger.info("This is an info message")
216
- logger.warn("This is a warning message")
217
- logger.error("This is an error message")
218
- ```
219
-
220
- ### Example 4: Logging with a Program Name
221
- ```ruby
222
- require 'logger'
223
-
224
- logger = Logger.new($stdout)
225
- logger.progname = 'Familia'
226
-
227
- logger.info("This is an info message")
228
- logger.warn("This is a warning message")
229
- logger.error("This is an error message")
230
- ```
231
-
232
- ### Example 5: Logging with a Block
233
- ```ruby
234
- require 'logger'
235
-
236
- # Calling any of the methods above with a block
237
- # (affects only the one entry).
238
- # Doing so can have two benefits:
239
- #
240
- # - Context: the block can evaluate the entire program context
241
- # and create a context-dependent message.
242
- # - Performance: the block is not evaluated unless the log level
243
- # permits the entry actually to be written:
244
- #
245
- # logger.error { my_slow_message_generator }
246
- #
247
- # Contrast this with the string form, where the string is
248
- # always evaluated, regardless of the log level:
249
- #
250
- # logger.error("#{my_slow_message_generator}")
251
- logger = Logger.new($stdout)
252
-
253
- logger.info { "This is an info message" }
254
- logger.warn { "This is a warning message" }
255
- logger.error { "This is an error message" }
256
- ```
@@ -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'