familia 2.0.0.pre10 → 2.0.0.pre13

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +2 -3
  3. data/CHANGELOG.rst +507 -0
  4. data/CLAUDE.md +5 -55
  5. data/Gemfile +1 -6
  6. data/Gemfile.lock +13 -7
  7. data/changelog.d/README.md +45 -34
  8. data/changelog.d/scriv.ini +5 -0
  9. data/docs/archive/FAMILIA_RELATIONSHIPS.md +1 -1
  10. data/docs/archive/FAMILIA_UPDATE.md +1 -1
  11. data/docs/archive/README.md +15 -19
  12. data/docs/guides/Feature-System-Autoloading.md +228 -0
  13. data/docs/guides/Home.md +1 -1
  14. data/docs/guides/Implementation-Guide.md +1 -1
  15. data/docs/guides/relationships-methods.md +1 -1
  16. data/docs/guides/time-utilities.md +221 -0
  17. data/docs/migrating/.gitignore +2 -0
  18. data/docs/migrating/v2.0.0-pre.md +84 -0
  19. data/docs/migrating/v2.0.0-pre11.md +253 -0
  20. data/docs/migrating/v2.0.0-pre12.md +306 -0
  21. data/docs/migrating/v2.0.0-pre13.md +329 -0
  22. data/docs/migrating/v2.0.0-pre5.md +110 -0
  23. data/docs/migrating/v2.0.0-pre6.md +154 -0
  24. data/docs/migrating/v2.0.0-pre7.md +222 -0
  25. data/docs/overview.md +6 -7
  26. data/{examples/redis_command_validation_example.rb → docs/reference/auditing_database_commands.rb} +29 -32
  27. data/examples/autoloader/mega_customer/safe_dump_fields.rb +6 -0
  28. data/examples/autoloader/mega_customer.rb +17 -0
  29. data/examples/{bit_encoding_integration.rb → permissions.rb} +30 -27
  30. data/examples/{relationships_basic.rb → relationships.rb} +2 -3
  31. data/examples/safe_dump.rb +281 -0
  32. data/familia.gemspec +5 -4
  33. data/lib/familia/autoloader.rb +53 -0
  34. data/lib/familia/base.rb +57 -0
  35. data/lib/familia/data_type.rb +4 -0
  36. data/lib/familia/encryption/encrypted_data.rb +4 -4
  37. data/lib/familia/encryption/manager.rb +6 -4
  38. data/lib/familia/{encryption_request_cache.rb → encryption/request_cache.rb} +1 -1
  39. data/lib/familia/encryption.rb +1 -1
  40. data/lib/familia/errors.rb +5 -0
  41. data/lib/familia/features/autoloadable.rb +113 -0
  42. data/lib/familia/features/encrypted_fields/concealed_string.rb +4 -2
  43. data/lib/familia/features/expiration.rb +4 -0
  44. data/lib/familia/features/external_identifier.rb +310 -0
  45. data/lib/familia/features/object_identifier.rb +307 -0
  46. data/lib/familia/features/quantization.rb +5 -0
  47. data/lib/familia/features/safe_dump.rb +74 -73
  48. data/lib/familia/features.rb +109 -17
  49. data/lib/familia/field_type.rb +2 -0
  50. data/lib/familia/horreum/core/serialization.rb +3 -3
  51. data/lib/familia/horreum/subclass/definition.rb +50 -7
  52. data/lib/familia/horreum.rb +2 -0
  53. data/lib/familia/json_serializer.rb +70 -0
  54. data/lib/familia/logging.rb +12 -10
  55. data/lib/familia/refinements/logger_trace.rb +57 -0
  56. data/lib/familia/refinements/snake_case.rb +40 -0
  57. data/lib/familia/refinements/time_utils.rb +248 -0
  58. data/lib/familia/refinements.rb +3 -49
  59. data/lib/familia/secure_identifier.rb +51 -75
  60. data/lib/familia/utils.rb +2 -0
  61. data/lib/familia/validation/{test_helpers.rb → validation_helpers.rb} +2 -2
  62. data/lib/familia/validation.rb +1 -1
  63. data/lib/familia/verifiable_identifier.rb +162 -0
  64. data/lib/familia/version.rb +1 -1
  65. data/lib/familia.rb +15 -2
  66. data/try/core/autoloader_try.rb +112 -0
  67. data/try/core/extensions_try.rb +38 -21
  68. data/try/core/familia_extended_try.rb +4 -3
  69. data/try/core/secure_identifier_try.rb +47 -18
  70. data/try/core/time_utils_try.rb +130 -0
  71. data/try/core/verifiable_identifier_try.rb +171 -0
  72. data/try/data_types/datatype_base_try.rb +3 -2
  73. data/try/features/autoloadable/autoloadable_try.rb +61 -0
  74. data/try/features/encrypted_fields/concealed_string_core_try.rb +8 -3
  75. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +59 -17
  76. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +36 -12
  77. data/try/features/{external_identifiers/external_identifiers_try.rb → external_identifier/external_identifier_try.rb} +25 -28
  78. data/try/features/feature_improvements_try.rb +127 -0
  79. data/try/features/{object_identifiers/object_identifiers_integration_try.rb → object_identifier/object_identifier_integration_try.rb} +28 -30
  80. data/try/features/{object_identifiers/object_identifiers_try.rb → object_identifier/object_identifier_try.rb} +13 -13
  81. data/try/features/real_feature_integration_try.rb +8 -7
  82. data/try/features/safe_dump/safe_dump_autoloading_try.rb +111 -0
  83. data/try/features/safe_dump/safe_dump_try.rb +8 -9
  84. data/try/helpers/test_helpers.rb +41 -17
  85. data/try/integration/cross_component_try.rb +3 -1
  86. metadata +61 -26
  87. data/CHANGELOG.md +0 -184
  88. data/changelog.d/fragments/.keep +0 -0
  89. data/changelog.d/template.md.j2 +0 -29
  90. data/lib/familia/core_ext.rb +0 -135
  91. data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +0 -120
  92. data/lib/familia/features/external_identifiers.rb +0 -111
  93. data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +0 -91
  94. data/lib/familia/features/object_identifiers.rb +0 -194
  95. data/setup.cfg +0 -12
