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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +2 -3
  3. data/CHANGELOG.rst +507 -0
  4. data/CLAUDE.md +1 -1
  5. data/Gemfile +1 -6
  6. data/Gemfile.lock +13 -7
  7. data/changelog.d/README.md +5 -5
  8. data/{setup.cfg → changelog.d/scriv.ini} +1 -1
  9. data/docs/guides/Feature-System-Autoloading.md +228 -0
  10. data/docs/guides/time-utilities.md +221 -0
  11. data/docs/migrating/v2.0.0-pre11.md +14 -16
  12. data/docs/migrating/v2.0.0-pre13.md +329 -0
  13. data/examples/autoloader/mega_customer/safe_dump_fields.rb +6 -0
  14. data/examples/autoloader/mega_customer.rb +17 -0
  15. data/familia.gemspec +1 -0
  16. data/lib/familia/autoloader.rb +53 -0
  17. data/lib/familia/base.rb +5 -0
  18. data/lib/familia/data_type.rb +4 -0
  19. data/lib/familia/encryption/encrypted_data.rb +4 -4
  20. data/lib/familia/encryption/manager.rb +6 -4
  21. data/lib/familia/encryption.rb +1 -1
  22. data/lib/familia/errors.rb +3 -0
  23. data/lib/familia/features/autoloadable.rb +113 -0
  24. data/lib/familia/features/encrypted_fields/concealed_string.rb +4 -2
  25. data/lib/familia/features/expiration.rb +4 -0
  26. data/lib/familia/features/quantization.rb +5 -0
  27. data/lib/familia/features/safe_dump.rb +7 -0
  28. data/lib/familia/features.rb +20 -16
  29. data/lib/familia/field_type.rb +2 -0
  30. data/lib/familia/horreum/core/serialization.rb +3 -3
  31. data/lib/familia/horreum/subclass/definition.rb +3 -4
  32. data/lib/familia/horreum.rb +2 -0
  33. data/lib/familia/json_serializer.rb +70 -0
  34. data/lib/familia/logging.rb +12 -10
  35. data/lib/familia/refinements/logger_trace.rb +57 -0
  36. data/lib/familia/refinements/snake_case.rb +40 -0
  37. data/lib/familia/refinements/time_utils.rb +248 -0
  38. data/lib/familia/refinements.rb +3 -49
  39. data/lib/familia/utils.rb +2 -0
  40. data/lib/familia/validation/{test_helpers.rb → validation_helpers.rb} +2 -2
  41. data/lib/familia/validation.rb +1 -1
  42. data/lib/familia/version.rb +1 -1
  43. data/lib/familia.rb +15 -3
  44. data/try/core/autoloader_try.rb +112 -0
  45. data/try/core/extensions_try.rb +38 -21
  46. data/try/core/familia_extended_try.rb +4 -3
  47. data/try/core/time_utils_try.rb +130 -0
  48. data/try/data_types/datatype_base_try.rb +3 -2
  49. data/try/features/autoloadable/autoloadable_try.rb +61 -0
  50. data/try/features/encrypted_fields/concealed_string_core_try.rb +8 -3
  51. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +59 -17
  52. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +36 -12
  53. data/try/features/feature_improvements_try.rb +2 -1
  54. data/try/features/real_feature_integration_try.rb +1 -1
  55. data/try/features/safe_dump/safe_dump_autoloading_try.rb +111 -0
  56. data/try/helpers/test_helpers.rb +24 -0
  57. data/try/integration/cross_component_try.rb +3 -1
  58. metadata +33 -6
  59. data/CHANGELOG.md +0 -247
  60. data/lib/familia/core_ext.rb +0 -135
  61. data/lib/familia/features/autoloader.rb +0 -57
