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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smells.yml +85 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +3 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +54 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +86 -4
- data/CLAUDE.md +39 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +624 -20
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +7 -7
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +51 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +15 -11
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +128 -14
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +12 -171
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +71 -66
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +57 -19
- data/lib/familia/features/object_identifier.rb +134 -25
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
- data/lib/familia/features/relationships/indexing.rb +182 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +10 -7
- data/lib/familia/features.rb +10 -14
- data/lib/familia/field_type.rb +6 -4
- data/lib/familia/horreum/connection.rb +297 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
- data/lib/familia/horreum/serialization.rb +172 -0
- data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
- data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
- data/lib/familia/horreum.rb +222 -119
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +2 -2
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +10 -10
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +11 -10
- data/try/core/errors_try.rb +11 -14
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/familia_try.rb +1 -1
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +3 -3
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/field_groups_try.rb +244 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +443 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +3 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +6 -7
- data/try/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +3 -1
- data/try/horreum/defensive_initialization_try.rb +86 -0
- data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/horreum/settings_try.rb +2 -0
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +2 -2
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +13 -15
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- data/try/valkey.conf +26 -0
- metadata +92 -52
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/horreum/core/connection.rb +0 -73
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
data/lib/familia/logging.rb
CHANGED
@@ -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 =
|
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
|
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
|
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,
|
145
|
+
def trace(label, instance_id = nil, ident = nil, extra_context = nil)
|
151
146
|
return unless Familia::Refinements::LoggerTrace::ENABLED
|
152
147
|
|
153
|
-
#
|
154
|
-
|
155
|
-
|
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
|
-
# #
|
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
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
101
|
-
|
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
|
-
#
|
113
|
-
def in_seconds = seconds
|
126
|
+
def in_seconds = seconds # for semantic purposes
|
114
127
|
|
115
128
|
# Time manipulation
|
116
|
-
def ago
|
117
|
-
def from_now
|
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
|
140
|
+
# @param unit [String, Symbol] Unit to convert to
|
128
141
|
# @return [Float] Converted time value
|
129
|
-
def in_seconds(
|
130
|
-
|
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 (
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
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 ||=
|
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
|
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
|
-
#
|
234
|
+
# Familia.now.older_than?(1.second) #=> false
|
234
235
|
def older_than?(duration)
|
235
|
-
self < (
|
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
|
-
#
|
242
|
+
# Familia.now.newer_than?(1.second) #=> false
|
242
243
|
def newer_than?(duration)
|
243
|
-
self > (
|
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 -
|
256
|
+
(self - Familia.now).abs <= duration
|
256
257
|
end
|
257
258
|
end
|
258
259
|
|
259
|
-
|
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
|
-
|
270
|
-
return nil unless
|
270
|
+
quantity, unit = scan(/([\d.]+)([a-zA-Zμs]+)?/).flatten
|
271
|
+
return nil unless quantity
|
271
272
|
|
272
|
-
|
273
|
-
|
274
|
-
|
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
|
data/lib/familia/refinements.rb
CHANGED