@@ -470,7 +470,7 @@ module Familia
470
470
  # If the distinguisher returns nil, try using the dump_method but only
471
471
  # use JSON serialization for complex types that need it.
472
472
  if prepared.nil? && (val.is_a?(Hash) || val.is_a?(Array))
473
- prepared = val.respond_to?(dump_method) ? val.send(dump_method) : JSON.dump(val)
473
+ prepared = val.respond_to?(dump_method) ? val.send(dump_method) : Familia::JsonSerializer.dump(val)
474
474
  end
475
475
 
476
476
  # If both the distinguisher and dump_method return nil, log an error
@@ -493,11 +493,11 @@ module Familia
493
493
 
494
494
  # Try to parse as JSON first for complex types
495
495
  begin
496
- parsed = JSON.parse(val, symbolize_names: symbolize)
496
+ parsed = Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
497
497
  # Only return parsed value if it's a complex type (Hash/Array)
498
498
  # Simple values should remain as strings
499
499
  return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
500
- rescue JSON::ParserError
500
+ rescue Familia::SerializerError
501
501
  # Not valid JSON, return as-is
502
502
  end
503
503
 
@@ -46,6 +46,8 @@ module Familia
46
46
  include Familia::Settings
47
47
  include Familia::Horreum::RelatedFieldsManagement # Provides DataType field methods
48
48
 
49
+ using Familia::Refinements::SnakeCase
50
+
49
51
  # Sets or retrieves the unique identifier field for the class.
50
52
  #
51
53
  # This method defines or returns the field or method that contains the unique
@@ -183,10 +185,7 @@ module Familia
183
185
  #
184
186
  # @return [String] The underscored class name as a string
185
187
  def config_name
186
- name.split('::').last
187
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
188
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
189
- .downcase
188
+ name.snake_case
190
189
  end
191
190
 
192
191
  def dump_method
@@ -237,10 +236,36 @@ module Familia
237
236
  field_types[field_type.name] = field_type
238
237
  end
239
238
 