@@ -0,0 +1,329 @@
1
+ # Migrating Guide: v2.0.0-pre13
2
+
3
+ This version introduces significant improvements to Familia's feature system, making it easier to organize and use features across complex projects through automatic discovery and loading of feature-specific configuration files.
4
+
5
+ ## Feature-Specific Autoloading System
6
+
7
+ ### Overview
8
+
9
+ The new autoloading system allows features to automatically discover and load extension files from your project directories. When you include a feature in your model, Familia now searches for configuration files using conventional patterns, enabling clean separation between core model definitions and feature-specific configurations.
10
+
11
+ ### Basic Usage
12
+
13
+ #### Before (Manual Configuration)
14
+ ```ruby
15
+ # app/models/user.rb
16
+ class User < Familia::Horreum
17
+ field :name, :email, :password
18
+
19
+ feature :safe_dump
20
+ # All configuration had to be done in the same file
21
+ safe_dump_fields :name, :email # password excluded for security
22
+ end
23
+ ```
24
+
25
+ #### After (Automatic Discovery)
26
+ ```ruby
27
+ # app/models/user.rb - Clean model definition
28
+ class User < Familia::Horreum
29
+ field :name, :email, :password
30
+ feature :safe_dump # ← Triggers autoloading
31
+ end
32
+
33
+ # app/models/user/safe_dump_extensions.rb - Automatically loaded
34
+ class User
35
+ safe_dump_fields :name, :email # password excluded for security
36
+ end
37
+ ```
38
+
39
+ ### File Naming Conventions
40
+
41
+ The autoloading system follows these patterns for discovering extension files:
42
+
43
+ #### Pattern: `{model_name}/{feature_name}_*.rb`
44
+
45
+ **Example Structures:**
46
+ ```
47
+ app/models/
48
+ ├── user.rb # Main model
49
+ ├── user/
50
+ │ ├── safe_dump_extensions.rb # SafeDump configuration
51
+ │ ├── safe_dump_custom.rb # Additional SafeDump setup
52
+ │ └── relationships_config.rb # Relationships configuration
53
+ ├── product.rb # Another model
54
+ └── product/
55
+ ├── safe_dump_fields.rb # Product's SafeDump config
56
+ └── expiration_settings.rb # Expiration configuration
57
+ ```
58
+
59
+ #### Supported Patterns
60
+ - `safe_dump_extensions.rb`
61
+ - `safe_dump_*.rb` (any filename starting with the feature name)
62
+ - `expiration_config.rb`
63
+ - `relationships_setup.rb`
64
+
65
+ ### Advanced Configuration Examples
66
+
67
+ #### Complex SafeDump Setup
68
+ ```ruby
69
+ # app/models/customer.rb
70
+ class Customer < Familia::Horreum
71
+ field :first_name, :last_name, :email, :phone
72
+ field :credit_card_number, :ssn, :internal_notes
73
+
74
+ feature :safe_dump
75
+ end
76
+
77
+ # app/models/customer/safe_dump_configuration.rb
78
+ class Customer
79
+ # Define which fields are safe for API responses
80
+ safe_dump_fields :first_name, :last_name, :email
81
+
82
+ # Custom serialization for specific fields
83
+ def safe_dump_email
84
+ email&.downcase
85
+ end
86
+
87
+ # Computed fields for API
88
+ def safe_dump_full_name
89
+ "#{first_name} #{last_name}".strip
90
+ end
91
+ end
92
+ ```
93
+
94
+ #### Multi-Feature Organization
95
+ ```ruby
96
+ # app/models/session.rb
97
+ class Session < Familia::Horreum
98
+ field :user_id, :token, :ip_address, :user_agent
99
+
100
+ feature :safe_dump
101
+ feature :expiration
102
+ end
103
+
104
+ # app/models/session/safe_dump_api.rb
105
+ class Session
106
+ safe_dump_fields :user_id, :ip_address
107
+ # token excluded for security
108
+ end
109
+
110
+ # app/models/session/expiration_policy.rb
111
+ class Session
112
+ # Sessions expire after 24 hours
113
+ def self.default_ttl
114
+ 24 * 60 * 60
115
+ end
116
+
117
+ # Cascade expiration to related data
118
+ cascade_expiration_to :user_sessions
119
+ end
120
+ ```
121
+
122
+ ### Consolidated Autoloader Architecture
123
+
124
+ #### Familia::Autoloader
125
+
126
+ The new `Familia::Autoloader` class provides a shared utility for consistent file loading patterns across the framework:
127
+
128
+ ```ruby
129
+ # Internal usage example (you typically won't call this directly)
130
+ autoloader = Familia::Autoloader.new(
131
+ base_class: User,
132
+ feature_name: :safe_dump,
133
+ search_patterns: ['user/safe_dump_*.rb']
134
+ )
135
+
136
+ # Discovers and loads matching files
137
+ autoloader.discover_and_load_extensions
138
+ ```
139
+
140
+ #### Autoloading Strategies
141
+
142
+ The autoloader supports multiple discovery strategies:
143
+
144
+ 1. **Directory-based**: Search in conventional directories
145
+ 2. **Pattern-based**: Use glob patterns for flexible matching
146
+ 3. **Explicit paths**: Load specific files when found
147
+
148
+ ### How Autoloading Works
149
+
150
+ #### Discovery Process
151
+
152
+ 1. **Feature Activation**: When `feature :safe_dump` is called
153
+ 2. **Path Resolution**: Autoloader determines search paths based on model location
154
+ 3. **Pattern Matching**: Searches for files matching `{model_name}/{feature_name}_*.rb`
155
+ 4. **File Loading**: Loads discovered files in alphabetical order
156
+ 5. **Extension Application**: Feature-specific methods become available
157
+
158
+ #### Search Locations
159
+
160
+ The autoloader searches in these locations (in order):
161
+
162
+ ```ruby
163
+ # If your model is in app/models/user.rb, it searches:
164
+ [
165
+ 'app/models/user/', # Same directory as model
166
+ 'lib/user/', # Lib directory
167
+ 'config/models/user/', # Config directory
168
+ './user/' # Current directory
169
+ ]
170
+ ```
171
+
172
+ ### Migration Steps
173
+
174
+ #### 1. Reorganize Existing Models
175
+
176
+ Move feature-specific configuration to separate files:
177
+
178
+ ```bash
179
+ # Create directories for feature configurations
180
+ mkdir -p app/models/user
181
+ mkdir -p app/models/product
182
+ mkdir -p app/models/order
183
+
184
+ # Move configurations
185
+ # From app/models/user.rb, extract safe_dump configuration to:
186
+ # app/models/user/safe_dump_extensions.rb
187
+ ```
188
+
189
+ #### 2. Update Model Files
190
+
191
+ Clean up your main model files:
192
+
193
+ ```ruby
194
+ # Before: Everything in one file
195
+ class User < Familia::Horreum
196
+ field :name, :email, :password, :role
197
+
198
+ feature :safe_dump
199
+ safe_dump_fields :name, :email
200
+
201
+ feature :expiration
202
+ def self.default_ttl
203
+ 86400
204
+ end
205
+ end
206
+
207
+ # After: Clean separation
208
+ class User < Familia::Horreum
209
+ field :name, :email, :password, :role
210
+
211
+ feature :safe_dump # Configuration auto-loaded
212
+ feature :expiration # Configuration auto-loaded
213
+ end
214
+ ```
215
+
216
+ #### 3. Create Extension Files
217
+
218
+ Set up your feature-specific configurations:
219
+
220
+ ```ruby
221
+ # app/models/user/safe_dump_extensions.rb
222
+ class User
223
+ safe_dump_fields :name, :email
224
+ # password and role excluded for security
225
+ end
226
+
227
+ # app/models/user/expiration_config.rb
228
+ class User
229
+ def self.default_ttl
230
+ 86400 # 24 hours
231
+ end
232
+ end
233
+ ```
234
+
235
+ ### Benefits of the New System
236
+
237
+ #### Code Organization
238
+ - **Separation of Concerns**: Feature logic separated from core model definition
239
+ - **Maintainability**: Easier to find and modify feature-specific code
240
+ - **Readability**: Core models are cleaner and more focused
241
+
242
+ #### Team Development
243
+ - **Reduced Conflicts**: Multiple developers can work on different features without merge conflicts
244
+ - **Feature Ownership**: Clear boundaries for feature-specific code
245
+ - **Testing**: Easier to test features in isolation
246
+
247
+ #### Scalability
248
+ - **Large Models**: Complex models remain manageable
249
+ - **Feature Growth**: New features can be added without bloating main model files
250
+ - **Refactoring**: Easier to extract and reorganize feature code
251
+
252
+ ### Debugging Autoloading
253
+
254
+ #### Enable Debug Output
255
+
256
+ ```ruby
257
+ # In your application initialization
258
+ ENV['FAMILIA_DEBUG'] = '1'
259
+
260
+ # Or programmatically
261
+ Familia::Features::Autoloadable.debug = true
262
+ ```
263
+
264
+ #### Debug Output Example
265
+ ```
266
+ [Familia::Autoloader] Searching for User safe_dump extensions...
267
+ [Familia::Autoloader] Found: app/models/user/safe_dump_extensions.rb
268
+ [Familia::Autoloader] Loading: app/models/user/safe_dump_extensions.rb
269
+ [Familia::Autoloader] SafeDump extension loaded successfully for User
270
+ ```
271
+
272
+ #### Troubleshooting
273
+
274
+ **Common Issues:**
275
+
276
+ 1. **File Not Found**: Ensure file naming follows the `{feature_name}_*.rb` pattern
277
+ 2. **Load Order**: Files are loaded alphabetically; prefix with numbers if order matters
278
+ 3. **Class Scope**: Extension files should reopen the same class as your model
279
+
280
+ **Validation:**
281
+ ```ruby
282
+ # Check if autoloading worked
283
+ User.respond_to?(:safe_dump_field_names) # => true
284
+ User.safe_dump_field_names # => [:name, :email]
285
+ ```
286
+
287
+ ### Backward Compatibility
288
+
289
+ The autoloading system is fully backward compatible:
290
+
291
+ - **Existing Models**: Continue to work without changes
292
+ - **Manual Configuration**: Still supported alongside autoloading
293
+ - **Gradual Migration**: You can migrate models one at a time
294
+
295
+ ### Performance Considerations
296
+
297
+ - **Load Time**: Files are loaded once during feature activation
298
+ - **Memory Usage**: No additional memory overhead after loading
299
+ - **Caching**: Discovered files are cached to avoid repeated filesystem scans
300
+
301
+ ### Testing Your Migration
302
+
303
+ ```ruby
304
+ # Test that autoloading is working
305
+ class TestUser < Familia::Horreum
306
+ field :name, :email
307
+ feature :safe_dump
308
+ end
309
+
310
+ # Verify extension methods are available
311
+ puts TestUser.respond_to?(:safe_dump_field_names)
312
+ puts TestUser.safe_dump_field_names.inspect
313
+
314
+ # Test instance methods
315
+ user = TestUser.new(name: "John", email: "john@example.com")
316
+ puts user.safe_dump.inspect
317
+ ```
318
+
319
+ ## New Capabilities
320
+
321
+ 1. **Automatic Extension Discovery**: No manual require statements needed
322
+ 2. **Conventional File Organization**: Standard patterns for consistent project structure
323
+ 3. **Feature Isolation**: Clean separation between core models and feature configurations
324
+ 4. **Shared Autoloader Infrastructure**: Consistent loading behavior across all features
325
+ 5. **Debug Support**: Built-in debugging for troubleshooting autoloading issues
326
+
327
+ ## Breaking Changes
328
+
329
+ **None** - This release is fully backward compatible. Existing models and feature configurations continue to work without modification.
@@ -0,0 +1,6 @@
1
+ # examples/autoloader/mega_customer/safe_dump_fields.rb
2
+
3
+ # Extend the MegaCustomer class to add safe dump fields
4
+ class MegaCustomer
5
+ safe_dump_fields :custid, :username, :created_at, :updated_at
6
+ end
@@ -0,0 +1,17 @@
1
+ # examples/autoloader/mega_customer.rb
2
+
3
+ require_relative '../../lib/familia'
4
+
5
+ class MegaCustomer < Familia::Horreum
6
+ field :custid
7
+ field :username
8
+ field :email
9
+ field :fname
10
+ field :lname
11
+ field :display_name
12
+ field :created_at
13
+ field :updated_at
14
+
15
+ feature :safe_dump
16
+ # feature :deprecated_fields
17
+ end
data/familia.gemspec CHANGED
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.add_dependency 'connection_pool', '~> 2.5'
24
24
  spec.add_dependency 'csv', '~> 3.3'
