familia 2.0.0.pre12 → 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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +2 -3
- data/CHANGELOG.rst +507 -0
- data/CLAUDE.md +1 -1
- data/Gemfile +1 -6
- data/Gemfile.lock +13 -7
- 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 +329 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +6 -0
- data/examples/autoloader/mega_customer.rb +17 -0
- 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/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_utils.rb +248 -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/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 +33 -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/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_utils'
|
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
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# try/core/autoloader_try.rb
|
2
|
+
|
3
|
+
require_relative '../../lib/familia'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'tmpdir'
|
6
|
+
|
7
|
+
# Create test directory structure for Autoloader testing
|
8
|
+
@test_dir = Dir.mktmpdir('familia_autoloader_test')
|
9
|
+
@features_dir = File.join(@test_dir, 'features')
|
10
|
+
@test_file1 = File.join(@features_dir, 'test_feature1.rb')
|
11
|
+
@test_file2 = File.join(@features_dir, 'test_feature2.rb')
|
12
|
+
@excluded_file = File.join(@features_dir, 'autoloader.rb')
|
13
|
+
|
14
|
+
# Create directory structure
|
15
|
+
FileUtils.mkdir_p(@features_dir)
|
16
|
+
|
17
|
+
# Write test files
|
18
|
+
File.write(@test_file1, <<~RUBY)
|
19
|
+
# Test feature file 1
|
20
|
+
$test_feature1_loaded = true
|
21
|
+
RUBY
|
22
|
+
|
23
|
+
File.write(@test_file2, <<~RUBY)
|
24
|
+
# Test feature file 2
|
25
|
+
$test_feature2_loaded = true
|
26
|
+
RUBY
|
27
|
+
|
28
|
+
File.write(@excluded_file, <<~RUBY)
|
29
|
+
# This should be excluded
|
30
|
+
$autoloader_file_loaded = true
|
31
|
+
RUBY
|
32
|
+
|
33
|
+
## Test that Familia::Autoloader exists and is a module
|
34
|
+
Familia::Autoloader.is_a?(Module)
|
35
|
+
#=> true
|
36
|
+
|
37
|
+
## Test that autoload_files class method exists
|
38
|
+
Familia::Autoloader.respond_to?(:autoload_files)
|
39
|
+
#=> true
|
40
|
+
|
41
|
+
## Test that included class method exists
|
42
|
+
Familia::Autoloader.respond_to?(:included)
|
43
|
+
#=> true
|
44
|
+
|
45
|
+
## Test autoload_files with single pattern
|
46
|
+
$test_feature1_loaded = false
|
47
|
+
$test_feature2_loaded = false
|
48
|
+
$autoloader_file_loaded = false
|
49
|
+
|
50
|
+
Familia::Autoloader.autoload_files(File.join(@features_dir, '*.rb'))
|
51
|
+
$test_feature1_loaded && $test_feature2_loaded
|
52
|
+
#=> true
|
53
|
+
|
54
|
+
## Test that autoload_files respects exclusions (using fresh files)
|
55
|
+
@exclude_test_dir = Dir.mktmpdir('familia_autoloader_exclude_test')
|
56
|
+
@exclude_features_dir = File.join(@exclude_test_dir, 'features')
|
57
|
+
@include_file = File.join(@exclude_features_dir, 'include_me.rb')
|
58
|
+
@exclude_file = File.join(@exclude_features_dir, 'autoloader.rb')
|
59
|
+
|
60
|
+
FileUtils.mkdir_p(@exclude_features_dir)
|
61
|
+
File.write(@include_file, '$include_me_loaded = true')
|
62
|
+
File.write(@exclude_file, '$exclude_me_loaded = true')
|
63
|
+
|
64
|
+
$include_me_loaded = false
|
65
|
+
$exclude_me_loaded = false
|
66
|
+
|
67
|
+
Familia::Autoloader.autoload_files(
|
68
|
+
File.join(@exclude_features_dir, '*.rb'),
|
69
|
+
exclude: ['autoloader.rb']
|
70
|
+
)
|
71
|
+
|
72
|
+
# Should load include file but not the excluded one
|
73
|
+
$include_me_loaded && !$exclude_me_loaded
|
74
|
+
#=> true
|
75
|
+
|
76
|
+
## Test autoload_files with array of patterns (using fresh files)
|
77
|
+
@pattern_test_dir = Dir.mktmpdir('familia_autoloader_pattern_test')
|
78
|
+
@pattern_dir1 = File.join(@pattern_test_dir, 'dir1')
|
79
|
+
@pattern_dir2 = File.join(@pattern_test_dir, 'dir2')
|
80
|
+
@pattern_file1 = File.join(@pattern_dir1, 'file1.rb')
|
81
|
+
@pattern_file2 = File.join(@pattern_dir2, 'file2.rb')
|
82
|
+
|
83
|
+
FileUtils.mkdir_p(@pattern_dir1)
|
84
|
+
FileUtils.mkdir_p(@pattern_dir2)
|
85
|
+
File.write(@pattern_file1, '$pattern1_loaded = true')
|
86
|
+
File.write(@pattern_file2, '$pattern2_loaded = true')
|
87
|
+
|
88
|
+
$pattern1_loaded = false
|
89
|
+
$pattern2_loaded = false
|
90
|
+
|
91
|
+
Familia::Autoloader.autoload_files([
|
92
|
+
File.join(@pattern_dir1, '*.rb'),
|
93
|
+
File.join(@pattern_dir2, '*.rb')
|
94
|
+
])
|
95
|
+
|
96
|
+
$pattern1_loaded && $pattern2_loaded
|
97
|
+
#=> true
|
98
|
+
|
99
|
+
## Test that included method loads features from features directory
|
100
|
+
# Create a mock module that includes Autoloader
|
101
|
+
@mock_features_module = Module.new do
|
102
|
+
include Familia::Autoloader
|
103
|
+
end
|
104
|
+
|
105
|
+
# The Features module already includes Autoloader, so test indirectly
|
106
|
+
Familia::Features.ancestors.include?(Familia::Autoloader)
|
107
|
+
#=> true
|
108
|
+
|
109
|
+
# Cleanup test files and directories
|
110
|
+
FileUtils.rm_rf(@test_dir)
|
111
|
+
FileUtils.rm_rf(@exclude_test_dir)
|
112
|
+
FileUtils.rm_rf(@pattern_test_dir)
|
data/try/core/extensions_try.rb
CHANGED
@@ -1,59 +1,76 @@
|
|
1
1
|
require_relative '../helpers/test_helpers'
|
2
2
|
|
3
|
+
module RefinedContext
|
4
|
+
using Familia::Refinements::TimeUtils
|
5
|
+
|
6
|
+
# This helper evaluates code within the refined context using eval.
|
7
|
+
# This works because eval executes the code as if it were written
|
8
|
+
# at this location, making the refinements available.
|
9
|
+
def self.eval_in_refined_context(code)
|
10
|
+
eval(code)
|
11
|
+
end
|
12
|
+
|
13
|
+
# This helper also evaluates code in the refined context using instance_eval.
|
14
|
+
# This provides an alternative approach for testing refinements.
|
15
|
+
def self.instance_eval_in_refined_context(code)
|
16
|
+
instance_eval(code)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
3
20
|
# Test core extensions
|
4
21
|
|
5
22
|
## String time parsing - seconds
|
6
|
-
'60s'.in_seconds
|
7
|
-
#=> 60
|
23
|
+
RefinedContext.eval_in_refined_context("'60s'.in_seconds")
|
24
|
+
#=> 60.0
|
8
25
|
|
9
26
|
## String time parsing - minutes
|
10
|
-
'5m'.in_seconds
|
11
|
-
#=> 300
|
27
|
+
RefinedContext.instance_eval_in_refined_context("'5m'.in_seconds")
|
28
|
+
#=> 300.0
|
12
29
|
|
13
30
|
## String time parsing - hours
|
14
|
-
'2h'.in_seconds
|
15
|
-
#=> 7200
|
31
|
+
RefinedContext.eval_in_refined_context("'2h'.in_seconds")
|
32
|
+
#=> 7200.0
|
16
33
|
|
17
34
|
## String time parsing - days
|
18
|
-
'1d'.in_seconds
|
19
|
-
#=> 86_400
|
35
|
+
RefinedContext.instance_eval_in_refined_context("'1d'.in_seconds")
|
36
|
+
#=> 86_400.0
|
20
37
|
|
21
|
-
## String time parsing -
|
22
|
-
'1y'.in_seconds
|
23
|
-
#=>
|
38
|
+
## String time parsing - years
|
39
|
+
RefinedContext.eval_in_refined_context("'1y'.in_seconds")
|
40
|
+
#=> 31556952.0
|
24
41
|
|
25
42
|
## Time::Units - second
|
26
|
-
1.second
|
43
|
+
RefinedContext.instance_eval_in_refined_context("1.second")
|
27
44
|
#=> 1
|
28
45
|
|
29
46
|
## Time::Units - minute
|
30
|
-
1.minute
|
47
|
+
RefinedContext.eval_in_refined_context("1.minute")
|
31
48
|
#=> 60
|
32
49
|
|
33
50
|
## Time::Units - hour
|
34
|
-
1.hour
|
51
|
+
RefinedContext.instance_eval_in_refined_context("1.hour")
|
35
52
|
#=> 3600
|
36
53
|
|
37
54
|
## Time::Units - day
|
38
|
-
1.day
|
55
|
+
RefinedContext.eval_in_refined_context("1.day")
|
39
56
|
#=> 86_400
|
40
57
|
|
41
58
|
## Time::Units - week
|
42
|
-
1.week
|
59
|
+
RefinedContext.instance_eval_in_refined_context("1.week")
|
43
60
|
#=> 604_800
|
44
61
|
|
45
62
|
## Numeric extension to_ms
|
46
|
-
1000.to_ms
|
47
|
-
#=>
|
63
|
+
RefinedContext.eval_in_refined_context("1000.to_ms")
|
64
|
+
#=> 1000000.0
|
48
65
|
|
49
66
|
## Numeric extension to_bytes - single byte
|
50
|
-
1.to_bytes
|
67
|
+
RefinedContext.instance_eval_in_refined_context("1.to_bytes")
|
51
68
|
#=> '1.00 B'
|
52
69
|
|
53
70
|
## Numeric extension to_bytes - kilobytes
|
54
|
-
1024.to_bytes
|
71
|
+
RefinedContext.eval_in_refined_context("1024.to_bytes")
|
55
72
|
#=> '1.00 KiB'
|
56
73
|
|
57
74
|
## Numeric extension to_bytes - megabytes
|
58
|
-
(1024 * 1024).to_bytes
|
75
|
+
RefinedContext.instance_eval_in_refined_context("(1024 * 1024).to_bytes")
|
59
76
|
#=> '1.00 MiB'
|
@@ -44,14 +44,15 @@ parsed_time = Familia.now(Time.parse('2011-04-10 20:56:20 UTC').utc)
|
|
44
44
|
#=> [1302468980.0, true, true]
|
45
45
|
|
46
46
|
## Familia.qnow
|
47
|
-
Familia.qstamp 10.minutes, time: 1_302_468_980
|
47
|
+
RefinedContext.eval_in_refined_context("Familia.qstamp 10.minutes, time: 1_302_468_980")
|
48
48
|
#=> 1302468600
|
49
49
|
|
50
50
|
## Familia::Object.qstamp
|
51
|
-
Limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1_302_468_980)
|
51
|
+
RefinedContext.eval_in_refined_context("Limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1_302_468_980)")
|
52
52
|
#=> '20:50'
|
53
53
|
|
54
54
|
## Familia::Object#qstamp
|
55
55
|
limiter = Limiter.new :request
|
56
|
-
|
56
|
+
RefinedContext.instance_variable_set(:@limiter, limiter)
|
57
|
+
RefinedContext.eval_in_refined_context("@limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1_302_468_980)")
|
57
58
|
#=> '20:50'
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require_relative '../helpers/test_helpers'
|
2
|
+
|
3
|
+
module RefinedContext
|
4
|
+
using Familia::Refinements::TimeUtils
|
5
|
+
|
6
|
+
def self.eval_in_refined_context(code)
|
7
|
+
eval(code)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.instance_eval_in_refined_context(code)
|
11
|
+
instance_eval(code)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Test TimeUtils refinement
|
16
|
+
|
17
|
+
## Numeric#months - convert number to months in seconds
|
18
|
+
result = RefinedContext.eval_in_refined_context("1.month")
|
19
|
+
result.round(0)
|
20
|
+
#=> 2629746.0
|
21
|
+
|
22
|
+
## Numeric#months - plural form
|
23
|
+
result = RefinedContext.instance_eval_in_refined_context("2.months")
|
24
|
+
result.round(0)
|
25
|
+
#=> 5259492.0
|
26
|
+
|
27
|
+
## Numeric#years - convert number to years in seconds
|
28
|
+
result = RefinedContext.eval_in_refined_context("1.year")
|
29
|
+
result.round(0)
|
30
|
+
#=> 31556952.0
|
31
|
+
|
32
|
+
## Numeric#in_months - convert seconds to months
|
33
|
+
RefinedContext.instance_eval_in_refined_context("2629746.in_months")
|
34
|
+
#=> 1.0
|
35
|
+
|
36
|
+
## Numeric#in_years - convert seconds to years
|
37
|
+
result = RefinedContext.eval_in_refined_context("#{Familia::Refinements::TimeUtils::PER_YEAR}.in_years")
|
38
|
+
result.round(1)
|
39
|
+
#=> 1.0
|
40
|
+
|
41
|
+
## String#in_seconds - parse month string
|
42
|
+
RefinedContext.instance_eval_in_refined_context("'1mo'.in_seconds")
|
43
|
+
#=> 2629746.0
|
44
|
+
|
45
|
+
## String#in_seconds - parse month string (long form)
|
46
|
+
RefinedContext.eval_in_refined_context("'2months'.in_seconds")
|
47
|
+
#=> 5259492.0
|
48
|
+
|
49
|
+
## String#in_seconds - parse year string
|
50
|
+
result = RefinedContext.instance_eval_in_refined_context("'1y'.in_seconds")
|
51
|
+
result.round(0)
|
52
|
+
#=> 31556952.0
|
53
|
+
|
54
|
+
## Numeric#age_in - calculate age in months from timestamp (approximately 1 month ago)
|
55
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_MONTH
|
56
|
+
result = RefinedContext.eval_in_refined_context("#{timestamp}.age_in(:months)")
|
57
|
+
(result - 1.0).abs < 0.01
|
58
|
+
#=> true
|
59
|
+
|
60
|
+
## Numeric#age_in - calculate age in years from timestamp (approximately 1 year ago)
|
61
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_YEAR
|
62
|
+
result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.age_in(:years)")
|
63
|
+
(result - 1.0).abs < 0.01
|
64
|
+
#=> true
|
65
|
+
|
66
|
+
## Numeric#months_old - convenience method for age_in(:months)
|
67
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_MONTH
|
68
|
+
result = RefinedContext.eval_in_refined_context("#{timestamp}.months_old")
|
69
|
+
(result - 1.0).abs < 0.01
|
70
|
+
#=> true
|
71
|
+
|
72
|
+
## Numeric#years_old - convenience method for age_in(:years)
|
73
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_YEAR
|
74
|
+
result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.years_old")
|
75
|
+
(result - 1.0).abs < 0.01
|
76
|
+
#=> true
|
77
|
+
|
78
|
+
## Numeric#months_old - should NOT return seconds (the original bug)
|
79
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_MONTH
|
80
|
+
result = RefinedContext.eval_in_refined_context("#{timestamp}.months_old")
|
81
|
+
result.between?(0.9, 1.1) # Should be ~1 month, not millions of seconds
|
82
|
+
#=> true
|
83
|
+
|
84
|
+
## Numeric#years_old - should NOT return seconds (the original bug)
|
85
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_YEAR
|
86
|
+
result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.years_old")
|
87
|
+
result.between?(0.9, 1.1) # Should be ~1 year, not millions of seconds
|
88
|
+
#=> true
|
89
|
+
|
90
|
+
## age_in with from_time parameter - months
|
91
|
+
past_time = Time.now - (2 * Familia::Refinements::TimeUtils::PER_MONTH) # 2 months ago
|
92
|
+
from_time = Time.now - Familia::Refinements::TimeUtils::PER_MONTH # 1 month ago
|
93
|
+
result = RefinedContext.eval_in_refined_context("#{past_time.to_f}.age_in(:months, #{from_time.to_f})")
|
94
|
+
(result - 1.0).abs < 0.01
|
95
|
+
#=> true
|
96
|
+
|
97
|
+
## age_in with from_time parameter - years
|
98
|
+
past_time = Time.now - (2 * Familia::Refinements::TimeUtils::PER_YEAR) # 2 years ago
|
99
|
+
from_time = Time.now - Familia::Refinements::TimeUtils::PER_YEAR # 1 year ago
|
100
|
+
result = RefinedContext.instance_eval_in_refined_context("#{past_time.to_f}.age_in(:years, #{from_time.to_f})")
|
101
|
+
(result - 1.0).abs < 0.01
|
102
|
+
#=> true
|
103
|
+
|
104
|
+
## Verify month constant is approximately correct (30.437 days)
|
105
|
+
expected_seconds_per_month = 30.437 * 24 * 60 * 60
|
106
|
+
Familia::Refinements::TimeUtils::PER_MONTH.round(0)
|
107
|
+
#=> 2629746.0
|
108
|
+
|
109
|
+
## Verify year constant (365.2425 days - Gregorian year)
|
110
|
+
expected_seconds_per_year = 365.2425 * 24 * 60 * 60
|
111
|
+
Familia::Refinements::TimeUtils::PER_YEAR.round(0)
|
112
|
+
#=> 31556952.0
|
113
|
+
|
114
|
+
## UNIT_METHODS contains months mapping
|
115
|
+
Familia::Refinements::TimeUtils::UNIT_METHODS['months']
|
116
|
+
#=> :months
|
117
|
+
|
118
|
+
## UNIT_METHODS contains mo mapping
|
119
|
+
Familia::Refinements::TimeUtils::UNIT_METHODS['mo']
|
120
|
+
#=> :months
|
121
|
+
|
122
|
+
## UNIT_METHODS contains month mapping
|
123
|
+
Familia::Refinements::TimeUtils::UNIT_METHODS['month']
|
124
|
+
#=> :months
|
125
|
+
|
126
|
+
## Calendar consistency - 12 months equals 1 year (fix for inconsistency issue)
|
127
|
+
result1 = RefinedContext.eval_in_refined_context("12.months")
|
128
|
+
result2 = RefinedContext.instance_eval_in_refined_context("1.year")
|
129
|
+
result1 == result2
|
130
|
+
#=> true
|
@@ -21,7 +21,7 @@ p [@a.name, @b.name]
|
|
21
21
|
#=> true
|
22
22
|
|
23
23
|
## Limiter#qstamp
|
24
|
-
@limiter1.counter.qstamp(10.minutes, '%H:%M', 1_302_468_980)
|
24
|
+
RefinedContext.eval_in_refined_context("@limiter1.counter.qstamp(10.minutes, '%H:%M', 1_302_468_980)")
|
25
25
|
##=> '20:50'
|
26
26
|
|
27
27
|
## Database Types can be stored to quantized stamp suffix
|
@@ -32,7 +32,8 @@ p [@a.name, @b.name]
|
|
32
32
|
@limiter2 = Limiter.new :requests
|
33
33
|
p [@limiter1.default_expiration, @limiter2.default_expiration]
|
34
34
|
p [@limiter1.counter.parent.default_expiration, @limiter2.counter.parent.default_expiration]
|
35
|
-
|
35
|
+
RefinedContext.instance_variable_set(:@limiter2, @limiter2)
|
36
|
+
RefinedContext.eval_in_refined_context("@limiter2.counter.qstamp(10.minutes, pattern: nil, time: 1_302_468_980)")
|
36
37
|
#=> 1302468600
|
37
38
|
|
38
39
|
## Database Types can be stored to quantized numeric suffix. This
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# try/features/autoloadable/autoloadable_try.rb
|
2
|
+
|
3
|
+
require_relative '../../../lib/familia'
|
4
|
+
|
5
|
+
# Create test feature module that includes Autoloadable
|
6
|
+
module TestAutoloadableFeature
|
7
|
+
include Familia::Features::Autoloadable
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
super
|
11
|
+
base.define_method(:test_feature_method) { "feature_loaded" }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Create test class to include the feature
|
16
|
+
class TestModelForAutoloadable < Familia::Horreum
|
17
|
+
field :name
|
18
|
+
end
|
19
|
+
|
20
|
+
## Test that Autoloadable can be included in feature modules
|
21
|
+
TestAutoloadableFeature.ancestors.include?(Familia::Features::Autoloadable)
|
22
|
+
#=> true
|
23
|
+
|
24
|
+
## Test that Autoloadable extends feature modules with ClassMethods
|
25
|
+
TestAutoloadableFeature.respond_to?(:post_inclusion_autoload)
|
26
|
+
#=> true
|
27
|
+
|
28
|
+
## Test that including autoloadable feature in Horreum class works
|
29
|
+
TestModelForAutoloadable.include(TestAutoloadableFeature)
|
30
|
+
TestModelForAutoloadable.ancestors.include?(TestAutoloadableFeature)
|
31
|
+
#=> true
|
32
|
+
|
33
|
+
## Test that post_inclusion_autoload can be called with test class
|
34
|
+
TestAutoloadableFeature.post_inclusion_autoload(TestModelForAutoloadable, :test_autoloadable_feature, {})
|
35
|
+
"success"
|
36
|
+
#=> "success"
|
37
|
+
|
38
|
+
## Test that feature methods are available on the model
|
39
|
+
@test_instance = TestModelForAutoloadable.new(name: 'test')
|
40
|
+
@test_instance.respond_to?(:test_feature_method)
|
41
|
+
#=> true
|
42
|
+
|
43
|
+
## Test that feature method works
|
44
|
+
@test_instance.test_feature_method
|
45
|
+
#=> "feature_loaded"
|
46
|
+
|
47
|
+
## Test that Autoloadable works with DataType classes (should not crash)
|
48
|
+
class TestDataTypeAutoloadable < Familia::DataType
|
49
|
+
include Familia::Features::Autoloadable
|
50
|
+
end
|
51
|
+
|
52
|
+
TestDataTypeAutoloadable.ancestors.include?(Familia::Features::Autoloadable)
|
53
|
+
#=> true
|
54
|
+
|
55
|
+
## Test that SafeDump includes Autoloadable (real-world usage)
|
56
|
+
Familia::Features::SafeDump.ancestors.include?(Familia::Features::Autoloadable)
|
57
|
+
#=> true
|
58
|
+
|
59
|
+
## Test that SafeDump has post_inclusion_autoload capability
|
60
|
+
Familia::Features::SafeDump.respond_to?(:post_inclusion_autoload)
|
61
|
+
#=> true
|
@@ -77,9 +77,14 @@ end
|
|
77
77
|
@doc.content.to_str
|
78
78
|
#=!> NoMethodError
|
79
79
|
|
80
|
-
## JSON serialization - to_json
|
81
|
-
|
82
|
-
|
80
|
+
## JSON serialization - to_json (fails for security)
|
81
|
+
begin
|
82
|
+
@doc.content.to_json
|
83
|
+
raise "Should have raised SerializerError"
|
84
|
+
rescue Familia::SerializerError => e
|
85
|
+
e.class
|
86
|
+
end
|
87
|
+
#=> Familia::SerializerError
|
83
88
|
|
84
89
|
## JSON serialization - as_json
|
85
90
|
@doc.content.as_json
|