240
- # Get feature options for a specific feature or all features
239
+ # Retrieves feature options for the current class.
240
+ #
241
+ # Feature options are stored **per-class** in instance variables, ensuring
242
+ # complete isolation between different Familia::Horreum subclasses. Each
243
+ # class maintains its own @feature_options hash that does not interfere
244
+ # with other classes' configurations.
245
+ #
246
+ # @param feature_name [Symbol, String, nil] the name of the feature to get options for.
247
+ # If nil, returns the entire feature options hash for this class.
248
+ # @return [Hash] the feature options hash, either for a specific feature or all features
249
+ #
250
+ # @example Getting options for a specific feature
251
+ # class MyModel < Familia::Horreum
252
+ # feature :object_identifier, generator: :uuid_v4
253
+ # end
254
+ #
255
+ # MyModel.feature_options(:object_identifier) #=> {generator: :uuid_v4}
256
+ # MyModel.feature_options #=> {object_identifier: {generator: :uuid_v4}}
257
+ #
258
+ # @example Per-class isolation
259
+ # class UserModel < Familia::Horreum
260
+ # feature :object_identifier, generator: :uuid_v4
261
+ # end
241
262
  #
242
- # @param feature_name [Symbol, nil] The feature name to get options for
243
- # @return [Hash] The options hash for the feature, or empty hash if none
263
+ # class SessionModel < Familia::Horreum
264
+ # feature :object_identifier, generator: :hex
265
+ # end
266
+ #
267
+ # UserModel.feature_options(:object_identifier) #=> {generator: :uuid_v4}
268
+ # SessionModel.feature_options(:object_identifier) #=> {generator: :hex}
244
269
  #
245
270
  def feature_options(feature_name = nil)
246
271
  @feature_options ||= {}
@@ -255,10 +280,28 @@ module Familia
255
280
  # without worrying about initialization state. Similar to register_field_type
256
281
  # for field types.
257
282
  #
283
+ # Feature options are stored at the **class level** using instance variables,
284
+ # ensuring complete isolation between different Familia::Horreum subclasses.
285
+ # Each class maintains its own @feature_options hash.
286
+ #
258
287
  # @param feature_name [Symbol] The feature name
259
288
  # @param options [Hash] The options to add/merge
260
289
  # @return [Hash] The updated options for the feature
261
290
  #
291
+ # @note This method only sets defaults for options that don't already exist,
292
+ # using the ||= operator to prevent overwrites.
293
+ #
294
+ # @example Per-class storage behavior
295
+ # class ModelA < Familia::Horreum
296
+ # # This stores options in ModelA's @feature_options
297
+ # add_feature_options(:my_feature, key: 'value_a')
298
+ # end
299
+ #
300
+ # class ModelB < Familia::Horreum
301
+ # # This stores options in ModelB's @feature_options (separate from ModelA)
302
+ # add_feature_options(:my_feature, key: 'value_b')
303
+ # end
304
+ #
262
305
  def add_feature_options(feature_name, **options)
263
306
  @feature_options ||= {}
264
307
  @feature_options[feature_name.to_sym] ||= {}
@@ -31,6 +31,8 @@ module Familia
31
31
  include Familia::Horreum::Core
32
32
  include Familia::Horreum::Settings
33
33
 
34
+ using Familia::Refinements::TimeUtils
35
+
34
36
  # Singleton Class Context
35
37
  #
36
38
  # The code within this block operates on the singleton class (also known as