25
25
  spec.add_dependency 'logger', '~> 1.7'
26
+ spec.add_dependency 'oj', '~> 3.16'
26
27
  spec.add_dependency 'redis', '>= 4.8.1', '< 6.0'
27
28
  spec.add_dependency 'stringio', '~> 3.1.1'
28
29
  spec.add_dependency 'uri-valkey', '~> 1.4'
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ # Provides autoloading functionality for Ruby files based on patterns and conventions.
5
+ #
6
+ # Used by the Features module at library startup to load feature files, and available
7
+ # as a utility for other modules requiring file autoloading capabilities.
8
+ module Autoloader
9
+ # Autoloads Ruby files matching the given patterns.
10
+ #
11
+ # @param patterns [String, Array<String>] file patterns to match (supports Dir.glob patterns)
12
+ # @param exclude [Array<String>] basenames to exclude from loading
13
+ # @param log_prefix [String] prefix for debug logging messages
14
+ def self.autoload_files(patterns, exclude: [], log_prefix: 'Autoloader')
15
+ patterns = Array(patterns)
16
+
17
+ patterns.each do |pattern|
18
+ Dir.glob(pattern).each do |file_path|
19
+ basename = File.basename(file_path)
20
+
21
+ # Skip excluded files
22
+ next if exclude.include?(basename)
23
+
24
+ Familia.trace :FEATURE, nil, "[#{log_prefix}] Loading #{file_path}", caller(1..1) if Familia.debug?
25
+ require File.expand_path(file_path)
26
+ end
27
+ end
28
+ 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
+ end
53
+ end
data/lib/familia/base.rb CHANGED
@@ -14,6 +14,9 @@ module Familia
14
14
  # @see Familia::DataType
