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,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::TimeUtils` module extends Ruby's built-in classes with intuitive time manipulation methods:
8
+
9
+ ```ruby
10
+ using Familia::Refinements::TimeUtils
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::TimeUtils
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::TimeUtils
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
- # apps/api/v2/models/customer/features.rb
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
- module V2
116
- class Customer < Familia::Horreum
117
- # Features now available for use
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
- # apps/api/v2/models/customer/features.rb
126
- module V2::Customer
123
+ # customer/features.rb
124
+
125
+ class Customer < Familia::Horreum
127
126
  module Features
128
- include Familia::Features::Autoloader
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
- module V2
134
- class Customer < Familia::Horreum
135
- # Features automatically loaded and available
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
- module ModelName
211
+ class ModelName < Familia::Horreum
214
212
  module Features
215
- include Familia::Features::Autoloader
213
+ include Familia::Autoloader
216
214
  end
217
215
  end
218
216
  end