familia 2.0.0.pre12 → 2.0.0.pre14
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/.rubocop_todo.yml +2 -3
- data/CHANGELOG.rst +529 -0
- data/CLAUDE.md +1 -1
- data/Gemfile +1 -6
- data/Gemfile.lock +13 -7
- data/README.md +21 -2
- data/changelog.d/README.md +5 -5
- data/{setup.cfg → changelog.d/scriv.ini} +1 -1
- data/docs/guides/Feature-System-Autoloading.md +228 -0
- data/docs/guides/time-utilities.md +221 -0
- data/docs/migrating/v2.0.0-pre11.md +14 -16
- data/docs/migrating/v2.0.0-pre13.md +95 -0
- data/docs/migrating/v2.0.0-pre14.md +37 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +6 -0
- data/examples/autoloader/mega_customer.rb +17 -0
- data/examples/safe_dump.rb +1 -1
- data/familia.gemspec +1 -0
- data/lib/familia/autoloader.rb +53 -0
- data/lib/familia/base.rb +5 -0
- data/lib/familia/data_type.rb +4 -0
- data/lib/familia/encryption/encrypted_data.rb +4 -4
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +3 -0
- data/lib/familia/features/autoloadable.rb +113 -0
- data/lib/familia/features/encrypted_fields/concealed_string.rb +4 -2
- data/lib/familia/features/expiration.rb +4 -0
- data/lib/familia/features/external_identifier.rb +3 -3
- data/lib/familia/features/quantization.rb +5 -0
- data/lib/familia/features/safe_dump.rb +7 -0
- data/lib/familia/features.rb +20 -16
- data/lib/familia/field_type.rb +2 -0
- data/lib/familia/horreum/core/serialization.rb +3 -3
- data/lib/familia/horreum/subclass/definition.rb +3 -4
- data/lib/familia/horreum.rb +2 -0
- data/lib/familia/json_serializer.rb +70 -0
- data/lib/familia/logging.rb +12 -10
- data/lib/familia/refinements/logger_trace.rb +57 -0
- data/lib/familia/refinements/snake_case.rb +40 -0
- data/lib/familia/refinements/time_literals.rb +279 -0
- data/lib/familia/refinements.rb +3 -49
- data/lib/familia/utils.rb +2 -0
- data/lib/familia/validation/{test_helpers.rb → validation_helpers.rb} +2 -2
- data/lib/familia/validation.rb +1 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +15 -3
- data/try/core/autoloader_try.rb +112 -0
- data/try/core/extensions_try.rb +38 -21
- data/try/core/familia_extended_try.rb +4 -3
- data/try/core/time_utils_try.rb +130 -0
- data/try/data_types/datatype_base_try.rb +3 -2
- data/try/features/autoloadable/autoloadable_try.rb +61 -0
- data/try/features/encrypted_fields/concealed_string_core_try.rb +8 -3
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +59 -17
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +36 -12
- data/try/features/external_identifier/external_identifier_try.rb +26 -0
- data/try/features/feature_improvements_try.rb +2 -1
- data/try/features/real_feature_integration_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +111 -0
- data/try/helpers/test_helpers.rb +24 -0
- data/try/integration/cross_component_try.rb +3 -1
- metadata +34 -6
- data/CHANGELOG.md +0 -247
- data/lib/familia/core_ext.rb +0 -135
- data/lib/familia/features/autoloader.rb +0 -57
data/lib/familia/logging.rb
CHANGED
@@ -17,7 +17,7 @@ module Familia
|
|
17
17
|
|
18
18
|
# Get the severity letter from the thread local variable or use
|
19
19
|
# the default. The thread local variable is set in the trace
|
20
|
-
# method in the
|
20
|
+
# method in the Familia::Refinements::LoggerTrace module. The name of the
|
21
21
|
# variable `severity_letter` is arbitrary and could be anything.
|
22
22
|
severity_letter = Thread.current[:severity_letter] || severity_letter
|
23
23
|
|
@@ -36,7 +36,7 @@ module Familia
|
|
36
36
|
# == Methods:
|
37
37
|
# trace::
|
38
38
|
# Logs a message at the TRACE level. This method is only available if the
|
39
|
-
#
|
39
|
+
# Familia::Refinements::LoggerTrace is used.
|
40
40
|
#
|
41
41
|
# debug::
|
42
42
|
# Logs a message at the DEBUG level. This is used for low-level system information
|
@@ -59,14 +59,14 @@ module Familia
|
|
59
59
|
# that will presumably lead the application to abort.
|
60
60
|
#
|
61
61
|
# == Usage:
|
62
|
-
# To use the Logging module, you need to include the
|
62
|
+
# To use the Logging module, you need to include the Familia::Refinements::LoggerTrace module
|
63
63
|
# and use the `using` keyword to enable the refinement. This will add the TRACE
|
64
64
|
# log level and the trace method to the Logger class.
|
65
65
|
#
|
66
66
|
# Example:
|
67
67
|
# require 'logger'
|
68
68
|
#
|
69
|
-
# module
|
69
|
+
# module Familia::Refinements::LoggerTrace
|
70
70
|
# refine Logger do
|
71
71
|
# TRACE = 0
|
72
72
|
#
|
@@ -76,7 +76,7 @@ module Familia
|
|
76
76
|
# end
|
77
77
|
# end
|
78
78
|
#
|
79
|
-
# using
|
79
|
+
# using Familia::Refinements::LoggerTrace
|
80
80
|
#
|
81
81
|
# logger = Logger.new(STDOUT)
|
82
82
|
# logger.trace("This is a trace message")
|
@@ -86,13 +86,13 @@ module Familia
|
|
86
86
|
# logger.error("This is an error message")
|
87
87
|
# logger.fatal("This is a fatal message")
|
88
88
|
#
|
89
|
-
# In this example, the
|
89
|
+
# In this example, the Familia::Refinements::LoggerTrace module is defined with a refinement
|
90
90
|
# for the Logger class. The TRACE constant and trace method are added to the Logger
|
91
91
|
# class within the refinement. The `using` keyword is used to apply the refinement
|
92
92
|
# in the scope where it's needed.
|
93
93
|
#
|
94
94
|
# == Conditions:
|
95
|
-
# The trace method and TRACE log level are only available if the
|
95
|
+
# The trace method and TRACE log level are only available if the Familia::Refinements::LoggerTrace
|
96
96
|
# module is used with the `using` keyword. Without this, the Logger class will not
|
97
97
|
# have the trace method or the TRACE log level.
|
98
98
|
#
|
@@ -103,7 +103,9 @@ module Familia
|
|
103
103
|
attr_reader :logger
|
104
104
|
|
105
105
|
# Gives our logger the ability to use our trace method.
|
106
|
-
|
106
|
+
if Familia::Refinements::LoggerTrace::ENABLED
|
107
|
+
using Familia::Refinements::LoggerTrace
|
108
|
+
end
|
107
109
|
|
108
110
|
def info(*msg)
|
109
111
|
@logger.info(*msg)
|
@@ -140,13 +142,13 @@ module Familia
|
|
140
142
|
#
|
141
143
|
# @return [nil]
|
142
144
|
#
|
143
|
-
# @note This method only executes if
|
145
|
+
# @note This method only executes if Familia::Refinements::LoggerTrace::ENABLED is true.
|
144
146
|
# @note The dbclient can be a Database object, Redis::Future (used in
|
145
147
|
# pipelined and multi blocks), or nil (when the database connection isn't
|
146
148
|
# relevant).
|
147
149
|
#
|
148
150
|
def trace(label, dbclient, ident, context = nil)
|
149
|
-
return unless
|
151
|
+
return unless Familia::Refinements::LoggerTrace::ENABLED
|
150
152
|
|
151
153
|
# Usually dbclient is a Database object, but it could be
|
152
154
|
# a Redis::Future which is what is used inside of pipelined
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# lib/familia/refinements/logger_trace.rb
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
# Controls whether tracing is enabled via an environment variable
|
7
|
+
FAMILIA_TRACE = ENV.fetch('FAMILIA_TRACE', 'false').downcase
|
8
|
+
|
9
|
+
# Familia::Refinements::LoggerTrace
|
10
|
+
#
|
11
|
+
# This module adds a 'trace' log level to the Ruby Logger class.
|
12
|
+
# It is enabled when the FAMILIA_TRACE environment variable is set to
|
13
|
+
# '1', 'true', or 'yes' (case-insensitive).
|
14
|
+
#
|
15
|
+
# @example Enabling trace logging
|
16
|
+
# # Set environment variable
|
17
|
+
# ENV['FAMILIA_TRACE'] = 'true'
|
18
|
+
#
|
19
|
+
# # In your Ruby code
|
20
|
+
# require 'logger'
|
21
|
+
# using Familia::Refinements::LoggerTrace
|
22
|
+
#
|
23
|
+
# logger = Logger.new(STDOUT)
|
24
|
+
# logger.trace("This is a trace message")
|
25
|
+
#
|
26
|
+
module Familia
|
27
|
+
module Refinements
|
28
|
+
|
29
|
+
# Familia::Refinements::LoggerTrace
|
30
|
+
module LoggerTrace
|
31
|
+
unless defined?(ENABLED)
|
32
|
+
# Indicates whether trace logging is enabled
|
33
|
+
ENABLED = %w[1 true yes].include?(FAMILIA_TRACE).freeze
|
34
|
+
# The numeric level for trace logging (same as DEBUG)
|
35
|
+
TRACE = 0
|
36
|
+
end
|
37
|
+
|
38
|
+
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
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# lib/familia/refinements/snake_case.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Refinements
|
5
|
+
module SnakeCase
|
6
|
+
# We refine String rather than Class or Module because this method operates on
|
7
|
+
# string representations of class names (like those from `Class#name`) rather
|
8
|
+
# than the class objects themselves. Refining String is safer because it
|
9
|
+
# limits its scope to only the subset string manipulation contexts where it is
|
10
|
+
# used.
|
11
|
+
#
|
12
|
+
# Appropriate for converting Ruby class names to database table names, config
|
13
|
+
# keys, part of a path or any other snake_case identifiers. The only situation
|
14
|
+
# it is not appropriate for is investigating actual snakes.
|
15
|
+
refine String do
|
16
|
+
# Converts a string from PascalCase/camelCase to snake_case format.
|
17
|
+
#
|
18
|
+
# @return [String] the snake_case version of the string
|
19
|
+
#
|
20
|
+
# @example Converting simple CamelCase
|
21
|
+
# "FirstName".snake_case #=> "first_name"
|
22
|
+
#
|
23
|
+
# @example Converting PascalCase with acronyms
|
24
|
+
# XMLHttpRequest.name.snake_case #=> "xml_http_request"
|
25
|
+
#
|
26
|
+
# @example Converting namespaced class names
|
27
|
+
# "MyApp::UserAccount".snake_case #=> "user_account"
|
28
|
+
#
|
29
|
+
# @example Handling mixed case with numbers
|
30
|
+
# "parseHTML5Document".snake_case #=> "parse_html5_document"
|
31
|
+
def snake_case
|
32
|
+
split('::').last
|
33
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
34
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
35
|
+
.downcase
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,279 @@
|
|
1
|
+
# lib/familia/refinements/time_literals.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Refinements
|
5
|
+
|
6
|
+
# Familia::Refinements::TimeLiterals
|
7
|
+
#
|
8
|
+
# This module provides a set of refinements for `Numeric` and `String` to
|
9
|
+
# enable readable and expressive time duration and timestamp manipulation.
|
10
|
+
#
|
11
|
+
# The name "TimeLiterals" reflects its core purpose: to allow us to treat
|
12
|
+
# numeric values directly as "literals" of time units (e.g., `5.minutes`,
|
13
|
+
# `1.day`). It extends this concept to include conversions between these
|
14
|
+
# literal time quantities, parsing string representations of time
|
15
|
+
# durations, and performing common timestamp-based calculations
|
16
|
+
# in an intuitive manner.
|
17
|
+
#
|
18
|
+
# @example Expressing durations
|
19
|
+
# 5.minutes.ago #=> A Time object 5 minutes in the past
|
20
|
+
# 1.day.from_now #=> A Time object 1 day in the future
|
21
|
+
# (2.5).hours #=> 9000.0 (seconds)
|
22
|
+
#
|
23
|
+
# @example Converting between units
|
24
|
+
# 3600.in_hours #=> 1.0
|
25
|
+
# 86400.in_days #=> 1.0
|
26
|
+
#
|
27
|
+
# @example Parsing string durations
|
28
|
+
# "30m".in_seconds #=> 1800.0
|
29
|
+
# "2.5h".in_seconds #=> 9000.0
|
30
|
+
#
|
31
|
+
# @example Timestamp calculations
|
32
|
+
# timestamp = 2.days.ago.to_i
|
33
|
+
# timestamp.days_old #=> ~2.0
|
34
|
+
# timestamp.older_than?(1.day) #=> true
|
35
|
+
#
|
36
|
+
# @note `to_bytes` also lives here until we find it a better home!
|
37
|
+
#
|
38
|
+
module TimeLiterals
|
39
|
+
# Time unit constants
|
40
|
+
PER_MICROSECOND = 0.000001
|
41
|
+
PER_MILLISECOND = 0.001
|
42
|
+
PER_MINUTE = 60.0
|
43
|
+
PER_HOUR = 3600.0
|
44
|
+
PER_DAY = 86_400.0
|
45
|
+
PER_WEEK = 604_800.0
|
46
|
+
PER_YEAR = 31_556_952.0 # 365.2425 days (Gregorian year)
|
47
|
+
PER_MONTH = PER_YEAR / 12.0 # 30.437 days (consistent with Gregorian year)
|
48
|
+
|
49
|
+
UNIT_METHODS = {
|
50
|
+
'y' => :years,
|
51
|
+
'year' => :years,
|
52
|
+
'years' => :years,
|
53
|
+
'mo' => :months,
|
54
|
+
'month' => :months,
|
55
|
+
'months' => :months,
|
56
|
+
'w' => :weeks,
|
57
|
+
'week' => :weeks,
|
58
|
+
'weeks' => :weeks,
|
59
|
+
'd' => :days,
|
60
|
+
'day' => :days,
|
61
|
+
'days' => :days,
|
62
|
+
'h' => :hours,
|
63
|
+
'hour' => :hours,
|
64
|
+
'hours' => :hours,
|
65
|
+
'm' => :minutes,
|
66
|
+
'minute' => :minutes,
|
67
|
+
'minutes' => :minutes,
|
68
|
+
'ms' => :milliseconds,
|
69
|
+
'millisecond' => :milliseconds,
|
70
|
+
'milliseconds' => :milliseconds,
|
71
|
+
'us' => :microseconds,
|
72
|
+
'microsecond' => :microseconds,
|
73
|
+
'microseconds' => :microseconds,
|
74
|
+
'μs' => :microseconds,
|
75
|
+
}.freeze
|
76
|
+
|
77
|
+
refine Numeric do
|
78
|
+
def microseconds = seconds * PER_MICROSECOND
|
79
|
+
def milliseconds = seconds * PER_MILLISECOND
|
80
|
+
def seconds = self
|
81
|
+
def minutes = seconds * PER_MINUTE
|
82
|
+
def hours = seconds * PER_HOUR
|
83
|
+
def days = seconds * PER_DAY
|
84
|
+
def weeks = seconds * PER_WEEK
|
85
|
+
def months = seconds * PER_MONTH
|
86
|
+
def years = seconds * PER_YEAR
|
87
|
+
|
88
|
+
# 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
|
98
|
+
|
99
|
+
# Shortest aliases
|
100
|
+
alias_method :ms, :milliseconds
|
101
|
+
alias_method :μs, :microseconds
|
102
|
+
|
103
|
+
# Seconds -> other time units
|
104
|
+
def in_years = seconds / PER_YEAR
|
105
|
+
def in_months = seconds / PER_MONTH
|
106
|
+
def in_weeks = seconds / PER_WEEK
|
107
|
+
def in_days = seconds / PER_DAY
|
108
|
+
def in_hours = seconds / PER_HOUR
|
109
|
+
def in_minutes = seconds / PER_MINUTE
|
110
|
+
def in_milliseconds = seconds / PER_MILLISECOND
|
111
|
+
def in_microseconds = seconds / PER_MICROSECOND
|
112
|
+
# For semantic purposes
|
113
|
+
def in_seconds = seconds
|
114
|
+
|
115
|
+
# Time manipulation
|
116
|
+
def ago = Time.now.utc - seconds
|
117
|
+
def from_now = Time.now.utc + seconds
|
118
|
+
def before(time) = time - seconds
|
119
|
+
def after(time) = time + seconds
|
120
|
+
def in_time = Time.at(seconds).utc
|
121
|
+
|
122
|
+
# Milliseconds conversion
|
123
|
+
def to_ms = seconds * 1000.0
|
124
|
+
|
125
|
+
# Converts seconds to specified time unit
|
126
|
+
#
|
127
|
+
# @param u [String, Symbol] Unit to convert to
|
128
|
+
# @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
|
143
|
+
end
|
144
|
+
|
145
|
+
# Converts the number to a human-readable string representation
|
146
|
+
#
|
147
|
+
# @return [String] A formatted string e.g. "1 day" or "10 seconds"
|
148
|
+
#
|
149
|
+
# @example
|
150
|
+
# 10.to_humanize #=> "10 seconds"
|
151
|
+
# 60.to_humanize #=> "1 minute"
|
152
|
+
# 3600.to_humanize #=> "1 hour"
|
153
|
+
# 86400.to_humanize #=> "1 day"
|
154
|
+
def humanize
|
155
|
+
gte_zero = positive? || zero?
|
156
|
+
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
|
163
|
+
gte_zero ? text : "#{text} ago"
|
164
|
+
end
|
165
|
+
|
166
|
+
# Converts the number to a human-readable byte representation using binary units
|
167
|
+
#
|
168
|
+
# @return [String] A formatted string of bytes, KiB, MiB, GiB, or TiB
|
169
|
+
#
|
170
|
+
# @example
|
171
|
+
# 1024.to_bytes #=> "1.00 KiB"
|
172
|
+
# 2_097_152.to_bytes #=> "2.00 MiB"
|
173
|
+
# 3_221_225_472.to_bytes #=> "3.00 GiB"
|
174
|
+
#
|
175
|
+
def to_bytes
|
176
|
+
units = %w[B KiB MiB GiB TiB]
|
177
|
+
size = abs.to_f
|
178
|
+
unit = 0
|
179
|
+
|
180
|
+
while size >= 1024 && unit < units.length - 1
|
181
|
+
size /= 1024
|
182
|
+
unit += 1
|
183
|
+
end
|
184
|
+
|
185
|
+
format('%3.2f %s', size, units[unit])
|
186
|
+
end
|
187
|
+
|
188
|
+
# Calculates age of timestamp in specified unit from reference time
|
189
|
+
#
|
190
|
+
# @param unit [String, Symbol] Time unit ('days', 'hours', 'minutes', 'weeks')
|
191
|
+
# @param from_time [Time, nil] Reference time (defaults to Time.now.utc)
|
192
|
+
# @return [Float] Age in specified unit
|
193
|
+
# @example
|
194
|
+
# timestamp = 2.days.ago.to_i
|
195
|
+
# timestamp.age_in(:days) #=> ~2.0
|
196
|
+
# timestamp.age_in('hours') #=> ~48.0
|
197
|
+
# timestamp.age_in(:days, 1.day.ago) #=> ~1.0
|
198
|
+
def age_in(unit, from_time = nil)
|
199
|
+
from_time ||= Time.now.utc
|
200
|
+
age_seconds = from_time.to_f - to_f
|
201
|
+
case UNIT_METHODS.fetch(unit.to_s.downcase, nil)
|
202
|
+
when :days then age_seconds / PER_DAY
|
203
|
+
when :hours then age_seconds / PER_HOUR
|
204
|
+
when :minutes then age_seconds / PER_MINUTE
|
205
|
+
when :weeks then age_seconds / PER_WEEK
|
206
|
+
when :months then age_seconds / PER_MONTH
|
207
|
+
when :years then age_seconds / PER_YEAR
|
208
|
+
else age_seconds
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Convenience methods for `age_in(unit)` calls.
|
213
|
+
#
|
214
|
+
# @param from_time [Time, nil] Reference time (defaults to Time.now.utc)
|
215
|
+
# @return [Float] Age in days
|
216
|
+
# @example
|
217
|
+
# timestamp.days_old #=> 2.5
|
218
|
+
def days_old(*) = age_in(:days, *)
|
219
|
+
def hours_old(*) = age_in(:hours, *)
|
220
|
+
def minutes_old(*) = age_in(:minutes, *)
|
221
|
+
def weeks_old(*) = age_in(:weeks, *)
|
222
|
+
def months_old(*) = age_in(:months, *)
|
223
|
+
def years_old(*) = age_in(:years, *)
|
224
|
+
|
225
|
+
# Checks if timestamp is older than specified duration in seconds
|
226
|
+
#
|
227
|
+
# @param duration [Numeric] Duration in seconds to compare against
|
228
|
+
# @return [Boolean] true if timestamp is older than duration
|
229
|
+
# @note Both older_than? and newer_than? can return false when timestamp
|
230
|
+
# is within the same second. Use within? to check this case.
|
231
|
+
#
|
232
|
+
# @example
|
233
|
+
# Time.now.older_than?(1.second) #=> false
|
234
|
+
def older_than?(duration)
|
235
|
+
self < (Time.now.utc.to_f - duration)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Checks if timestamp is newer than specified duration in the future
|
239
|
+
#
|
240
|
+
# @example
|
241
|
+
# Time.now.newer_than?(1.second) #=> false
|
242
|
+
def newer_than?(duration)
|
243
|
+
self > (Time.now.utc.to_f + duration)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Checks if timestamp is within specified duration of now (past or future)
|
247
|
+
#
|
248
|
+
# @param duration [Numeric] Duration in seconds to compare against
|
249
|
+
# @return [Boolean] true if timestamp is within duration of now
|
250
|
+
# @example
|
251
|
+
# 30.minutes.ago.to_i.within?(1.hour) #=> true
|
252
|
+
# 30.minutes.from_now.to_i.within?(1.hour) #=> true
|
253
|
+
# 2.hours.ago.to_i.within?(1.hour) #=> false
|
254
|
+
def within?(duration)
|
255
|
+
(self - Time.now.utc.to_f).abs <= duration
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
refine ::String do
|
260
|
+
# Converts string time representation to seconds
|
261
|
+
#
|
262
|
+
# @example
|
263
|
+
# "60m".in_seconds #=> 3600.0
|
264
|
+
# "2.5h".in_seconds #=> 9000.0
|
265
|
+
# "1y".in_seconds #=> 31536000.0
|
266
|
+
#
|
267
|
+
# @return [Float, nil] Time in seconds or nil if invalid
|
268
|
+
def in_seconds
|
269
|
+
q, u = scan(/([\d.]+)([a-zA-Zμs]+)?/).flatten
|
270
|
+
return nil unless q
|
271
|
+
|
272
|
+
q = q.to_f
|
273
|
+
u ||= 's'
|
274
|
+
q.in_seconds(u)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
data/lib/familia/refinements.rb
CHANGED
@@ -1,51 +1,5 @@
|
|
1
1
|
# lib/familia/refinements.rb
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
# Controls whether tracing is enabled via an environment variable
|
7
|
-
FAMILIA_TRACE = ENV.fetch('FAMILIA_TRACE', 'false').downcase
|
8
|
-
|
9
|
-
# LoggerTraceRefinement
|
10
|
-
#
|
11
|
-
# This module adds a 'trace' log level to the Ruby Logger class.
|
12
|
-
# It is enabled when the FAMILIA_TRACE environment variable is set to
|
13
|
-
# '1', 'true', or 'yes' (case-insensitive).
|
14
|
-
#
|
15
|
-
# @example Enabling trace logging
|
16
|
-
# # Set environment variable
|
17
|
-
# ENV['FAMILIA_TRACE'] = 'true'
|
18
|
-
#
|
19
|
-
# # In your Ruby code
|
20
|
-
# require 'logger'
|
21
|
-
# using LoggerTraceRefinement
|
22
|
-
#
|
23
|
-
# logger = Logger.new(STDOUT)
|
24
|
-
# logger.trace("This is a trace message")
|
25
|
-
#
|
26
|
-
module LoggerTraceRefinement
|
27
|
-
unless defined?(ENABLED)
|
28
|
-
# Indicates whether trace logging is enabled
|
29
|
-
ENABLED = %w[1 true yes].include?(FAMILIA_TRACE).freeze
|
30
|
-
# The numeric level for trace logging (same as DEBUG)
|
31
|
-
TRACE = 0
|
32
|
-
end
|
33
|
-
|
34
|
-
refine Logger do
|
35
|
-
##
|
36
|
-
# Logs a message at the TRACE level.
|
37
|
-
#
|
38
|
-
# @param progname [String] The program name to include in the log message
|
39
|
-
# @yield A block that evaluates to the message to log
|
40
|
-
# @return [true] Always returns true
|
41
|
-
#
|
42
|
-
# @example Logging a trace message
|
43
|
-
# logger.trace("MyApp") { "Detailed trace information" }
|
44
|
-
def trace(progname = nil, &block)
|
45
|
-
Thread.current[:severity_letter] = 'T'
|
46
|
-
add(LoggerTraceRefinement::TRACE, nil, progname, &block)
|
47
|
-
ensure
|
48
|
-
Thread.current[:severity_letter] = nil
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
3
|
+
require_relative 'refinements/logger_trace'
|
4
|
+
require_relative 'refinements/snake_case'
|
5
|
+
require_relative 'refinements/time_literals'
|
data/lib/familia/utils.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# lib/familia/validation/
|
1
|
+
# lib/familia/validation/validation_helpers.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
module Validation
|
@@ -7,7 +7,7 @@ module Familia
|
|
7
7
|
# and automatic setup/cleanup for command validation tests.
|
8
8
|
#
|
9
9
|
# @example Basic usage in a try file
|
10
|
-
# require_relative '../validation/
|
10
|
+
# require_relative '../validation/validation_helpers'
|
11
11
|
# extend Familia::Validation::TestHelpers
|
12
12
|
#
|
13
13
|
# ## User save should execute expected Redis commands
|
data/lib/familia/validation.rb
CHANGED
@@ -51,7 +51,7 @@
|
|
51
51
|
require_relative 'validation/command_recorder'
|
52
52
|
require_relative 'validation/expectations'
|
53
53
|
require_relative 'validation/validator'
|
54
|
-
require_relative 'validation/
|
54
|
+
require_relative 'validation/validation_helpers'
|
55
55
|
|
56
56
|
module Familia
|
57
57
|
module Validation
|
data/lib/familia/version.rb
CHANGED
data/lib/familia.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# lib/familia.rb
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'oj'
|
4
4
|
require 'redis'
|
5
5
|
require 'uri/valkey'
|
6
6
|
require 'connection_pool'
|
7
7
|
|
8
|
-
|
8
|
+
# OJ configuration is handled internally by Familia::JsonSerializer
|
9
|
+
|
9
10
|
require_relative 'familia/refinements'
|
10
11
|
require_relative 'familia/errors'
|
11
12
|
require_relative 'familia/version'
|
@@ -71,6 +72,7 @@ module Familia
|
|
71
72
|
require_relative 'familia/connection'
|
72
73
|
require_relative 'familia/settings'
|
73
74
|
require_relative 'familia/utils'
|
75
|
+
require_relative 'familia/json_serializer'
|
74
76
|
|
75
77
|
extend SecureIdentifier
|
76
78
|
extend Connection
|
@@ -80,8 +82,18 @@ module Familia
|
|
80
82
|
end
|
81
83
|
|
82
84
|
require_relative 'familia/base'
|
85
|
+
require_relative 'familia/features/autoloadable'
|
83
86
|
require_relative 'familia/features'
|
84
|
-
require_relative 'familia/features/autoloader'
|
85
87
|
require_relative 'familia/data_type'
|
86
88
|
require_relative 'familia/horreum'
|
87
89
|
require_relative 'familia/encryption'
|
90
|
+
|
91
|
+
# Ensure JSON constant is available for backward compatibility with existing code
|
92
|
+
# This approach is safer than monkey-patching core classes globally
|
93
|
+
begin
|
94
|
+
require 'json'
|
95
|
+
rescue LoadError
|
96
|
+
# If json gem is not available, define a minimal JSON constant
|
97
|
+
# that delegates to Familia::JsonSerializer for compatibility
|
98
|
+
JSON = Familia::JsonSerializer
|
99
|
+
end
|