15
15
  #
16
16
  module Base
17
+
18
+ using Familia::Refinements::TimeUtils
19
+
17
20
  @features_available = nil
18
21
  @feature_definitions = nil
19
22
  @dump_method = :to_json
@@ -24,6 +27,8 @@ module Familia
24
27
  base.extend(ClassMethods)
25
28
  end
26
29
 
30
+ # Familia::Base::ClassMethods
31
+ #
27
32
  module ClassMethods
28
33
  attr_reader :features_available, :feature_definitions
29
34
  attr_accessor :dump_method, :load_method
@@ -3,6 +3,8 @@
3
3
  require_relative 'data_type/commands'
4
4
  require_relative 'data_type/serialization'
5
5
 
6
+ # Familia
7
+ #
6
8
  module Familia
7
9
  # DataType - Base class for Database data type wrappers
8
10
  #
@@ -14,6 +16,8 @@ module Familia
14
16
  include Familia::Base
15
17
  extend Familia::Features
16
18
 
19
+ using Familia::Refinements::TimeUtils
20
+
17
21
  @registered_types = {}
18
22
  @valid_options = %i[class parent default_expiration default logical_database dbkey dbclient suffix prefix]
19
23
  @logical_database = nil
@@ -9,7 +9,7 @@ module Familia
9
9
  return false unless json_string.kind_of?(::String)
