familia 2.0.0.pre14 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21e0d1d581a958fbf3492579039d9190764a83fb4add2a47fe3eb23d2d27ef2b
4
- data.tar.gz: 9cda237209e910633ddcb9a7605cecb291eb66825de05aacd3bc6522168d220b
3
+ metadata.gz: 00416beac08786c1fffd75a0c2578669c6a0d18169824ca1a23e9122018a386b
4
+ data.tar.gz: c38f7bfe985a7d6151abf2ce4b6d7ca32ddb97adf8d069880f7e919662937851
5
5
  SHA512:
6
- metadata.gz: 65cda30c3f3f96c0315c03f200dff37b507474d9a6367c6ce03b899450fd041403263ce6a5bfdb318bc90120c59234190e07548e576a522f92bcb4718dba6a55
7
- data.tar.gz: 7a14a093a0fd83bce0e1fd08be41b98c4c418352d1d4210937cc3473f29264e1bf3dae2b671dee18c89b75faeb3a8e38b93ebf55632bba696da8653eb91b4eb3
6
+ metadata.gz: a3d6a60c86460578567fbd6f33e96c8ae0ee2dbfc80f3ffc6f123dc6fb84c6020efd359601122e63543c0128401d235587e9770df8acbbfa1f55cb908a012e07
7
+ data.tar.gz: 5062f93d7494df05a6d62dade395a0522ca8ab02afd4fdc5a00ee21db08b05ace8c3227d07739ef7cdd727fa72888ea4ec415491eb60028b25166d107d574e8e
data/CHANGELOG.rst CHANGED
@@ -44,7 +44,7 @@ Added
44
44
 
45
45
  - **Feature Autoloading System**: Features can now automatically discover and load extension files from your project directories. When you include a feature like ``safe_dump``, Familia searches for configuration files using conventional patterns like ``{model_name}/{feature_name}_*.rb``, enabling clean separation between core model definitions and feature-specific configurations. See ``docs/migrating/v2.0.0-pre13.md`` for migration details.
46
46
 
47
- - **Consolidated autoloader architecture**: Introduced ``Familia::Autoloader`` as a shared utility for consistent file loading patterns across the framework, supporting both general-purpose and feature-specific autoloading scenarios.
47
+ - **Consolidated autoloader architecture**: Introduced ``Familia::Features::Autoloader`` as a shared utility for consistent file loading patterns across the framework, supporting both general-purpose and feature-specific autoloading scenarios.
48
48
 
49
49
  - Added ``PER_MONTH`` constant (2,629,746 seconds = 30.437 days) derived from Gregorian year for consistent month calculations.
50
50
  - Added ``months``, ``month``, and ``in_months`` conversion methods to Numeric refinement.
@@ -183,7 +183,7 @@ Added
183
183
  with ancestry chain traversal for model-specific feature
184
184
  registration. This enables better organization, standardized naming,
185
185
  and automatic loading of project-specific features via the new
186
- ``Familia::Autoloader`` module.
186
+ ``Familia::Features::Autoloader`` module.
187
187
  - **Improved SafeDump DSL**: Replaced the internal
188
188
  ``@safe_dump_fields`` implementation with a cleaner, more robust DSL
189
189
  using ``safe_dump_field`` and ``safe_dump_fields`` methods.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (2.0.0.pre14)
4
+ familia (2.0.0.pre15)
5
5
  benchmark (~> 0.4)
6
6
  connection_pool (~> 2.5)
7
7
  csv (~> 3.3)
@@ -44,7 +44,7 @@ end
44
44
 
45
45
  The `post_inclusion_autoload` system works in **two phases**:
46
46
 
47
- ### Phase 1: Feature System Hook
47
+ ### Feature System Hook
48
48
 
49
49
  In `lib/familia/features.rb`, after including the feature module:
50
50
 
@@ -61,29 +61,6 @@ def feature(feature_name, **options)
61
61
  end
