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
@@ -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,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
|
data/lib/familia/data_type.rb
CHANGED
@@ -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 =
|
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
|
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 =
|
35
|
-
rescue
|
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
|
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(**
|
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
|
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}"
|
data/lib/familia/encryption.rb
CHANGED
data/lib/familia/errors.rb
CHANGED
@@ -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
|
-
|
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
|
#
|