10
10
 
11
11
  begin
12
- parsed = JSON.parse(json_string, symbolize_names: true)
12
+ parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true)
13
13
  return false unless parsed.is_a?(Hash)
14
14
 
15
15
  # Check for required fields
@@ -17,7 +17,7 @@ module Familia
17
17
  result = required_fields.all? { |field| parsed.key?(field) }
18
18
  Familia.ld "[valid?] result: #{result}, parsed: #{parsed}, required: #{required_fields}"
19
19
  result
20
- rescue JSON::ParserError => e
20
+ rescue Familia::SerializerError => e
21
21
  Familia.ld "[valid?] JSON error: #{e.message}"
22
22
  false
23
23
  end
@@ -31,8 +31,8 @@ module Familia
31
31
  end
32
32
 
33
33
  begin
34
- parsed = JSON.parse(json_string, symbolize_names: true)
35
- rescue JSON::ParserError => e
34
+ parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true)
35
+ rescue Familia::SerializerError => e
36
36
  raise EncryptionError, "Invalid JSON structure: #{e.message}"
37
37
  end
38
38
 
@@ -19,13 +19,15 @@ module Familia
19
19
 
20
20
  result = @provider.encrypt(plaintext, key, additional_data)
21
21
 
22
- Familia::Encryption::EncryptedData.new(
22
+ encrypted_data = Familia::Encryption::EncryptedData.new(
23
23
  algorithm: @provider.algorithm,
24
24
  nonce: Base64.strict_encode64(result[:nonce]),
25
25
  ciphertext: Base64.strict_encode64(result[:ciphertext]),
26
26
  auth_tag: Base64.strict_encode64(result[:auth_tag]),
27
27
  key_version: current_key_version
28
- ).to_h.to_json
28
+ ).to_h
29
+
30
+ Familia::JsonSerializer.dump(encrypted_data)
29
31
  ensure