62
62
  ```
63
63
 
64
- ### Phase 2: Autoloadable Implementation
65
-
66
- The `post_inclusion_autoload` method in `lib/familia/features/autoloadable.rb`:
67
-
68
- 1. **Gets the source location** of the user's class file using Ruby's introspection:
69
- ```ruby
70
- location_info = Module.const_source_location(base.name)
71
- source_location = location_info&.first # e.g., "/path/to/models/user.rb"
72
- ```
73
-
74
- 2. **Calculates extension file paths** based on conventions:
75
- ```ruby
76
- base_dir = File.dirname(location_path) # "/path/to/models"
77
- model_name = base.name.snake_case # "user"
78
-
79
- # Look for files like:
80
- patterns = [
81
- "/path/to/models/user/safe_dump_*.rb",
82
- "/path/to/models/user/features/safe_dump_*.rb",
83
- "/path/to/models/features/safe_dump_*.rb"
84
- ]
85
- ```
86
-
87
64
  3. **Loads any matching files** found in those locations
88
65
 
89
66
  ## Why This Timing Matters
@@ -199,17 +176,10 @@ The `post_inclusion_autoload` system provides a clean, automatic, and safe way t
199
176
 
200
177
  ## Implementation Details
201
178
 
202
- ### Autoloadable Module
179
+ ### Autoloader
203
180
 
204
- Features that support autoloading include the `Familia::Features::Autoloadable` module:
181
+ Looks for features files in models/features.rb, models/features/, models/model_name/features.rb, models/model_name/features/
205
182
 
206
- ```ruby
207
- module Familia::Features::SafeDump
208
- include Familia::Features::Autoloadable # ← Enables autoloading
209
-
210
- # Feature implementation...
211
- end
212
- ```
213
183
 
214
184
  ### Anonymous Class Handling
215
185
 
@@ -124,7 +124,7 @@ end
124
124
 
125
125
  class Customer < Familia::Horreum
126
126
  module Features
127
- include Familia::Autoloader
127
+ include Familia::Features::Autoloader
128
128
  # Automatically discovers and loads all *.rb files from customer/features/
129
129
  end
130
130
  end
@@ -210,7 +210,7 @@ If you have project-specific features, set up auto-loading:
210
210
  module YourProject
211
211
  class ModelName < Familia::Horreum
212
212
  module Features
213
- include Familia::Autoloader
213
+ include Familia::Features::Autoloader
214
214
  end
215
215
  end
216
216
  end
@@ -68,7 +68,7 @@ Common issues:
68
68
 
69
69
  The Feature Autoloading System consists of two key components:
70
70
 
71
- ### Familia::Autoloader
71
+ ### Familia::Features::Autoloader
72
72
  A utility module providing shared file loading functionality:
73
73
  - Handles Dir.glob pattern matching and file loading
74
74
  - Provides consistent debug logging across all autoloading scenarios
@@ -1,11 +1,41 @@
1
- # frozen_string_literal: true
1
+ # lib/familia/features/autoloader.rb
2
2
 
3
- module Familia
3
+ module Familia::Features
4
4
  # Provides autoloading functionality for Ruby files based on patterns and conventions.
5
5
  #
6
6
  # Used by the Features module at library startup to load feature files, and available
7
7
  # as a utility for other modules requiring file autoloading capabilities.
8
8
  module Autoloader
9
+ using Familia::Refinements::SnakeCase
10
+
11
+ # Autoloads feature files when this module is included.
12
+ #
13
+ # Discovers and loads all Ruby files in the features/ directory relative to the
14
+ # including module's location. Typically used by Familia::Features.
15
+ #
16
+ # @param base [Module] the module including this autoloader
17
+ def self.included(base)
18
+
19
+ # Get the directory where the including module is defined
20
+ # This should be lib/familia for the Features module
21
+ base_path = File.dirname(caller_locations(1, 1).first.path)
22
+ model_name = base.name.snake_case
23
+ dir_patterns = [
24
+ File.join(base_path, 'features', '*.rb'),
25
+ File.join(base_path, model_name, 'features', '*.rb'),
26
+ File.join(base_path, model_name, 'features.rb'),
27
+ ]
28
+
29
+ # Ensure the Features module exists within the base module
30
+ unless base.const_defined?(:Features) || model_name.eql?('features')
31
+ base.const_set(:Features, Module.new)
32
+ end
33
+
34
+
35
+ # Use the shared autoload_files method
36
+ autoload_files(dir_patterns, log_prefix: "Autoloader[#{model_name}]")
37
+ end
38
+
9
39
  # Autoloads Ruby files matching the given patterns.
10
40
  #
11
41
  # @param patterns [String, Array<String>] file patterns to match (supports Dir.glob patterns)
@@ -15,6 +45,7 @@ module Familia
15
45
  patterns = Array(patterns)
16
46
 
17
47
  patterns.each do |pattern|
48
+ Familia.ld "[#{log_prefix}] Autoloader loading features from #{pattern}"
18
49
  Dir.glob(pattern).each do |file_path|
19
50
  basename = File.basename(file_path)
20
51
 
@@ -26,28 +57,5 @@ module Familia
26
57
  end
27
58
  end
28
59
  end
29
-
30
- # Autoloads feature files when this module is included.
31
- #
32
- # Discovers and loads all Ruby files in the features/ directory relative to the
33
- # including module's location. Typically used by Familia::Features.
34
- #
35
- # @param base [Module] the module including this autoloader
36
- def self.included(base)
37
- # Get the directory where the including module is defined
38
- # This should be lib/familia for the Features module
39
- base_path = File.dirname(caller_locations(1, 1).first.path)
40
- features_dir = File.join(base_path, 'features')
41
-
42
- Familia.ld "[DEBUG] Autoloader loading features from #{features_dir}"
43
-
44
- return unless Dir.exist?(features_dir)
45
-
46
- # Use the shared autoload_files method
47
- autoload_files(
48
- File.join(features_dir, '*.rb'),
49
- log_prefix: 'Autoloader'
50
- )
51
- end
52
60
  end
53
61
  end
@@ -255,6 +255,8 @@ module Familia
255
255
  # - Insider threats with application access
256
256
  #
257
257
  module EncryptedFields
258
+ Familia::Base.add_feature self, :encrypted_fields
259
+
258
260
  def self.included(base)
259
261
  Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
260
262
  base.extend ClassMethods
@@ -430,8 +432,6 @@ module Familia
430
432
  end
431
433
  end
432
434
  end
433
-
434
- Familia::Base.add_feature self, :encrypted_fields
435
435
  end
436
436
  end
437
437
  end
@@ -0,0 +1,61 @@
1
+ # lib/familia/features/expiration/extensions.rb
2
+
3
+ module Familia
4
+ # Add a default update_expiration method for all classes that include
5
+ # Familia::Base. Since expiration is a core feature, we can confidently
6
+ # call `horreum_instance.update_expiration` without defensive programming
7
+ # even when expiration is not enabled for the horreum_instance class.
8
+ module Base
9
+ # Base implementation of update_expiration that maintains API compatibility
10
+ # with the :expiration feature's implementation.
11
+ #
12
+ # This is a no-op implementation that gets overridden by the :expiration
13
+ # feature. It accepts an optional default_expiration parameter to maintain
14
+ # interface compatibility with the overriding implementations.
15
+ #
16
+ # @param default_expiration [Numeric, nil] Time To Live in seconds
17
+ # @return [nil] Always returns nil for the base implementation
18
+ #
19
+ # @note This is a no-op implementation. Classes that need expiration
20
+ # functionality should include the :expiration feature.
21
+ #
22
+ # @example Enable expiration feature
23
+ # class MyModel < Familia::Horreum
24
+ # feature :expiration
25
+ # default_expiration 1.hour
26
+ # end
27
+ #
28
+ def update_expiration(default_expiration: nil)
29
+ Familia.ld <<~LOG
30
+ [update_expiration] Expiration feature not enabled for #{self.class}.
31
+ Key: #{dbkey} Arg: #{default_expiration} (caller: #{caller(1..1)})
32
+ LOG
33
+ nil
34
+ end
35
+
36
+ # Base implementation of ttl that returns -1 (no expiration set)
37
+ #
38
+ # @return [Integer] Always returns -1 for the base implementation
39
+ #
40
+ def ttl
41
+ -1
42
+ end
43
+
44
+ # Base implementation of expires? that returns false
45
+ #
46
+ # @return [Boolean] Always returns false for the base implementation
47
+ #
48
+ def expires?
49
+ false
50
+ end
51
+
52
+ # Base implementation of expired? that returns false
53
+ #
54
+ # @param threshold [Numeric] Ignored in base implementation
55
+ # @return [Boolean] Always returns false for the base implementation
56
+ #
57
+ def expired?(_threshold = 0)
58
+ false
59
+ end
60
+ end
61
+ end
@@ -1,5 +1,7 @@
1
1
  # lib/familia/features/expiration.rb
2
2
 
3
+ require_relative 'expiration/extensions'
4
+
3
5
  module Familia
4
6
  module Features
5
7
  # Expiration is a feature that provides Time To Live (TTL) management for Familia
@@ -149,6 +151,8 @@ module Familia
149
151
  module Expiration
150
152
  @default_expiration = nil
151
153
 
154
+ Familia::Base.add_feature self, :expiration
155
+
152
156
  using Familia::Refinements::TimeLiterals
153
157
 
154
158
  def self.included(base)
@@ -354,67 +358,6 @@ module Familia
354
358
  redis.persist(dbkey)
355
359
  end
356
360
 
357
- Familia::Base.add_feature self, :expiration
358
- end
359
- end
360
- end
361
-
362
- module Familia
363
- # Add a default update_expiration method for all classes that include
364
- # Familia::Base. Since expiration is a core feature, we can confidently
365
- # call `horreum_instance.update_expiration` without defensive programming
366
- # even when expiration is not enabled for the horreum_instance class.
367
- module Base
368
- # Base implementation of update_expiration that maintains API compatibility
369
- # with the :expiration feature's implementation.
370
- #
371
- # This is a no-op implementation that gets overridden by the :expiration
372
- # feature. It accepts an optional default_expiration parameter to maintain
373
- # interface compatibility with the overriding implementations.
374
- #
375
- # @param default_expiration [Numeric, nil] Time To Live in seconds
376
- # @return [nil] Always returns nil for the base implementation
377
- #
378
- # @note This is a no-op implementation. Classes that need expiration
379
- # functionality should include the :expiration feature.
380
- #
381
- # @example Enable expiration feature
382
- # class MyModel < Familia::Horreum
383
- # feature :expiration
384
- # default_expiration 1.hour
385
- # end
386
- #
387
- def update_expiration(default_expiration: nil)
388
- Familia.ld <<~LOG
389
- [update_expiration] Expiration feature not enabled for #{self.class}.
390
- Key: #{dbkey} Arg: #{default_expiration} (caller: #{caller(1..1)})
391
- LOG
392
- nil
393
- end
394
-
395
- # Base implementation of ttl that returns -1 (no expiration set)
396
- #
397
- # @return [Integer] Always returns -1 for the base implementation
398
- #
399
- def ttl
400
- -1
401
- end
402
-
403
- # Base implementation of expires? that returns false
404
- #
405
- # @return [Boolean] Always returns false for the base implementation
406
- #
407
- def expires?
408
- false
409
- end
410
-
411
- # Base implementation of expired? that returns false
412
- #
413
- # @param threshold [Numeric] Ignored in base implementation
414
- # @return [Boolean] Always returns false for the base implementation
415
- #
416
- def expired?(_threshold = 0)
417
- false
418
361
  end
419
362
  end
420
363
  end
@@ -5,6 +5,9 @@ module Familia
5
5
  # Familia::Features::ExternalIdentifier
6
6
  #
7
7
  module ExternalIdentifier
8
+
9
+ Familia::Base.add_feature self, :external_identifier, depends_on: [:object_identifier]
10
+
8
11
  def self.included(base)
9
12
  Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
10
13
  base.extend ClassMethods
@@ -304,7 +307,6 @@ module Familia
304
307
  end
305
308
  end
306
309
 
307
- Familia::Base.add_feature self, :external_identifier, depends_on: [:object_identifier]
308
310
  end
309
311
  end
310
312
  end
@@ -81,6 +81,8 @@ module Familia
81
81
  # - Custom generators allow domain-specific security requirements
82
82
  #
83
83
  module ObjectIdentifier
84
+ Familia::Base.add_feature self, :object_identifier, depends_on: []
85
+
84
86
  DEFAULT_GENERATOR = :uuid_v7
85
87
 
86
88
  def self.included(base)
@@ -301,7 +303,6 @@ module Familia
301
303
  Familia.trace :OBJID_INIT, dbclient, "Generator strategy: #{generator}", caller(1..1)
302
304
  end
303
305
 
304
- Familia::Base.add_feature self, :object_identifier, depends_on: []
305
306
  end
306
307
  end
307
308
  end
@@ -246,6 +246,8 @@ module Familia
246
246
  #
247
247
  module Quantization
248
248
 
249
+ Familia::Base.add_feature self, :quantization
250
+
249
251
  using Familia::Refinements::TimeLiterals
250
252
 
251
253
  def self.included(base)
@@ -397,8 +399,6 @@ module Familia
397
399
  end
398
400
 
399
401
  extend ClassMethods
400
-
401
- Familia::Base.add_feature self, :quantization
402
402
  end
403
403
  end
404
404
  end
@@ -98,9 +98,13 @@ module Familia
98
98
  # { owner: team, collection: :domains }
99
99
  # ], min_permission: :read)
100
100
  module Relationships
101
+
102
+ # Register the feature with Familia
103
+ Familia::Base.add_feature Relationships, :relationships
104
+
101
105
  # Feature initialization
102
106
  def self.included(base)
103
- puts "[DEBUG] Relationships included in #{base}"
107
+ Familia.ld "[#{base}] Relationships included"
104
108
  base.extend ClassMethods
105
109
  base.include InstanceMethods
106
110
 
@@ -108,11 +112,8 @@ module Familia
108
112
  base.include ScoreEncoding
109
113
  base.include RedisOperations
110
114
 
111
- puts '[DEBUG] Including Tracking module'
112
115
  base.include Tracking
113
- puts '[DEBUG] Extending with Tracking::ClassMethods'
114
116
  base.extend Tracking::ClassMethods
115
- puts "[DEBUG] Base now responds to tracked_in: #{base.respond_to?(:tracked_in)}"
116
117
 
117
118
  base.include Indexing
118
119
  base.extend Indexing::ClassMethods
@@ -466,8 +467,6 @@ module Familia
466
467
  end
467
468
  end
468
469
 
469
- # Register the feature with Familia
470
- Familia::Base.add_feature Relationships, :relationships
471
470
  end
472
471
  end
473
472
  end
@@ -41,15 +41,15 @@ module Familia::Features
41
41
  # of symbols in the order they were defined.
42
42
  #
43
43
  module SafeDump
44
- include Familia::Features::Autoloadable
44
+
45
+ Familia::Base.add_feature self, :safe_dump
46
+
45
47
  using Familia::Refinements::SnakeCase
46
48
 
47
49
  @dump_method = :to_json
48
50
  @load_method = :from_json
49
51
 
50
52
  def self.included(base)
51
- # Call the Autoloadable module's included method for post-inclusion setup
52
- super
53
53
 
54
54
  Familia.trace(:LOADED, self, base, caller(1..1)) if Familia.debug?
55
55
  base.extend ClassMethods
@@ -153,8 +153,6 @@ module Familia::Features
153
153
  end
154
154
 
155
155
  extend ClassMethods
156
-
157
- Familia::Base.add_feature self, :safe_dump
158
156
  end
159
157
  end
160
158
  # rubocop:enable ThreadSafety/ClassInstanceVariable
@@ -104,6 +104,9 @@ module Familia
104
104
  # (HashiCorp Vault, AWS Secrets Manager) or languages with secure memory handling.
105
105
  #
106
106
  module TransientFields
107
+
108
+ Familia::Base.add_feature self, :transient_fields, depends_on: nil
109
+
107
110
  def self.included(base)
108
111
  Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
109
112
  base.extend ClassMethods
@@ -221,7 +224,6 @@ module Familia
221
224
  end
222
225
  end
223
226
 
224
- Familia::Base.add_feature self, :transient_fields, depends_on: nil
225
227
  end
226
228
  end
227
229
  end
@@ -1,7 +1,7 @@
1
1
  # lib/familia/features.rb
2
2
 
3
3
  # Load the Autoloader first, then use it to load all other features
4
- require_relative 'autoloader'
4
+ require_relative 'features/autoloader'
5
5
 
6
6
  module Familia
7
7
  FeatureDefinition = Data.define(:name, :depends_on)
@@ -25,12 +25,21 @@ module Familia
25
25
  # feature options. When you enable a feature with options in different models,
26
26
  # each model stores its own separate configuration without interference.
27
27
  #
28
- # ## Project Organization with Autoloadable
28
+ # ## Project Organization with Autoloader
29
29
  #
30
- # For large projects, use {Familia::Features::Autoloadable} to automatically load
30
+ # For large projects, use {Familia::Features::Autoloader} to automatically load
31
31
  # project-specific features from a dedicated directory structure. This helps
32
32
  # organize complex models by separating features into individual files.
33
33
  #
34
+ # ### Class Reopening (Deprecated)
35
+ #
36
+ # Direct class reopening still works but generates deprecation warnings:
37
+ #
38
+ # # app/models/customer/safe_dump_extensions.rb
39
+ # class Customer
40
+ # safe_dump_fields :name, :email # Works but not recommended
41
+ # end
42
+ #
34
43
  # @example Different models with different feature options
35
44
  # class UserModel < Familia::Horreum
36
45
  # feature :object_identifier, generator: :uuid_v4
@@ -57,15 +66,15 @@ module Familia
57
66
  # # In your model file: app/models/customer.rb
58
67
  # class Customer < Familia::Horreum
59
68
  # module Features
60
- # include Familia::Features::Autoloadable
69
+ # include Familia::Features::Autoloader
61
70
  # # Automatically loads all .rb files from app/models/customer/features/
62
71
  # end
63
72
  # end
64
73
  #
65
- # @see Familia::Features::Autoloadable For automatic feature loading
74
+ # @see Familia::Features::Autoloader For automatic feature loading
66
75
  #
67
76
  module Features
68
- include Familia::Autoloader
77
+ include Familia::Features::Autoloader
69
78
 
70
79
  @features_enabled = nil
71
80
  attr_reader :features_enabled
@@ -110,8 +119,8 @@ module Familia
110
119
 
111
120
  # If there's a value provided check that it's a valid feature
112
121
  feature_name = feature_name.to_sym
113
- feature_class = Familia::Base.find_feature(feature_name, self)
114
- unless feature_class
122
+ feature_module = Familia::Base.find_feature(feature_name, self)
123
+ unless feature_module
115
124
  raise Familia::Problem, "Unsupported feature: #{feature_name}"
116
125
  end
117
126
 
@@ -146,11 +155,11 @@ module Familia
146
155
  end
147
156
 
148
157
  # Extend the Familia::Base subclass (e.g. Customer) with the feature module
149
- include feature_class
158
+ include feature_module
150
159
 
151
160
  # Trigger post-inclusion autoloading for features that support it
152
- if feature_class.respond_to?(:post_inclusion_autoload)
153
- feature_class.post_inclusion_autoload(self, feature_name, options)
161
+ if feature_module.respond_to?(:post_inclusion_autoload)
162
+ feature_module.post_inclusion_autoload(self, feature_name, options)
154
163
  end
155
164
 
156
165
  # NOTE: Do we want to extend Familia::DataType here? That would make it
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Familia
4
4
  # Version information for the Familia
5
- VERSION = '2.0.0.pre14'.freeze unless defined?(Familia::VERSION)
5
+ VERSION = '2.0.0.pre15'.freeze unless defined?(Familia::VERSION)
6
6
  end
data/lib/familia.rb CHANGED
@@ -82,7 +82,6 @@ module Familia
82
82
  end
83
83
 
84
84
  require_relative 'familia/base'
85
- require_relative 'familia/features/autoloadable'
86
85
  require_relative 'familia/features'
87
86
  require_relative 'familia/data_type'
88
87
  require_relative 'familia/horreum'
@@ -30,16 +30,16 @@ File.write(@excluded_file, <<~RUBY)
30
30
  $autoloader_file_loaded = true
31
31
  RUBY
32
32
 
33
- ## Test that Familia::Autoloader exists and is a module
34
- Familia::Autoloader.is_a?(Module)
33
+ ## Test that Familia::Features::Autoloader exists and is a module
34
+ Familia::Features::Autoloader.is_a?(Module)
35
35
  #=> true
36
36
 
37
37
  ## Test that autoload_files class method exists
38
- Familia::Autoloader.respond_to?(:autoload_files)
38
+ Familia::Features::Autoloader.respond_to?(:autoload_files)
39
39
  #=> true
40
40
 
41
41
  ## Test that included class method exists
42
- Familia::Autoloader.respond_to?(:included)
42
+ Familia::Features::Autoloader.respond_to?(:included)
43
43
  #=> true
44
44
 
45
45
  ## Test autoload_files with single pattern
@@ -47,7 +47,7 @@ $test_feature1_loaded = false
47
47
  $test_feature2_loaded = false
48
48
  $autoloader_file_loaded = false
49
49
 
50
- Familia::Autoloader.autoload_files(File.join(@features_dir, '*.rb'))
50
+ Familia::Features::Autoloader.autoload_files(File.join(@features_dir, '*.rb'))
51
51
  $test_feature1_loaded && $test_feature2_loaded
52
52
  #=> true
53
53
 
@@ -64,7 +64,7 @@ File.write(@exclude_file, '$exclude_me_loaded = true')
64
64
  $include_me_loaded = false
65
65
  $exclude_me_loaded = false
66
66
 
67
- Familia::Autoloader.autoload_files(
67
+ Familia::Features::Autoloader.autoload_files(
68
68
  File.join(@exclude_features_dir, '*.rb'),
69
69
  exclude: ['autoloader.rb']
70
70
  )
@@ -88,7 +88,7 @@ File.write(@pattern_file2, '$pattern2_loaded = true')
88
88
  $pattern1_loaded = false
89
89
  $pattern2_loaded = false
90
90
 
91
- Familia::Autoloader.autoload_files([
91
+ Familia::Features::Autoloader.autoload_files([
92
92
  File.join(@pattern_dir1, '*.rb'),
93
93
  File.join(@pattern_dir2, '*.rb')
94
94
  ])
@@ -99,11 +99,11 @@ $pattern1_loaded && $pattern2_loaded
99
99
  ## Test that included method loads features from features directory
100
100
  # Create a mock module that includes Autoloader
101
101
  @mock_features_module = Module.new do
102
- include Familia::Autoloader
102
+ include Familia::Features::Autoloader
103
103
  end
104
104
 
105
105
  # The Features module already includes Autoloader, so test indirectly
106
- Familia::Features.ancestors.include?(Familia::Autoloader)
106
+ Familia::Features.ancestors.include?(Familia::Features::Autoloader)
107
107
  #=> true
108
108
 
109
109
  # Cleanup test files and directories
@@ -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
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.pre14
4
+ version: 2.0.0.pre15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -193,7 +193,6 @@ files:
193
193
  - examples/safe_dump.rb
194
194
  - familia.gemspec
195
195
  - lib/familia.rb
196
- - lib/familia/autoloader.rb
197
196
  - lib/familia/base.rb
198
197
  - lib/familia/connection.rb
199
198
  - lib/familia/data_type.rb
@@ -217,11 +216,12 @@ files:
217
216
  - lib/familia/encryption/request_cache.rb
218
217
  - lib/familia/errors.rb
219
218
  - lib/familia/features.rb
220
- - lib/familia/features/autoloadable.rb
219
+ - lib/familia/features/autoloader.rb
221
220
  - lib/familia/features/encrypted_fields.rb
222
221
  - lib/familia/features/encrypted_fields/concealed_string.rb
223
222
  - lib/familia/features/encrypted_fields/encrypted_field_type.rb
224
223
  - lib/familia/features/expiration.rb
224
+ - lib/familia/features/expiration/extensions.rb
225
225
  - lib/familia/features/external_identifier.rb
226
226
  - lib/familia/features/object_identifier.rb
227
227
  - lib/familia/features/quantization.rb
@@ -336,7 +336,6 @@ files:
336
336
  - try/encryption/providers/xchacha20_poly1305_provider_try.rb
337
337
  - try/encryption/roundtrip_validation_try.rb
338
338
  - try/encryption/secure_memory_handling_try.rb
339
- - try/features/autoloadable/autoloadable_try.rb
340
339
  - try/features/encrypted_fields/aad_protection_try.rb
341
340
  - try/features/encrypted_fields/concealed_string_core_try.rb
342
341
  - try/features/encrypted_fields/context_isolation_try.rb
@@ -370,6 +369,7 @@ files:
370
369
  - try/features/relationships/relationships_performance_try.rb
371
370
  - try/features/relationships/relationships_performance_working_try.rb
372
371
  - try/features/relationships/relationships_try.rb
372
+ - try/features/safe_dump/module_based_extensions_try.rb
373
373
  - try/features/safe_dump/safe_dump_advanced_try.rb
374
374
  - try/features/safe_dump/safe_dump_autoloading_try.rb
375
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