familia 2.0.0.pre12 → 2.0.0.pre14
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 +529 -0
- data/CLAUDE.md +1 -1
- data/Gemfile +1 -6
- data/Gemfile.lock +13 -7
- data/README.md +21 -2
- 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 +95 -0
- data/docs/migrating/v2.0.0-pre14.md +37 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +6 -0
- data/examples/autoloader/mega_customer.rb +17 -0
- data/examples/safe_dump.rb +1 -1
- 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/external_identifier.rb +3 -3
- 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_literals.rb +279 -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/external_identifier/external_identifier_try.rb +26 -0
- 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 +34 -6
- data/CHANGELOG.md +0 -247
- data/lib/familia/core_ext.rb +0 -135
- data/lib/familia/features/autoloader.rb +0 -57
data/changelog.d/README.md
CHANGED
@@ -17,11 +17,11 @@ This approach provides several benefits:
|
|
17
17
|
|
18
18
|
## Relevant paths
|
19
19
|
|
20
|
-
* `changelog.d/` - (e.g. changelog.d/YYYYMMDD_HHmmss_username_branch.
|
20
|
+
* `changelog.d/` - (e.g. changelog.d/YYYYMMDD_HHmmss_username_branch.rst)
|
21
21
|
* `docs/migrating/` - (e.g. docs/migrating/v2.0.0-pre.md)
|
22
|
-
* `CHANGELOG.
|
22
|
+
* `CHANGELOG.rst` - The full changelog for all releases, in reverse chronological order. Careful: LARGE DOCUMENT. Limit reading to the first 50 lines.
|
23
23
|
|
24
|
-
* `
|
24
|
+
* `changelog.d/scriv.ini` - Scriv tool settings
|
25
25
|
|
26
26
|
## How to Add a Changelog Entry
|
27
27
|
|
@@ -44,7 +44,7 @@ Compare the headers of your draft content with the headers of the previous migra
|
|
44
44
|
|
45
45
|
4. **Commit with Your Code:**
|
46
46
|
```bash
|
47
|
-
git add changelog.d/YYYYMMDD_HHmmss_username_branch.
|
47
|
+
git add changelog.d/YYYYMMDD_HHmmss_username_branch.rst [docs/migrating/v2.0.0-pre.md]
|
48
48
|
git commit
|
49
49
|
```
|
50
50
|
|
@@ -74,4 +74,4 @@ Use these categories:
|
|
74
74
|
|
75
75
|
## Release Process
|
76
76
|
|
77
|
-
At release time, scriv will collect all fragments into the main `CHANGELOG.
|
77
|
+
At release time, scriv will collect all fragments into the main `CHANGELOG.rst` file with th command `scriv collect`. The version is taken automatically from `lib/familia/version.rb`.
|
@@ -0,0 +1,228 @@
|
|
1
|
+
# Feature System Autoloading
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
Familia's feature system includes an autoloading mechanism that automatically discovers and loads feature-specific extension files when features are included in your classes. This allows you to keep your main model files clean while organizing feature-specific configurations in separate files.
|
6
|
+
|
7
|
+
## The Problem It Solves
|
8
|
+
|
9
|
+
When you include a feature like `safe_dump` in a Familia class:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
class User < Familia::Horreum
|
13
|
+
feature :safe_dump # This line should trigger autoloading
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
You want to be able to define the safe dump configuration in a separate file:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
# user/safe_dump_extensions.rb
|
21
|
+
class User
|
22
|
+
safe_dump_fields :name, :email # Don't dump :password
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
But there's a **timing problem**: when should this extension file be loaded?
|
27
|
+
|
28
|
+
## Why Standard `included` Hook Doesn't Work
|
29
|
+
|
30
|
+
The original approach tried to use the standard `included` hook:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
module SafeDump
|
34
|
+
def self.included(base)
|
35
|
+
# Try to autoload here - BUT THIS IS TOO EARLY!
|
36
|
+
autoload_files_for(base)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
**Problem**: This happens **during** the feature inclusion process, before the feature is fully set up. The class isn't in a stable state yet.
|
42
|
+
|
43
|
+
## The Solution: Post-Inclusion Hook
|
44
|
+
|
45
|
+
The `post_inclusion_autoload` system works in **two phases**:
|
46
|
+
|
47
|
+
### Phase 1: Feature System Hook
|
48
|
+
|
49
|
+
In `lib/familia/features.rb`, after including the feature module:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
def feature(feature_name, **options)
|
53
|
+
# ... setup code ...
|
54
|
+
|
55
|
+
include feature_class # Include the feature module
|
56
|
+
|
57
|
+
# NOW call the post-inclusion hook
|
58
|
+
if feature_class.respond_to?(:post_inclusion_autoload)
|
59
|
+
feature_class.post_inclusion_autoload(self, feature_name, options)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
```
|
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
|
+
3. **Loads any matching files** found in those locations
|
88
|
+
|
89
|
+
## Why This Timing Matters
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
class User < Familia::Horreum
|
93
|
+
feature :safe_dump # ← Timing is critical here
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
**What happens in order:**
|
98
|
+
|
99
|
+
1. `feature :safe_dump` is called
|
100
|
+
2. Feature system includes `Familia::Features::SafeDump` module
|
101
|
+
3. **Feature is now fully included and stable**
|
102
|
+
4. `post_inclusion_autoload` is called
|
103
|
+
5. Extension files are discovered and loaded
|
104
|
+
6. `safe_dump_fields :name, :email` executes in the extension file
|
105
|
+
|
106
|
+
## File Naming Conventions
|
107
|
+
|
108
|
+
The autoloading system looks for files matching these patterns (in order of precedence):
|
109
|
+
|
110
|
+
1. `{model_directory}/{model_name}/{feature_name}_*.rb`
|
111
|
+
2. `{model_directory}/{model_name}/features/{feature_name}_*.rb`
|
112
|
+
3. `{model_directory}/features/{feature_name}_*.rb`
|
113
|
+
|
114
|
+
### Examples
|
115
|
+
|
116
|
+
For a `User` class defined in `app/models/user.rb` with `feature :safe_dump`:
|
117
|
+
|
118
|
+
```
|
119
|
+
app/models/user/safe_dump_extensions.rb # ← Most specific
|
120
|
+
app/models/user/safe_dump_config.rb # ← Also matches pattern
|
121
|
+
app/models/user/features/safe_dump_*.rb # ← Feature subdirectory
|
122
|
+
app/models/features/safe_dump_*.rb # ← Shared feature configs
|
123
|
+
```
|
124
|
+
|
125
|
+
## Complete Example
|
126
|
+
|
127
|
+
### Main Model File
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
# app/models/user.rb
|
131
|
+
class User < Familia::Horreum
|
132
|
+
field :name
|
133
|
+
field :email
|
134
|
+
field :password
|
135
|
+
field :created_at
|
136
|
+
|
137
|
+
feature :safe_dump # ← Triggers autoloading
|
138
|
+
feature :expiration
|
139
|
+
end
|
140
|
+
```
|
141
|
+
|
142
|
+
### Safe Dump Extensions
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
# app/models/user/safe_dump_extensions.rb
|
146
|
+
class User
|
147
|
+
# Configure which fields are safe to dump in API responses
|
148
|
+
safe_dump_fields :name, :email, :created_at
|
149
|
+
# Note: :password is intentionally excluded for security
|
150
|
+
|
151
|
+
def safe_dump_display_name
|
152
|
+
"#{name} (#{email})"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
```
|
156
|
+
|
157
|
+
### Expiration Extensions
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
# app/models/user/expiration_config.rb
|
161
|
+
class User
|
162
|
+
# Set default TTL for user objects
|
163
|
+
expires_in 30.days
|
164
|
+
|
165
|
+
# Custom expiration logic
|
166
|
+
def should_expire?
|
167
|
+
!active? && last_login_at < 90.days.ago
|
168
|
+
end
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
### Result
|
173
|
+
|
174
|
+
After loading, the `User` class has:
|
175
|
+
- `User.safe_dump_field_names` returns `[:name, :email, :created_at]`
|
176
|
+
- `User.ttl` returns the configured expiration
|
177
|
+
- All extension methods are available on instances
|
178
|
+
|
179
|
+
## Key Benefits
|
180
|
+
|
181
|
+
1. **Separation of Concerns**: Main model file focuses on core definition, extension files handle feature-specific configuration
|
182
|
+
|
183
|
+
2. **Convention Over Configuration**: No manual requires, just follow naming conventions
|
184
|
+
|
185
|
+
3. **Safe Timing**: Extension files load after the feature is fully set up
|
186
|
+
|
187
|
+
4. **Thread Safe**: No shared state between classes
|
188
|
+
|
189
|
+
5. **Discoverable**: Clear file organization makes extensions easy to find
|
190
|
+
|
191
|
+
## Why It's Better Than Alternatives
|
192
|
+
|
193
|
+
- **Manual requires**: Error-prone, verbose, easy to forget
|
194
|
+
- **Configuration blocks**: Clutters the main model file
|
195
|
+
- **Included hook**: Wrong timing, class not stable yet
|
196
|
+
- **Class_eval strings**: Complex, hard to debug and maintain
|
197
|
+
|
198
|
+
The `post_inclusion_autoload` system provides a clean, automatic, and safe way to extend feature behavior without polluting the main class definitions.
|
199
|
+
|
200
|
+
## Implementation Details
|
201
|
+
|
202
|
+
### Autoloadable Module
|
203
|
+
|
204
|
+
Features that support autoloading include the `Familia::Features::Autoloadable` module:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
module Familia::Features::SafeDump
|
208
|
+
include Familia::Features::Autoloadable # ← Enables autoloading
|
209
|
+
|
210
|
+
# Feature implementation...
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
### Anonymous Class Handling
|
215
|
+
|
216
|
+
The system gracefully handles edge cases:
|
217
|
+
|
218
|
+
- **Anonymous classes**: Classes without names (e.g., `Class.new`) are skipped
|
219
|
+
- **Eval contexts**: Classes defined in `eval` or irb are skipped
|
220
|
+
- **Missing files**: No errors if extension files don't exist
|
221
|
+
|
222
|
+
### Error Handling
|
223
|
+
|
224
|
+
- Missing extension files are silently ignored
|
225
|
+
- Syntax errors in extension files propagate normally
|
226
|
+
- `NameError` during constant resolution is caught and logged
|
227
|
+
|
228
|
+
This robust error handling ensures the autoloading system never breaks your application, even with unusual class definitions or missing files.
|
@@ -0,0 +1,221 @@
|
|
1
|
+
# Time Utilities Guide
|
2
|
+
|
3
|
+
Familia provides a comprehensive time utilities refinement that adds convenient time conversion and age calculation methods to Ruby's `Numeric` and `String` classes.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The `Familia::Refinements::TimeLiterals` module extends Ruby's built-in classes with intuitive time manipulation methods:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
using Familia::Refinements::TimeLiterals
|
11
|
+
|
12
|
+
2.hours #=> 7200 (seconds)
|
13
|
+
"30m".in_seconds #=> 1800
|
14
|
+
timestamp.days_old #=> 5.2
|
15
|
+
```
|
16
|
+
|
17
|
+
## Time Constants
|
18
|
+
|
19
|
+
Familia uses **Gregorian year** calculations for consistent time conversions:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
PER_YEAR = 31_556_952.0 # 365.2425 days (accounts for leap years)
|
23
|
+
PER_MONTH = 2_629_746.0 # PER_YEAR / 12 (30.437 days)
|
24
|
+
PER_WEEK = 604_800.0 # 7 days
|
25
|
+
PER_DAY = 86_400.0 # 24 hours
|
26
|
+
```
|
27
|
+
|
28
|
+
### Why Gregorian Year?
|
29
|
+
|
30
|
+
The key design decision ensures **mathematical consistency**:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
12.months == 1.year #=> true (both equal 31,556,952 seconds)
|
34
|
+
```
|
35
|
+
|
36
|
+
This prevents subtle bugs where `12.months` and `1.year` would differ by ~5.8 hours.
|
37
|
+
|
38
|
+
## Basic Time Conversions
|
39
|
+
|
40
|
+
### Converting Numbers to Time Units
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
using Familia::Refinements::TimeLiterals
|
44
|
+
|
45
|
+
# Singular and plural forms work identically
|
46
|
+
1.second #=> 1
|
47
|
+
30.seconds #=> 30
|
48
|
+
5.minutes #=> 300
|
49
|
+
2.hours #=> 7200
|
50
|
+
3.days #=> 259200
|
51
|
+
1.week #=> 604800
|
52
|
+
2.months #=> 5259492
|
53
|
+
1.year #=> 31556952
|
54
|
+
```
|
55
|
+
|
56
|
+
### Converting Time Units Back
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
7200.in_hours #=> 2.0
|
60
|
+
259200.in_days #=> 3.0
|
61
|
+
5259492.in_months #=> 2.0
|
62
|
+
31556952.in_years #=> 1.0
|
63
|
+
```
|
64
|
+
|
65
|
+
## String Time Parsing
|
66
|
+
|
67
|
+
Parse human-readable time strings:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
"30s".in_seconds #=> 30.0
|
71
|
+
"5m".in_seconds #=> 300.0
|
72
|
+
"2h".in_seconds #=> 7200.0
|
73
|
+
"1d".in_seconds #=> 86400.0
|
74
|
+
"1w".in_seconds #=> 604800.0
|
75
|
+
"2mo".in_seconds #=> 5259492.0
|
76
|
+
"1y".in_seconds #=> 31556952.0
|
77
|
+
```
|
78
|
+
|
79
|
+
### Supported String Formats
|
80
|
+
|
81
|
+
| Unit | Abbreviations | Example |
|
82
|
+
|------|---------------|---------|
|
83
|
+
| Microseconds | `us`, `μs`, `microsecond`, `microseconds` | `"500us"` |
|
84
|
+
| Milliseconds | `ms`, `millisecond`, `milliseconds` | `"250ms"` |
|
85
|
+
| Seconds | `s`, `second`, `seconds` | `"30s"` |
|
86
|
+
| Minutes | `m`, `minute`, `minutes` | `"15m"` |
|
87
|
+
| Hours | `h`, `hour`, `hours` | `"2h"` |
|
88
|
+
| Days | `d`, `day`, `days` | `"7d"` |
|
89
|
+
| Weeks | `w`, `week`, `weeks` | `"2w"` |
|
90
|
+
| Months | `mo`, `month`, `months` | `"6mo"` |
|
91
|
+
| Years | `y`, `year`, `years` | `"1y"` |
|
92
|
+
|
93
|
+
**Note**: Use `"mo"` for months to avoid confusion with `"m"` (minutes).
|
94
|
+
|
95
|
+
## Age Calculations
|
96
|
+
|
97
|
+
Calculate how old timestamps are:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
# Basic age calculation
|
101
|
+
old_timestamp = 2.days.ago.to_i
|
102
|
+
old_timestamp.age_in(:days) #=> ~2.0
|
103
|
+
old_timestamp.age_in(:hours) #=> ~48.0
|
104
|
+
old_timestamp.age_in(:months) #=> ~0.066
|
105
|
+
|
106
|
+
# Convenience methods
|
107
|
+
old_timestamp.days_old #=> ~2.0
|
108
|
+
old_timestamp.hours_old #=> ~48.0
|
109
|
+
old_timestamp.minutes_old #=> ~2880.0
|
110
|
+
old_timestamp.weeks_old #=> ~0.28
|
111
|
+
old_timestamp.months_old #=> ~0.066
|
112
|
+
old_timestamp.years_old #=> ~0.005
|
113
|
+
|
114
|
+
# Calculate age from specific reference time
|
115
|
+
past_timestamp = 1.week.ago.to_i
|
116
|
+
reference_time = 3.days.ago
|
117
|
+
past_timestamp.age_in(:days, reference_time) #=> ~4.0
|
118
|
+
```
|
119
|
+
|
120
|
+
## Time Comparisons
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
timestamp = 2.hours.ago.to_i
|
124
|
+
|
125
|
+
# Check if older than duration
|
126
|
+
timestamp.older_than?(1.hour) #=> true
|
127
|
+
timestamp.older_than?(3.hours) #=> false
|
128
|
+
|
129
|
+
# Check if newer than duration (future)
|
130
|
+
future_timestamp = 1.hour.from_now.to_i
|
131
|
+
future_timestamp.newer_than?(30.minutes) #=> true
|
132
|
+
|
133
|
+
# Check if within duration of now (past or future)
|
134
|
+
timestamp.within?(3.hours) #=> true
|
135
|
+
timestamp.within?(1.hour) #=> false
|
136
|
+
```
|
137
|
+
|
138
|
+
## Practical Examples
|
139
|
+
|
140
|
+
### Cache Expiration
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
using Familia::Refinements::TimeLiterals
|
144
|
+
|
145
|
+
class CacheEntry < Familia::Horreum
|
146
|
+
field :data
|
147
|
+
field :created_at
|
148
|
+
|
149
|
+
def expired?
|
150
|
+
created_at.to_i.older_than?(1.hour)
|
151
|
+
end
|
152
|
+
|
153
|
+
def cache_age
|
154
|
+
created_at.to_i.minutes_old
|
155
|
+
end
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
### Session Management
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
class UserSession < Familia::Horreum
|
163
|
+
field :last_active
|
164
|
+
|
165
|
+
def stale?
|
166
|
+
last_active.to_i.older_than?(30.minutes)
|
167
|
+
end
|
168
|
+
|
169
|
+
def session_duration
|
170
|
+
"Active for #{last_active.to_i.hours_old.round(1)} hours"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
### Data Retention
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
def cleanup_old_logs
|
179
|
+
Log.all.select do |log|
|
180
|
+
log.timestamp.to_i.older_than?(30.days)
|
181
|
+
end.each(&:destroy)
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
185
|
+
## Important Notes
|
186
|
+
|
187
|
+
### Calendar vs. Precise Time
|
188
|
+
|
189
|
+
Familia's time utilities use **average durations** suitable for:
|
190
|
+
- Age calculations
|
191
|
+
- Cache expiration
|
192
|
+
- Time-based cleanup
|
193
|
+
- General time arithmetic
|
194
|
+
|
195
|
+
For **calendar-aware operations** (exact months, leap years), use Ruby's `Date`/`Time` classes:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
# For average durations (Familia)
|
199
|
+
user_age = signup_date.to_i.months_old #=> 6.2
|
200
|
+
|
201
|
+
# For exact calendar operations (Ruby stdlib)
|
202
|
+
exact_months = Date.today.months_since(signup_date) #=> 6
|
203
|
+
```
|
204
|
+
|
205
|
+
### Thread Safety
|
206
|
+
|
207
|
+
All time utility methods are thread-safe and work with frozen objects.
|
208
|
+
|
209
|
+
## Migration from v1.x
|
210
|
+
|
211
|
+
If upgrading from earlier versions:
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
# Old behavior (inconsistent)
|
215
|
+
12.months != 1.year # Different values
|
216
|
+
|
217
|
+
# New behavior (consistent)
|
218
|
+
12.months == 1.year # Same value: 31,556,952 seconds
|
219
|
+
```
|
220
|
+
|
221
|
+
Update any code that relied on the old 365-day year constant to expect the new Gregorian year values.
|
@@ -102,7 +102,7 @@ end
|
|
102
102
|
|
103
103
|
### Before: Manual Loading
|
104
104
|
```ruby
|
105
|
-
#
|
105
|
+
# customer/features.rb
|
106
106
|
|
107
107
|
# Manual feature loading (copied from Familia)
|
108
108
|
features_dir = File.join(__dir__, 'features')
|
@@ -112,29 +112,26 @@ if Dir.exist?(features_dir)
|
|
112
112
|
end
|
113
113
|
end
|
114
114
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
feature :deprecated_fields
|
119
|
-
end
|
115
|
+
class Customer < Familia::Horreum
|
116
|
+
# Features now available for use
|
117
|
+
feature :deprecated_fields
|
120
118
|
end
|
121
119
|
```
|
122
120
|
|
123
121
|
### After: Automatic Loading
|
124
122
|
```ruby
|
125
|
-
#
|
126
|
-
|
123
|
+
# customer/features.rb
|
124
|
+
|
125
|
+
class Customer < Familia::Horreum
|
127
126
|
module Features
|
128
|
-
include Familia::
|
127
|
+
include Familia::Autoloader
|
129
128
|
# Automatically discovers and loads all *.rb files from customer/features/
|
130
129
|
end
|
131
130
|
end
|
132
131
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
feature :deprecated_fields
|
137
|
-
end
|
132
|
+
class Customer < Familia::Horreum
|
133
|
+
# Features automatically loaded and available
|
134
|
+
feature :deprecated_fields
|
138
135
|
end
|
139
136
|
```
|
140
137
|
|
@@ -162,6 +159,7 @@ Feature modules can now define fields directly in their `ClassMethods` modules.
|
|
162
159
|
### Example
|
163
160
|
```ruby
|
164
161
|
# features/common_fields.rb
|
162
|
+
|
165
163
|
module CommonFields
|
166
164
|
def self.included(base)
|
167
165
|
base.extend ClassMethods
|
@@ -210,9 +208,9 @@ If you have project-specific features, set up auto-loading:
|
|
210
208
|
```ruby
|
211
209
|
# Create: models/[model_name]/features.rb
|
212
210
|
module YourProject
|
213
|
-
|
211
|
+
class ModelName < Familia::Horreum
|
214
212
|
module Features
|
215
|
-
include Familia::
|
213
|
+
include Familia::Autoloader
|
216
214
|
end
|
217
215
|
end
|
218
216
|
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# Migrating Guide: v2.0.0-pre13
|
2
|
+
|
3
|
+
This version introduces the Feature Autoloading System for automatic discovery and loading of feature-specific configuration files, enabling cleaner separation between core model definitions and feature configurations.
|
4
|
+
|
5
|
+
## Feature Autoloading System
|
6
|
+
|
7
|
+
### What Changed
|
8
|
+
|
9
|
+
Features now automatically discover and load extension files from your project directories using conventional file naming patterns. This eliminates the need to configure features in your main model files.
|
10
|
+
|
11
|
+
### Basic Migration
|
12
|
+
|
13
|
+
#### Before
|
14
|
+
```ruby
|
15
|
+
# app/models/user.rb
|
16
|
+
class User < Familia::Horreum
|
17
|
+
field :name, :email, :password
|
18
|
+
|
19
|
+
feature :safe_dump
|
20
|
+
safe_dump_fields :name, :email # Configuration mixed with model
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
#### After
|
25
|
+
```ruby
|
26
|
+
# app/models/user.rb - Clean model definition
|
27
|
+
class User < Familia::Horreum
|
28
|
+
field :name, :email, :password
|
29
|
+
feature :safe_dump # Configuration auto-loaded
|
30
|
+
end
|
31
|
+
|
32
|
+
# app/models/user/safe_dump_extensions.rb - Automatically discovered
|
33
|
+
class User
|
34
|
+
safe_dump_fields :name, :email
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
### File Naming Convention
|
39
|
+
|
40
|
+
Extension files follow the pattern: `{model_name}/{feature_name}_*.rb`
|
41
|
+
|
42
|
+
```
|
43
|
+
app/models/
|
44
|
+
├── user.rb
|
45
|
+
├── user/
|
46
|
+
│ ├── safe_dump_extensions.rb # SafeDump configuration
|
47
|
+
│ └── expiration_config.rb # Expiration settings
|
48
|
+
```
|
49
|
+
|
50
|
+
### Migration Steps
|
51
|
+
|
52
|
+
1. **Create extension directories**: `mkdir -p app/models/user`
|
53
|
+
2. **Extract feature configuration** from main model files to separate extension files
|
54
|
+
3. **Verify autoloading**: Check that feature methods are available after migration
|
55
|
+
|
56
|
+
### Debugging
|
57
|
+
|
58
|
+
Enable debug output to troubleshoot autoloading:
|
59
|
+
```ruby
|
60
|
+
ENV['FAMILIA_DEBUG'] = '1' # Shows discovered and loaded files
|
61
|
+
```
|
62
|
+
|
63
|
+
Common issues:
|
64
|
+
- Files must follow `{feature_name}_*.rb` naming pattern
|
65
|
+
- Extension files should reopen the same class as your model
|
66
|
+
|
67
|
+
## Architecture
|
68
|
+
|
69
|
+
The Feature Autoloading System consists of two key components:
|
70
|
+
|
71
|
+
### Familia::Autoloader
|
72
|
+
A utility module providing shared file loading functionality:
|
73
|
+
- Handles Dir.glob pattern matching and file loading
|
74
|
+
- Provides consistent debug logging across all autoloading scenarios
|
75
|
+
- Used by both feature-specific and general-purpose autoloading
|
76
|
+
|
77
|
+
### Familia::Features::Autoloadable
|
78
|
+
A mixin for feature modules that enables post-inclusion autoloading:
|
79
|
+
- Uses `Module.const_source_location` to find where user classes are defined
|
80
|
+
- Discovers extension files using conventional patterns relative to the user class location
|
81
|
+
- Integrates with the feature system's inclusion lifecycle
|
82
|
+
|
83
|
+
When you call `feature :safe_dump`, the SafeDump module (which includes Autoloadable) triggers post-inclusion autoloading that searches for `user/safe_dump_*.rb` files and loads them automatically.
|
84
|
+
|
85
|
+
## New Capabilities
|
86
|
+
|
87
|
+
1. **Automatic Extension Discovery**: No manual require statements needed
|
88
|
+
2. **Conventional File Organization**: Standard patterns for consistent project structure
|
89
|
+
3. **Feature Isolation**: Clean separation between core models and feature configurations
|
90
|
+
4. **Shared Autoloader Infrastructure**: Consistent loading behavior across all features
|
91
|
+
5. **Debug Support**: Built-in debugging for troubleshooting autoloading issues
|
92
|
+
|
93
|
+
## Breaking Changes
|
94
|
+
|
95
|
+
**None** - This release is fully backward compatible. Existing models and feature configurations continue to work without modification.
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Migrating Guide: v2.0.0-pre14
|
2
|
+
|
3
|
+
This version renames TimeUtils to TimeLiterals for semantic clarity and fixes an ExternalIdentifier bug.
|
4
|
+
|
5
|
+
## Breaking Change: TimeUtils → TimeLiterals
|
6
|
+
|
7
|
+
**Migration Required:**
|
8
|
+
|
9
|
+
Find and replace in your codebase:
|
10
|
+
```bash
|
11
|
+
# Find files to update
|
12
|
+
grep -r "using Familia::Refinements::TimeUtils" .
|
13
|
+
|
14
|
+
# Replace the import
|
15
|
+
sed -i 's/using Familia::Refinements::TimeUtils/using Familia::Refinements::TimeLiterals/g' *.rb
|
16
|
+
```
|
17
|
+
|
18
|
+
**Before:**
|
19
|
+
```ruby
|
20
|
+
using Familia::Refinements::TimeUtils
|
21
|
+
```
|
22
|
+
|
23
|
+
**After:**
|
24
|
+
```ruby
|
25
|
+
using Familia::Refinements::TimeLiterals
|
26
|
+
```
|
27
|
+
|
28
|
+
All functionality remains identical - only the module name changed.
|
29
|
+
|
30
|
+
## Bug Fix: ExternalIdentifier
|
31
|
+
|
32
|
+
Fixed `NoMethodError` when using ExternalIdentifier by replacing incorrect `.del()` calls with `.remove_field()` in HashKey operations. This affected:
|
33
|
+
- Changing external identifier values
|
34
|
+
- Looking up objects by external ID
|
35
|
+
- Destroying objects with external identifiers
|
36
|
+
|
37
|
+
No migration needed - the fix is automatic.
|