30
32
  Familia::Encryption.secure_wipe(key) if key
31
33
  end
@@ -37,7 +39,7 @@ module Familia
37
39
  Familia::Encryption.derivation_count.increment
38
40
 
39
41
  begin
40
- data = Familia::Encryption::EncryptedData.new(**JSON.parse(encrypted_json, symbolize_names: true))
42
+ data = Familia::Encryption::EncryptedData.new(**Familia::JsonSerializer.parse(encrypted_json, symbolize_names: true))
41
43
 
42
44
  # Validate algorithm support
43
45
  provider = Registry.get(data.algorithm)
@@ -51,7 +53,7 @@ module Familia
51
53
  provider.decrypt(ciphertext, key, nonce, auth_tag, additional_data)
52
54
  rescue EncryptionError
53
55
  raise
54
- rescue JSON::ParserError => e
56
+ rescue Familia::SerializerError => e
55
57
  raise EncryptionError, "Invalid JSON structure: #{e.message}"
56
58
  rescue StandardError => e
57
59
  raise EncryptionError, "Decryption failed: #{e.message}"
@@ -1,7 +1,7 @@
1
1
  # lib/familia/encryption.rb
2
2
 
3
3
  require 'base64'
4
- require 'json'
4
+ require 'oj'
5
5
  require 'openssl'
6
6
 
7
7
  # Provider system components
@@ -6,6 +6,9 @@ module Familia
6
6
  class NonUniqueKey < Problem; end
7
7
 
8
8
  class FieldTypeError < Problem; end
9
+ class AutoloadError < Problem; end
10
+
11
+ class SerializerError < Problem; end
9
12
 
10
13
  class HighRiskFactor < Problem
11
14
  attr_reader :value
@@ -0,0 +1,113 @@
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
@@ -37,6 +37,8 @@
37
37
  # user.to_json # Safe - contains [CONCEALED]
38
38
  #
39
39
  class ConcealedString
40
+ REDACTED = '[CONCEALED]'.freeze
41
+
40
42
  # Create a concealed string wrapper
41
43
  #
42
44
  # @param encrypted_data [String] The encrypted JSON data
@@ -264,9 +266,9 @@ class ConcealedString
264
266
  { concealed: true }
265
267
  end
266
268
 
267
- # Prevent exposure in JSON serialization
269
+ # Prevent exposure in JSON serialization - fail closed for security
268
270
  def to_json(*)
269
- '"[CONCEALED]"'
271
+ raise Familia::SerializerError, "ConcealedString cannot be serialized to JSON"
270
272
  end
271
273
 
272
274
  # Prevent exposure in Rails serialization (as_json -> to_json)
@@ -149,6 +149,8 @@ module Familia
149
149
  module Expiration
150
150
  @default_expiration = nil
151
151
 
152
+ using Familia::Refinements::TimeUtils
153
+
152
154
  def self.included(base)
153
155
  Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
154
156
  base.extend ClassMethods
@@ -160,6 +162,8 @@ module Familia
160
162
  base.instance_variable_set(:@default_expiration, @default_expiration)
161
163
  end
162
164
 
165
+ # Familia::Expiration::ClassMethods
166
+ #
163
167
  module ClassMethods
164
168
  # Set the default expiration time for instances of this class
165
169
  #