@@ -0,0 +1,70 @@
1
+ # lib/familia/json_serializer.rb
2
+
3
+ module Familia
4
+ # JsonSerializer provides a high-performance JSON interface using OJ
5
+ #
6
+ # This module wraps OJ with a clean API that can be easily swapped out
7
+ # or benchmarked against other JSON implementations. Uses OJ's :strict
8
+ # mode for RFC 7159 compliant JSON output.
9
+ #
10
+ # @example Basic usage
11
+ # data = { name: 'test', value: 123 }
12
+ # json = Familia::JsonSerializer.dump(data)
13
+ # parsed = Familia::JsonSerializer.parse(json, symbolize_names: true)
14
+ #
15
+ module JsonSerializer
16
+
17
+ class << self
18
+ # Parse JSON string into Ruby objects
19
+ #
20
+ # @param source [String] JSON string to parse
21
+ # @param opts [Hash] parsing options
22
+ # @option opts [Boolean] :symbolize_names convert hash keys to symbols
23
+ # @return [Object] parsed Ruby object
24
+ # @raise [SerializerError] if JSON is malformed
25
+ def parse(source, opts = {})
26
+ return nil if source.nil? || source == ''
27
+
28
+ symbolize_names = opts[:symbolize_names] || opts['symbolize_names']
29
+
30
+ if symbolize_names
31
+ Oj.load(source, mode: :strict, symbol_keys: true)
32
+ else
33
+ Oj.load(source, mode: :strict)
34
+ end
35
+ rescue Oj::ParseError, Oj::Error, EncodingError => e
36
+ raise SerializerError, e.message
37
+ end
38
+
39
+ # Serialize Ruby object to JSON string
40
+ #
41
+ # @param obj [Object] Ruby object to serialize
42
+ # @return [String] JSON string
43
+ def dump(obj)
44
+ Oj.dump(obj, mode: :strict)
45
+ rescue Oj::Error, TypeError, EncodingError => e
46
+ raise SerializerError, e.message
47
+ end
48
+
49
+ # Alias for dump for JSON gem compatibility
50
+ #
51
+ # @param obj [Object] Ruby object to serialize
52
+ # @return [String] JSON string
53
+ def generate(obj)
54
+ Oj.dump(obj, mode: :strict)
55
+ rescue Oj::Error, TypeError, EncodingError => e
56
+ raise SerializerError, e.message
57
+ end
58
+
59
+ # Serialize Ruby object to pretty-formatted JSON string
60
+ #
61
+ # @param obj [Object] Ruby object to serialize
62
+ # @return [String] pretty-formatted JSON string
63
+ def pretty_generate(obj)
64
+ Oj.dump(obj, mode: :strict, indent: 2)
65
+ rescue Oj::Error, TypeError, EncodingError => e
66
+ raise SerializerError, e.message
67
+ end
68
+ end
69
+ end
70
+ end
@@ -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 LoggerTraceRefinement module. The name of 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
- # LoggerTraceRefinement is used.
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 LoggerTraceRefinement module
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 LoggerTraceRefinement
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 LoggerTraceRefinement
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 LoggerTraceRefinement module is defined with a refinement
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 LoggerTraceRefinement
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
- using LoggerTraceRefinement if LoggerTraceRefinement::ENABLED
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 LoggerTraceRefinement::ENABLED is true.
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 LoggerTraceRefinement::ENABLED
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,248 @@
1
+ # lib/familia/refinements/time_utils.rb
2
+
3
+ module Familia
4
+ module Refinements
5
+
6
+ # Familia::Refinements::TimeUtils
7
+ module TimeUtils
8
+ # Time unit constants
9
+ PER_MICROSECOND = 0.000001
10
+ PER_MILLISECOND = 0.001
11
+ PER_MINUTE = 60.0
12
+ PER_HOUR = 3600.0
13
+ PER_DAY = 86_400.0
14
+ PER_WEEK = 604_800.0
15
+ PER_YEAR = 31_556_952.0 # 365.2425 days (Gregorian year)
16
+ PER_MONTH = PER_YEAR / 12.0 # 30.437 days (consistent with Gregorian year)
17
+
18
+ UNIT_METHODS = {
19
+ 'y' => :years,
20
+ 'year' => :years,
21
+ 'years' => :years,
22
+ 'mo' => :months,
23
+ 'month' => :months,
24
+ 'months' => :months,
25
+ 'w' => :weeks,
26
+ 'week' => :weeks,
27
+ 'weeks' => :weeks,
28
+ 'd' => :days,
29
+ 'day' => :days,
30
+ 'days' => :days,
31
+ 'h' => :hours,
32
+ 'hour' => :hours,
33
+ 'hours' => :hours,
34
+ 'm' => :minutes,
35
+ 'minute' => :minutes,
36
+ 'minutes' => :minutes,
37
+ 'ms' => :milliseconds,
38
+ 'millisecond' => :milliseconds,
39
+ 'milliseconds' => :milliseconds,
40
+ 'us' => :microseconds,
41
+ 'microsecond' => :microseconds,
42
+ 'microseconds' => :microseconds,
43
+ 'μs' => :microseconds,
44
+ }.freeze
45
+
46
+ refine Numeric do
47
+ def microseconds = seconds * PER_MICROSECOND
48
+ def milliseconds = seconds * PER_MILLISECOND
49
+ def seconds = self
50
+ def minutes = seconds * PER_MINUTE
51
+ def hours = seconds * PER_HOUR
52
+ def days = seconds * PER_DAY
53
+ def weeks = seconds * PER_WEEK
54
+ def months = seconds * PER_MONTH
55
+ def years = seconds * PER_YEAR
56
+
57
+ # Aliases with singular forms
58
+ alias_method :microsecond, :microseconds
59
+ alias_method :millisecond, :milliseconds
60
+ alias_method :second, :seconds
61
+ alias_method :minute, :minutes
62
+ alias_method :hour, :hours
63
+ alias_method :day, :days
64
+ alias_method :week, :weeks
65
+ alias_method :month, :months
66
+ alias_method :year, :years
67
+
68
+ # Fun aliases
69
+ alias_method :ms, :milliseconds
70
+ alias_method :μs, :microseconds
71
+
72
+ # Seconds -> other time units
73
+ def in_years = seconds / PER_YEAR
74
+ def in_months = seconds / PER_MONTH
75
+ def in_weeks = seconds / PER_WEEK
76
+ def in_days = seconds / PER_DAY
77
+ def in_hours = seconds / PER_HOUR
78
+ def in_minutes = seconds / PER_MINUTE
79
+ def in_milliseconds = seconds / PER_MILLISECOND
80
+ def in_microseconds = seconds / PER_MICROSECOND
81
+ # For semantic purposes
82
+ def in_seconds = seconds
83
+
84
+ # Time manipulation
85
+ def ago = Time.now.utc - seconds
86
+ def from_now = Time.now.utc + seconds
87
+ def before(time) = time - seconds
88
+ def after(time) = time + seconds
89
+ def in_time = Time.at(seconds).utc
90
+
91
+ # Milliseconds conversion
92
+ def to_ms = seconds * 1000.0
93
+
94
+ # Converts seconds to specified time unit
95
+ #
96
+ # @param u [String, Symbol] Unit to convert to
97
+ # @return [Float] Converted time value
98
+ def in_seconds(u = nil)
99
+ return self unless u
100
+
101
+ case UNIT_METHODS.fetch(u.to_s.downcase, nil)
102
+ when :milliseconds then self * PER_MILLISECOND
103
+ when :microseconds then self * PER_MICROSECOND
104
+ when :minutes then self * PER_MINUTE
105
+ when :hours then self * PER_HOUR
106
+ when :days then self * PER_DAY
107
+ when :weeks then self * PER_WEEK
108
+ when :months then self * PER_MONTH
109
+ when :years then self * PER_YEAR
110
+ else self
111
+ end
112
+ end
113
+
114
+ # Converts the number to a human-readable string representation
115
+ #
116
+ # @return [String] A formatted string e.g. "1 day" or "10 seconds"
117
+ #
118
+ # @example
119
+ # 10.to_humanize #=> "10 seconds"
120
+ # 60.to_humanize #=> "1 minute"
121
+ # 3600.to_humanize #=> "1 hour"
122
+ # 86400.to_humanize #=> "1 day"
123
+ def humanize
124
+ gte_zero = positive? || zero?
125
+ duration = (gte_zero ? self : abs) # let's keep it positive up in here
126
+ text = case (s = duration.to_i)
127
+ in 0..59 then "#{s} second#{'s' if s != 1}"
128
+ in 60..3599 then "#{s /= 60} minute#{'s' if s != 1}"
129
+ in 3600..86_399 then "#{s /= 3600} hour#{'s' if s != 1}"
130
+ else "#{s /= 86_400} day#{'s' if s != 1}"
131
+ end
132
+ gte_zero ? text : "#{text} ago"
133
+ end
134
+
135
+ # Converts the number to a human-readable byte representation using binary units
136
+ #
137
+ # @return [String] A formatted string of bytes, KiB, MiB, GiB, or TiB
138
+ #
139
+ # @example
140
+ # 1024.to_bytes #=> "1.00 KiB"
141
+ # 2_097_152.to_bytes #=> "2.00 MiB"
142
+ # 3_221_225_472.to_bytes #=> "3.00 GiB"
143
+ #
144
+ def to_bytes
145
+ units = %w[B KiB MiB GiB TiB]
146
+ size = abs.to_f
147
+ unit = 0
148
+
149
+ while size >= 1024 && unit < units.length - 1
150
+ size /= 1024
151
+ unit += 1
152
+ end
153
+
154
+ format('%3.2f %s', size, units[unit])
155
+ end
156
+
157
+ # Calculates age of timestamp in specified unit from reference time
158
+ #
159
+ # @param unit [String, Symbol] Time unit ('days', 'hours', 'minutes', 'weeks')
160
+ # @param from_time [Time, nil] Reference time (defaults to Time.now.utc)
161
+ # @return [Float] Age in specified unit
162
+ # @example
163
+ # timestamp = 2.days.ago.to_i
164
+ # timestamp.age_in(:days) #=> ~2.0
165
+ # timestamp.age_in('hours') #=> ~48.0
166
+ # timestamp.age_in(:days, 1.day.ago) #=> ~1.0
167
+ def age_in(unit, from_time = nil)
168
+ from_time ||= Time.now.utc
169
+ age_seconds = from_time.to_f - to_f
170
+ case UNIT_METHODS.fetch(unit.to_s.downcase, nil)
171
+ when :days then age_seconds / PER_DAY
172
+ when :hours then age_seconds / PER_HOUR
173
+ when :minutes then age_seconds / PER_MINUTE
174
+ when :weeks then age_seconds / PER_WEEK
175
+ when :months then age_seconds / PER_MONTH
176
+ when :years then age_seconds / PER_YEAR
177
+ else age_seconds
178
+ end
179
+ end
180
+
181
+ # Convenience methods for `age_in(unit)` calls.
182
+ #
183
+ # @param from_time [Time, nil] Reference time (defaults to Time.now.utc)
184
+ # @return [Float] Age in days
185
+ # @example
186
+ # timestamp.days_old #=> 2.5
187
+ def days_old(*) = age_in(:days, *)
188
+ def hours_old(*) = age_in(:hours, *)
189
+ def minutes_old(*) = age_in(:minutes, *)
190
+ def weeks_old(*) = age_in(:weeks, *)
191
+ def months_old(*) = age_in(:months, *)
192
+ def years_old(*) = age_in(:years, *)
193
+
194
+ # Checks if timestamp is older than specified duration in seconds
195
+ #
196
+ # @param duration [Numeric] Duration in seconds to compare against
197
+ # @return [Boolean] true if timestamp is older than duration
198
+ # @note Both older_than? and newer_than? can return false when timestamp
199
+ # is within the same second. Use within? to check this case.
200
+ #
201
+ # @example
202
+ # Time.now.older_than?(1.second) #=> false
203
+ def older_than?(duration)
204
+ self < (Time.now.utc.to_f - duration)
205
+ end
206
+
207
+ # Checks if timestamp is newer than specified duration in the future
208
+ #
209
+ # @example
210
+ # Time.now.newer_than?(1.second) #=> false
211
+ def newer_than?(duration)
212
+ self > (Time.now.utc.to_f + duration)
213
+ end
214
+
215
+ # Checks if timestamp is within specified duration of now (past or future)
216
+ #
217
+ # @param duration [Numeric] Duration in seconds to compare against
218
+ # @return [Boolean] true if timestamp is within duration of now
219
+ # @example
220
+ # 30.minutes.ago.to_i.within?(1.hour) #=> true
221
+ # 30.minutes.from_now.to_i.within?(1.hour) #=> true
222
+ # 2.hours.ago.to_i.within?(1.hour) #=> false
223
+ def within?(duration)
224
+ (self - Time.now.utc.to_f).abs <= duration
225
+ end
226
+ end
227
+
228
+ refine ::String do
229
+ # Converts string time representation to seconds
230
+ #
231
+ # @example
232
+ # "60m".in_seconds #=> 3600.0
233
+ # "2.5h".in_seconds #=> 9000.0
234
+ # "1y".in_seconds #=> 31536000.0
235
+ #
236
+ # @return [Float, nil] Time in seconds or nil if invalid
237
+ def in_seconds
238
+ q, u = scan(/([\d.]+)([a-zA-Zμs]+)?/).flatten
239
+ return nil unless q
240
+
241
+ q = q.to_f
242
+ u ||= 's'
243
+ q.in_seconds(u)
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -1,51 +1,5 @@
1
1
  # lib/familia/refinements.rb
2
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
- # 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_utils'