familia 2.0.0.pre13 → 2.0.0.pre15
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/CHANGELOG.rst +29 -7
- data/Gemfile.lock +1 -1
- data/README.md +21 -2
- data/docs/guides/Feature-System-Autoloading.md +3 -33
- data/docs/guides/time-utilities.md +4 -4
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre13.md +38 -272
- data/docs/migrating/v2.0.0-pre14.md +37 -0
- data/examples/safe_dump.rb +1 -1
- data/lib/familia/base.rb +1 -1
- data/lib/familia/data_type.rb +1 -1
- data/lib/familia/{autoloader.rb → features/autoloader.rb} +33 -25
- data/lib/familia/features/encrypted_fields.rb +2 -2
- data/lib/familia/features/expiration/extensions.rb +61 -0
- data/lib/familia/features/expiration.rb +5 -62
- data/lib/familia/features/external_identifier.rb +6 -4
- data/lib/familia/features/object_identifier.rb +2 -1
- data/lib/familia/features/quantization.rb +3 -3
- data/lib/familia/features/relationships.rb +5 -6
- data/lib/familia/features/safe_dump.rb +3 -5
- data/lib/familia/features/transient_fields.rb +3 -1
- data/lib/familia/features.rb +20 -11
- data/lib/familia/field_type.rb +1 -1
- data/lib/familia/horreum.rb +1 -1
- data/lib/familia/refinements/{time_utils.rb → time_literals.rb} +35 -4
- data/lib/familia/refinements.rb +1 -1
- data/lib/familia/utils.rb +1 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +0 -1
- data/try/core/autoloader_try.rb +9 -9
- data/try/core/extensions_try.rb +1 -1
- data/try/core/time_utils_try.rb +18 -18
- data/try/features/external_identifier/external_identifier_try.rb +26 -0
- data/try/features/safe_dump/module_based_extensions_try.rb +100 -0
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -4
- data/try/helpers/test_helpers.rb +6 -6
- metadata +6 -5
- data/lib/familia/features/autoloadable.rb +0 -113
- data/try/features/autoloadable/autoloadable_try.rb +0 -61
data/try/core/time_utils_try.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require_relative '../helpers/test_helpers'
|
2
2
|
|
3
3
|
module RefinedContext
|
4
|
-
using Familia::Refinements::
|
4
|
+
using Familia::Refinements::TimeLiterals
|
5
5
|
|
6
6
|
def self.eval_in_refined_context(code)
|
7
7
|
eval(code)
|
@@ -12,7 +12,7 @@ module RefinedContext
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
# Test
|
15
|
+
# Test TimeLiterals refinement
|
16
16
|
|
17
17
|
## Numeric#months - convert number to months in seconds
|
18
18
|
result = RefinedContext.eval_in_refined_context("1.month")
|
@@ -34,7 +34,7 @@ RefinedContext.instance_eval_in_refined_context("2629746.in_months")
|
|
34
34
|
#=> 1.0
|
35
35
|
|
36
36
|
## Numeric#in_years - convert seconds to years
|
37
|
-
result = RefinedContext.eval_in_refined_context("#{Familia::Refinements::
|
37
|
+
result = RefinedContext.eval_in_refined_context("#{Familia::Refinements::TimeLiterals::PER_YEAR}.in_years")
|
38
38
|
result.round(1)
|
39
39
|
#=> 1.0
|
40
40
|
|
@@ -52,75 +52,75 @@ result.round(0)
|
|
52
52
|
#=> 31556952.0
|
53
53
|
|
54
54
|
## Numeric#age_in - calculate age in months from timestamp (approximately 1 month ago)
|
55
|
-
timestamp = Time.now.to_f - Familia::Refinements::
|
55
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_MONTH
|
56
56
|
result = RefinedContext.eval_in_refined_context("#{timestamp}.age_in(:months)")
|
57
57
|
(result - 1.0).abs < 0.01
|
58
58
|
#=> true
|
59
59
|
|
60
60
|
## Numeric#age_in - calculate age in years from timestamp (approximately 1 year ago)
|
61
|
-
timestamp = Time.now.to_f - Familia::Refinements::
|
61
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_YEAR
|
62
62
|
result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.age_in(:years)")
|
63
63
|
(result - 1.0).abs < 0.01
|
64
64
|
#=> true
|
65
65
|
|
66
66
|
## Numeric#months_old - convenience method for age_in(:months)
|
67
|
-
timestamp = Time.now.to_f - Familia::Refinements::
|
67
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_MONTH
|
68
68
|
result = RefinedContext.eval_in_refined_context("#{timestamp}.months_old")
|
69
69
|
(result - 1.0).abs < 0.01
|
70
70
|
#=> true
|
71
71
|
|
72
72
|
## Numeric#years_old - convenience method for age_in(:years)
|
73
|
-
timestamp = Time.now.to_f - Familia::Refinements::
|
73
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_YEAR
|
74
74
|
result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.years_old")
|
75
75
|
(result - 1.0).abs < 0.01
|
76
76
|
#=> true
|
77
77
|
|
78
78
|
## Numeric#months_old - should NOT return seconds (the original bug)
|
79
|
-
timestamp = Time.now.to_f - Familia::Refinements::
|
79
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_MONTH
|
80
80
|
result = RefinedContext.eval_in_refined_context("#{timestamp}.months_old")
|
81
81
|
result.between?(0.9, 1.1) # Should be ~1 month, not millions of seconds
|
82
82
|
#=> true
|
83
83
|
|
84
84
|
## Numeric#years_old - should NOT return seconds (the original bug)
|
85
|
-
timestamp = Time.now.to_f - Familia::Refinements::
|
85
|
+
timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_YEAR
|
86
86
|
result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.years_old")
|
87
87
|
result.between?(0.9, 1.1) # Should be ~1 year, not millions of seconds
|
88
88
|
#=> true
|
89
89
|
|
90
90
|
## age_in with from_time parameter - months
|
91
|
-
past_time = Time.now - (2 * Familia::Refinements::
|
92
|
-
from_time = Time.now - Familia::Refinements::
|
91
|
+
past_time = Time.now - (2 * Familia::Refinements::TimeLiterals::PER_MONTH) # 2 months ago
|
92
|
+
from_time = Time.now - Familia::Refinements::TimeLiterals::PER_MONTH # 1 month ago
|
93
93
|
result = RefinedContext.eval_in_refined_context("#{past_time.to_f}.age_in(:months, #{from_time.to_f})")
|
94
94
|
(result - 1.0).abs < 0.01
|
95
95
|
#=> true
|
96
96
|
|
97
97
|
## age_in with from_time parameter - years
|
98
|
-
past_time = Time.now - (2 * Familia::Refinements::
|
99
|
-
from_time = Time.now - Familia::Refinements::
|
98
|
+
past_time = Time.now - (2 * Familia::Refinements::TimeLiterals::PER_YEAR) # 2 years ago
|
99
|
+
from_time = Time.now - Familia::Refinements::TimeLiterals::PER_YEAR # 1 year ago
|
100
100
|
result = RefinedContext.instance_eval_in_refined_context("#{past_time.to_f}.age_in(:years, #{from_time.to_f})")
|
101
101
|
(result - 1.0).abs < 0.01
|
102
102
|
#=> true
|
103
103
|
|
104
104
|
## Verify month constant is approximately correct (30.437 days)
|
105
105
|
expected_seconds_per_month = 30.437 * 24 * 60 * 60
|
106
|
-
Familia::Refinements::
|
106
|
+
Familia::Refinements::TimeLiterals::PER_MONTH.round(0)
|
107
107
|
#=> 2629746.0
|
108
108
|
|
109
109
|
## Verify year constant (365.2425 days - Gregorian year)
|
110
110
|
expected_seconds_per_year = 365.2425 * 24 * 60 * 60
|
111
|
-
Familia::Refinements::
|
111
|
+
Familia::Refinements::TimeLiterals::PER_YEAR.round(0)
|
112
112
|
#=> 31556952.0
|
113
113
|
|
114
114
|
## UNIT_METHODS contains months mapping
|
115
|
-
Familia::Refinements::
|
115
|
+
Familia::Refinements::TimeLiterals::UNIT_METHODS['months']
|
116
116
|
#=> :months
|
117
117
|
|
118
118
|
## UNIT_METHODS contains mo mapping
|
119
|
-
Familia::Refinements::
|
119
|
+
Familia::Refinements::TimeLiterals::UNIT_METHODS['mo']
|
120
120
|
#=> :months
|
121
121
|
|
122
122
|
## UNIT_METHODS contains month mapping
|
123
|
-
Familia::Refinements::
|
123
|
+
Familia::Refinements::TimeLiterals::UNIT_METHODS['month']
|
124
124
|
#=> :months
|
125
125
|
|
126
126
|
## Calendar consistency - 12 months equals 1 year (fix for inconsistency issue)
|
@@ -198,3 +198,29 @@ ExternalIdTest.extid_lookup[@test_obj.extid]
|
|
198
198
|
|
199
199
|
# Cleanup test objects
|
200
200
|
@test_obj.destroy! rescue nil
|
201
|
+
|
202
|
+
## Test 1: Changing extid value (should work after bug fix)
|
203
|
+
bug_test_obj = ExternalIdTest.new(id: 'bug_test', name: 'Bug Test Object')
|
204
|
+
bug_test_obj.save
|
205
|
+
bug_test_obj.extid = 'new_extid_value'
|
206
|
+
bug_test_obj.extid
|
207
|
+
#=> "new_extid_value"
|
208
|
+
|
209
|
+
## Test 2: find_by_extid with deleted object (should work after bug fix)
|
210
|
+
delete_test_obj = ExternalIdTest.new(id: 'delete_test', name: 'Delete Test')
|
211
|
+
delete_test_obj.save
|
212
|
+
test_extid = delete_test_obj.extid
|
213
|
+
# Delete the object directly from Redis to simulate cleanup scenario
|
214
|
+
ExternalIdTest.dbclient.del(delete_test_obj.dbkey)
|
215
|
+
# Now try to find by extid - this should clean up mapping and return nil
|
216
|
+
ExternalIdTest.find_by_extid(test_extid)
|
217
|
+
#=> nil
|
218
|
+
|
219
|
+
## Test 3: destroy! method (should work after bug fix)
|
220
|
+
destroy_test_obj = ExternalIdTest.new(id: 'destroy_test', name: 'Destroy Test')
|
221
|
+
destroy_test_obj.save
|
222
|
+
destroy_extid = destroy_test_obj.extid
|
223
|
+
destroy_test_obj.destroy!
|
224
|
+
# Verify mapping was cleaned up
|
225
|
+
ExternalIdTest.extid_lookup.key?(destroy_extid)
|
226
|
+
#=> false
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# try/features/safe_dump/module_based_extensions_try.rb
|
2
|
+
|
3
|
+
require_relative '../../../lib/familia'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'tmpdir'
|
6
|
+
|
7
|
+
# Create test directory structure for module-based SafeDump extensions
|
8
|
+
@test_dir = Dir.mktmpdir('familia_module_extensions_test')
|
9
|
+
@model_file = File.join(@test_dir, 'test_model.rb')
|
10
|
+
@extension_file = File.join(@test_dir, 'test_model', 'safe_dump_extensions.rb')
|
11
|
+
@extension_dir = File.join(@test_dir, 'test_model')
|
12
|
+
|
13
|
+
# Create directory structure
|
14
|
+
FileUtils.mkdir_p(@extension_dir)
|
15
|
+
|
16
|
+
# Write test model file that uses SafeDump
|
17
|
+
File.write(@model_file, <<~RUBY)
|
18
|
+
class TestModel < Familia::Horreum
|
19
|
+
field :name
|
20
|
+
field :email
|
21
|
+
field :secret
|
22
|
+
|
23
|
+
feature :safe_dump
|
24
|
+
end
|
25
|
+
RUBY
|
26
|
+
|
27
|
+
# Write extension file using NEW module-based pattern
|
28
|
+
File.write(@extension_file, <<~RUBY)
|
29
|
+
module TestModel::SafeDumpExtensions
|
30
|
+
def self.included(base)
|
31
|
+
# Define safe dump fields using the DSL
|
32
|
+
base.safe_dump_fields :name, :email
|
33
|
+
|
34
|
+
# Add computed field
|
35
|
+
base.safe_dump_field :display_name, ->(obj) { "\#{obj.name} <\#{obj.email}>" }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Add instance method to verify module inclusion
|
39
|
+
def module_extension_loaded?
|
40
|
+
true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
RUBY
|
44
|
+
|
45
|
+
## Test module-based autoloading by loading model file
|
46
|
+
@model_instance = nil
|
47
|
+
|
48
|
+
begin
|
49
|
+
# Add test directory to load path for extension file loading
|
50
|
+
$LOAD_PATH.unshift(@test_dir)
|
51
|
+
|
52
|
+
require @model_file
|
53
|
+
@model_instance = TestModel.new(
|
54
|
+
name: 'Jane Doe',
|
55
|
+
email: 'jane@example.com',
|
56
|
+
secret: 'top secret'
|
57
|
+
)
|
58
|
+
true
|
59
|
+
rescue => e
|
60
|
+
puts "Error: #{e.message}"
|
61
|
+
false
|
62
|
+
end
|
63
|
+
#=> true
|
64
|
+
|
65
|
+
## Test that module extension method is available
|
66
|
+
@model_instance.respond_to?(:module_extension_loaded?)
|
67
|
+
#=> true
|
68
|
+
|
69
|
+
## Test module extension method works
|
70
|
+
@model_instance.module_extension_loaded?
|
71
|
+
#=> true
|
72
|
+
|
73
|
+
## Test that safe_dump fields were loaded from module extension
|
74
|
+
TestModel.safe_dump_field_names.sort
|
75
|
+
#=> [:display_name, :email, :name]
|
76
|
+
|
77
|
+
## Test that safe_dump functionality works with module-loaded fields
|
78
|
+
@dump_result = @model_instance.safe_dump
|
79
|
+
@dump_result.keys.sort
|
80
|
+
#=> [:display_name, :email, :name]
|
81
|
+
|
82
|
+
## Test basic field values
|
83
|
+
[@dump_result[:name], @dump_result[:email]]
|
84
|
+
#=> ["Jane Doe", "jane@example.com"]
|
85
|
+
|
86
|
+
## Test computed field from module
|
87
|
+
@dump_result[:display_name]
|
88
|
+
#=> "Jane Doe <jane@example.com>"
|
89
|
+
|
90
|
+
## Test that secret field is excluded
|
91
|
+
@dump_result.key?(:secret)
|
92
|
+
#=> false
|
93
|
+
|
94
|
+
## Test that the module was actually included (not just loaded)
|
95
|
+
TestModel.included_modules.any? { |mod| mod.name&.include?('SafeDumpExtensions') }
|
96
|
+
#=> true
|
97
|
+
|
98
|
+
# Cleanup test files and directories
|
99
|
+
FileUtils.rm_rf(@test_dir)
|
100
|
+
$LOAD_PATH.shift if $LOAD_PATH.first == @test_dir
|
@@ -37,10 +37,6 @@ File.write(@extension_file, <<~RUBY)
|
|
37
37
|
end
|
38
38
|
RUBY
|
39
39
|
|
40
|
-
## Test that SafeDump includes Autoloadable
|
41
|
-
Familia::Features::SafeDump.ancestors.include?(Familia::Features::Autoloadable)
|
42
|
-
#=> true
|
43
|
-
|
44
40
|
## Test that SafeDump has post_inclusion_autoload capability
|
45
41
|
Familia::Features::SafeDump.respond_to?(:post_inclusion_autoload)
|
46
42
|
#=> true
|
data/try/helpers/test_helpers.rb
CHANGED
@@ -12,7 +12,7 @@ Familia.enable_database_logging = true
|
|
12
12
|
Familia.enable_database_counter = true
|
13
13
|
|
14
14
|
class Bone < Familia::Horreum
|
15
|
-
using Familia::Refinements::
|
15
|
+
using Familia::Refinements::TimeLiterals
|
16
16
|
|
17
17
|
identifier_field :token
|
18
18
|
field :token
|
@@ -42,7 +42,7 @@ end
|
|
42
42
|
|
43
43
|
class Customer < Familia::Horreum
|
44
44
|
|
45
|
-
using Familia::Refinements::
|
45
|
+
using Familia::Refinements::TimeLiterals
|
46
46
|
|
47
47
|
logical_database 15 # Use something other than the default DB
|
48
48
|
default_expiration 5.years
|
@@ -106,7 +106,7 @@ end
|
|
106
106
|
@c.custid = 'd@example.com'
|
107
107
|
|
108
108
|
class Session < Familia::Horreum
|
109
|
-
using Familia::Refinements::
|
109
|
+
using Familia::Refinements::TimeLiterals
|
110
110
|
|
111
111
|
logical_database 14 # don't use Onetime's default DB
|
112
112
|
default_expiration 180.minutes
|
@@ -130,7 +130,7 @@ end
|
|
130
130
|
@s = Session.new
|
131
131
|
|
132
132
|
class CustomDomain < Familia::Horreum
|
133
|
-
using Familia::Refinements::
|
133
|
+
using Familia::Refinements::TimeLiterals
|
134
134
|
|
135
135
|
feature :expiration
|
136
136
|
|
@@ -161,7 +161,7 @@ end
|
|
161
161
|
@d.custid = @c.custid
|
162
162
|
|
163
163
|
class Limiter < Familia::Horreum
|
164
|
-
using Familia::Refinements::
|
164
|
+
using Familia::Refinements::TimeLiterals
|
165
165
|
|
166
166
|
feature :expiration
|
167
167
|
feature :quantization
|
@@ -243,7 +243,7 @@ end
|
|
243
243
|
|
244
244
|
# Helper module for testing refinements in tryouts
|
245
245
|
module RefinedContext
|
246
|
-
using Familia::Refinements::
|
246
|
+
using Familia::Refinements::TimeLiterals
|
247
247
|
|
248
248
|
def self.eval_in_refined_context(code)
|
249
249
|
eval(code)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: familia
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.0.
|
4
|
+
version: 2.0.0.pre15
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delano Mandelbaum
|
@@ -180,6 +180,7 @@ files:
|
|
180
180
|
- docs/migrating/v2.0.0-pre11.md
|
181
181
|
- docs/migrating/v2.0.0-pre12.md
|
182
182
|
- docs/migrating/v2.0.0-pre13.md
|
183
|
+
- docs/migrating/v2.0.0-pre14.md
|
183
184
|
- docs/migrating/v2.0.0-pre5.md
|
184
185
|
- docs/migrating/v2.0.0-pre6.md
|
185
186
|
- docs/migrating/v2.0.0-pre7.md
|
@@ -192,7 +193,6 @@ files:
|
|
192
193
|
- examples/safe_dump.rb
|
193
194
|
- familia.gemspec
|
194
195
|
- lib/familia.rb
|
195
|
-
- lib/familia/autoloader.rb
|
196
196
|
- lib/familia/base.rb
|
197
197
|
- lib/familia/connection.rb
|
198
198
|
- lib/familia/data_type.rb
|
@@ -216,11 +216,12 @@ files:
|
|
216
216
|
- lib/familia/encryption/request_cache.rb
|
217
217
|
- lib/familia/errors.rb
|
218
218
|
- lib/familia/features.rb
|
219
|
-
- lib/familia/features/
|
219
|
+
- lib/familia/features/autoloader.rb
|
220
220
|
- lib/familia/features/encrypted_fields.rb
|
221
221
|
- lib/familia/features/encrypted_fields/concealed_string.rb
|
222
222
|
- lib/familia/features/encrypted_fields/encrypted_field_type.rb
|
223
223
|
- lib/familia/features/expiration.rb
|
224
|
+
- lib/familia/features/expiration/extensions.rb
|
224
225
|
- lib/familia/features/external_identifier.rb
|
225
226
|
- lib/familia/features/object_identifier.rb
|
226
227
|
- lib/familia/features/quantization.rb
|
@@ -255,7 +256,7 @@ files:
|
|
255
256
|
- lib/familia/refinements.rb
|
256
257
|
- lib/familia/refinements/logger_trace.rb
|
257
258
|
- lib/familia/refinements/snake_case.rb
|
258
|
-
- lib/familia/refinements/
|
259
|
+
- lib/familia/refinements/time_literals.rb
|
259
260
|
- lib/familia/secure_identifier.rb
|
260
261
|
- lib/familia/settings.rb
|
261
262
|
- lib/familia/utils.rb
|
@@ -335,7 +336,6 @@ files:
|
|
335
336
|
- try/encryption/providers/xchacha20_poly1305_provider_try.rb
|
336
337
|
- try/encryption/roundtrip_validation_try.rb
|
337
338
|
- try/encryption/secure_memory_handling_try.rb
|
338
|
-
- try/features/autoloadable/autoloadable_try.rb
|
339
339
|
- try/features/encrypted_fields/aad_protection_try.rb
|
340
340
|
- try/features/encrypted_fields/concealed_string_core_try.rb
|
341
341
|
- try/features/encrypted_fields/context_isolation_try.rb
|
@@ -369,6 +369,7 @@ files:
|
|
369
369
|
- try/features/relationships/relationships_performance_try.rb
|
370
370
|
- try/features/relationships/relationships_performance_working_try.rb
|
371
371
|
- try/features/relationships/relationships_try.rb
|
372
|
+
- try/features/safe_dump/module_based_extensions_try.rb
|
372
373
|
- try/features/safe_dump/safe_dump_advanced_try.rb
|
373
374
|
- try/features/safe_dump/safe_dump_autoloading_try.rb
|
374
375
|
- try/features/safe_dump/safe_dump_try.rb
|
@@ -1,113 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../refinements/snake_case'
|
4
|
-
|
5
|
-
module Familia
|
6
|
-
module Features
|
7
|
-
# Enables automatic loading of feature-specific files when a feature is included in a user class.
|
8
|
-
#
|
9
|
-
# When included in a feature module, adds ClassMethods that detect when the feature is
|
10
|
-
# included in user classes, derives the feature name, and autoloads files matching
|
11
|
-
# conventional patterns in the user class's directory structure.
|
12
|
-
module Autoloadable
|
13
|
-
using Familia::Refinements::SnakeCase
|
14
|
-
|
15
|
-
# Sets up a feature module with autoloading capabilities.
|
16
|
-
#
|
17
|
-
# Extends the feature module with ClassMethods to handle post-inclusion autoloading.
|
18
|
-
#
|
19
|
-
# @param feature_module [Module] the feature module being enhanced
|
20
|
-
def self.included(feature_module)
|
21
|
-
feature_module.extend(ClassMethods)
|
22
|
-
end
|
23
|
-
|
24
|
-
# Methods added to feature modules that include Autoloadable.
|
25
|
-
module ClassMethods
|
26
|
-
# Triggered when the feature is included in a user class.
|
27
|
-
#
|
28
|
-
# Sets up for post-inclusion autoloading. The actual autoloading
|
29
|
-
# is deferred until after feature setup completes.
|
30
|
-
#
|
31
|
-
# @param base [Class] the user class including this feature
|
32
|
-
def included(base)
|
33
|
-
# Call parent included method if it exists (defensive programming for mixed-in contexts)
|
34
|
-
super if defined?(super)
|
35
|
-
|
36
|
-
# No autoloading here - it's deferred to post_inclusion_autoload
|
37
|
-
# to ensure the feature is fully set up before extension files are loaded
|
38
|
-
end
|
39
|
-
|
40
|
-
# Called by the feature system after the feature is fully included.
|
41
|
-
#
|
42
|
-
# Uses const_source_location to determine where the base class is defined,
|
43
|
-
# then autoloads feature-specific extension files from that location.
|
44
|
-
#
|
45
|
-
# @param base [Class] the class that included this feature
|
46
|
-
# @param feature_name [Symbol] the name of the feature
|
47
|
-
# @param options [Hash] feature options (unused but kept for compatibility)
|
48
|
-
def post_inclusion_autoload(base, feature_name, options)
|
49
|
-
Familia.trace :FEATURE, nil, "[Autoloadable] post_inclusion_autoload called for #{feature_name} on #{base.name || base}", caller(1..1) if Familia.debug?
|
50
|
-
|
51
|
-
# Get the source location via Ruby's built-in introspection
|
52
|
-
source_location = nil
|
53
|
-
|
54
|
-
# Check for named classes that can be looked up via const_source_location
|
55
|
-
# Class#name always returns String or nil, so type check is redundant
|
56
|
-
if base.name && !base.name.empty?
|
57
|
-
begin
|
58
|
-
location_info = Module.const_source_location(base.name)
|
59
|
-
source_location = location_info&.first
|
60
|
-
Familia.trace :FEATURE, nil, "[Autoloadable] Source location for #{base.name}: #{source_location}", caller(1..1) if Familia.debug?
|
61
|
-
rescue NameError => e
|
62
|
-
# Handle cases where the class name is not a valid constant name
|
63
|
-
# This can happen in test environments with dynamically created classes
|
64
|
-
Familia.trace :FEATURE, nil, "[Autoloadable] Cannot resolve source location for #{base.name}: #{e.message}", caller(1..1) if Familia.debug?
|
65
|
-
end
|
66
|
-
else
|
67
|
-
Familia.trace :FEATURE, nil, "[Autoloadable] Skipping source location detection - base.name=#{base.name.inspect}", caller(1..1) if Familia.debug?
|
68
|
-
end
|
69
|
-
|
70
|
-
# Autoload feature-specific files if we have a valid source location
|
71
|
-
if source_location && !source_location.include?('-e') # Skip eval/irb contexts
|
72
|
-
Familia.trace :FEATURE, nil, "[Autoloadable] Calling autoload_feature_files with #{source_location}", caller(1..1) if Familia.debug?
|
73
|
-
autoload_feature_files(source_location, base, feature_name.to_s.snake_case)
|
74
|
-
else
|
75
|
-
Familia.trace :FEATURE, nil, "[Autoloadable] Skipping autoload - no valid source location", caller(1..1) if Familia.debug?
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
private
|
80
|
-
|
81
|
-
# Autoloads feature-specific files from conventional directory patterns.
|
82
|
-
#
|
83
|
-
# Searches for files matching patterns like:
|
84
|
-
# - model_name/feature_name_*.rb
|
85
|
-
# - model_name/features/feature_name_*.rb
|
86
|
-
# - features/feature_name_*.rb
|
87
|
-
#
|
88
|
-
# @param location_path [String] path where the user class is defined
|
89
|
-
# @param base [Class] the user class including the feature
|
90
|
-
# @param feature_name [String] snake_case name of the feature
|
91
|
-
def autoload_feature_files(location_path, base, feature_name)
|
92
|
-
base_dir = File.dirname(location_path)
|
93
|
-
|
94
|
-
# Handle anonymous classes gracefully
|
95
|
-
model_name = base.name ? base.name.snake_case : "anonymous_#{base.object_id}"
|
96
|
-
|
97
|
-
# Look for feature-specific files in conventional locations
|
98
|
-
patterns = [
|
99
|
-
File.join(base_dir, model_name, "#{feature_name}_*.rb"),
|
100
|
-
File.join(base_dir, model_name, 'features', "#{feature_name}_*.rb"),
|
101
|
-
File.join(base_dir, 'features', "#{feature_name}_*.rb"),
|
102
|
-
]
|
103
|
-
|
104
|
-
# Use Autoloader's shared method for consistent file loading
|
105
|
-
Familia::Autoloader.autoload_files(
|
106
|
-
patterns,
|
107
|
-
log_prefix: "Autoloadable(#{feature_name})"
|
108
|
-
)
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
@@ -1,61 +0,0 @@
|
|
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
|