familia 2.0.0.pre13 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca101edcf531af301428b4b29bc83f464a84e47339a9d67f1b8a70ead828aa74
4
- data.tar.gz: 56255e7fbc191f8c15b75d5cab0a990b83c3e54fe07362f1b20dbf392edca65d
3
+ metadata.gz: 21e0d1d581a958fbf3492579039d9190764a83fb4add2a47fe3eb23d2d27ef2b
4
+ data.tar.gz: 9cda237209e910633ddcb9a7605cecb291eb66825de05aacd3bc6522168d220b
5
5
  SHA512:
6
- metadata.gz: d92e09f275bcf262a73afe443fa5e099fff6c0836502de52d485358ec1f5d5a095878fe69ff9c4d5fcdbe26aa00c404474418cc72e9748b10d783ded2942e1b5
7
- data.tar.gz: 2e22b89f457a6b188b31820e794960a5ad288c406ad7550fc47f48501489394fb891c966f7e147fe2b89b8d2eaa36b736258b75184a0aee5e5aa2aaf63ac6878
6
+ metadata.gz: 65cda30c3f3f96c0315c03f200dff37b507474d9a6367c6ce03b899450fd041403263ce6a5bfdb318bc90120c59234190e07548e576a522f92bcb4718dba6a55
7
+ data.tar.gz: 7a14a093a0fd83bce0e1fd08be41b98c4c418352d1d4210937cc3473f29264e1bf3dae2b671dee18c89b75faeb3a8e38b93ebf55632bba696da8653eb91b4eb3
data/CHANGELOG.rst CHANGED
@@ -12,6 +12,28 @@ Versioning <https://semver.org/spec/v2.0.0.html>`__.
12
12
 
13
13
  <!--scriv-insert-here-->
14
14
 
15
+ .. _changelog-2.0.0.pre14:
16
+
17
+ 2.0.0.pre14 — 2025-09-08
18
+ ========================
19
+
20
+ Changed
21
+ -------
22
+
23
+ - **BREAKING CHANGE**: Renamed ``Familia::Refinements::TimeUtils`` to ``Familia::Refinements::TimeLiterals`` to better reflect the module's primary purpose of enabling numeric and string values to be treated as time unit literals (e.g., ``5.minutes``, ``"30m".in_seconds``). Functionality remains the same - only the module name has changed. Users must update their refinement usage from ``using Familia::Refinements::TimeUtils`` to ``using Familia::Refinements::TimeLiterals``.
24
+
25
+ Fixed
26
+ -----
27
+
28
+ - Fixed ExternalIdentifier HashKey method calls by replacing incorrect ``.del()`` calls with ``.remove_field()`` in three critical locations: extid setter (cleanup old mapping when changing value), find_by_extid (cleanup orphaned mapping when object not found), and destroy! (cleanup mapping when object is destroyed). Added comprehensive test coverage for all scenarios to prevent regression. PR #100
29
+
30
+ AI Assistance
31
+ -------------
32
+
33
+ - Claude Code helped rename TimeUtils to TimeLiterals throughout the codebase, including module name, file path, all usage references, and updating existing documentation.
34
+ - Gemini 2.5 Flash wrote the inline docs for TimeLiterals based on a discussion re: naming rationale.
35
+ - Claude Code fixed the ExternalIdentifier HashKey method bug, replacing incorrect ``.del()`` calls with proper ``.remove_field()`` calls, and implemented test coverage for the affected scenarios.
36
+
15
37
  .. _changelog-2.0.0.pre13:
16
38
 
17
39
  2.0.0.pre13 — 2025-09-07
@@ -20,13 +42,13 @@ Versioning <https://semver.org/spec/v2.0.0.html>`__.
20
42
  Added
21
43
  -----
22
44
 
23
- - **Feature-specific autoloading**: Features can now automatically discover and load extension files from your project directories. When you include a feature like ``safe_dump``, Familia searches for configuration files using conventional patterns like ``{model_name}/{feature_name}_*.rb``, enabling clean separation between core model definitions and feature-specific configurations.
45
+ - **Feature Autoloading System**: Features can now automatically discover and load extension files from your project directories. When you include a feature like ``safe_dump``, Familia searches for configuration files using conventional patterns like ``{model_name}/{feature_name}_*.rb``, enabling clean separation between core model definitions and feature-specific configurations. See ``docs/migrating/v2.0.0-pre13.md`` for migration details.
24
46
 
25
47
  - **Consolidated autoloader architecture**: Introduced ``Familia::Autoloader`` as a shared utility for consistent file loading patterns across the framework, supporting both general-purpose and feature-specific autoloading scenarios.
26
48
 
27
49
  - Added ``PER_MONTH`` constant (2,629,746 seconds = 30.437 days) derived from Gregorian year for consistent month calculations.
28
50
  - Added ``months``, ``month``, and ``in_months`` conversion methods to Numeric refinement.
29
- - Added month unit mappings (``'mo'``, ``'month'``, ``'months'``) to TimeUtils ``UNIT_METHODS`` hash.
51
+ - Added month unit mappings (``'mo'``, ``'month'``, ``'months'``) to TimeLiterals ``UNIT_METHODS`` hash.
30
52
 
31
53
  - **Error Handling**: Added ``NotSupportedError`` for invalid serialization mode combinations in encryption subsystem. PR #97
32
54
 
@@ -34,7 +56,7 @@ Changed
34
56
  -------
35
57
 
36
58
  - Refactored time and numeric extensions from global monkey patches to proper Ruby refinements for better encapsulation and reduced global namespace pollution
37
- - Updated all internal classes to use refinements via ``using Familia::Refinements::TimeUtils`` statements
59
+ - Updated all internal classes to use refinements via ``using Familia::Refinements::TimeLiterals`` statements
38
60
  - Added centralized ``RefinedContext`` module in test helpers to support refinement testing in tryouts files
39
61
 
40
62
  - Updated ``PER_YEAR`` constant to use Gregorian year (31,556,952 seconds = 365.2425 days) for calendar consistency.
@@ -49,7 +71,7 @@ Fixed
49
71
  - Fixed byte conversion logic in ``to_bytes`` method to correctly handle exact 1024-byte boundaries (``size >= 1024`` instead of ``size > 1024``)
50
72
  - Resolved refinement testing issues in tryouts by implementing ``eval``-based code execution within refined contexts
51
73
 
52
- - Fixed TimeUtils refinement ``months_old`` and ``years_old`` methods returning incorrect values (raw seconds instead of months/years). The underlying ``age_in`` method now properly handles ``:months`` and ``:years`` units. Issue #94.
74
+ - Fixed TimeLiterals refinement ``months_old`` and ``years_old`` methods returning incorrect values (raw seconds instead of months/years). The underlying ``age_in`` method now properly handles ``:months`` and ``:years`` units. Issue #94.
53
75
  - Fixed calendar consistency issue where ``12.months != 1.year`` by updating ``PER_YEAR`` to use Gregorian year (365.2425 days) and defining ``PER_MONTH`` as ``PER_YEAR / 12``.
54
76
 
55
77
  Security
@@ -72,7 +94,7 @@ AI Assistance
72
94
 
73
95
  - Significant AI assistance in architectural design and implementation of the feature-specific autoloading system, including pattern matching logic, Ruby introspection methods, and comprehensive debugging of edge cases and thread safety considerations.
74
96
 
75
- - Claude Code assisted with implementing the fix for broken ``months_old`` and ``years_old`` methods in the TimeUtils refinement, including analysis, implementation, testing, and documentation.
97
+ - Claude Code assisted with implementing the fix for broken ``months_old`` and ``years_old`` methods in the TimeLiterals refinement, including analysis, implementation, testing, and documentation.
76
98
 
77
99
  - Performance optimization research and OJ gem integration strategy, including compatibility analysis and testing approach for seamless stdlib JSON replacement. PR #97
78
100
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (2.0.0.pre13)
4
+ familia (2.0.0.pre14)
5
5
  benchmark (~> 0.4)
6
6
  connection_pool (~> 2.5)
7
7
  csv (~> 3.3)
data/README.md CHANGED
@@ -170,7 +170,26 @@ All relationships support automatic indexing and tracking - objects are automati
170
170
 
171
171
  ## Organizing Complex Models
172
172
 
173
- For large applications, you can organize model complexity using custom features:
173
+ For large applications, you can organize model complexity using custom features and the Feature Autoloading System:
174
+
175
+ ### Feature Autoloading System
176
+
177
+ Familia automatically discovers and loads feature-specific configuration files, enabling clean separation between core model definitions and feature configurations:
178
+
179
+ ```ruby
180
+ # app/models/user.rb - Clean model definition
181
+ class User < Familia::Horreum
182
+ field :name, :email, :password
183
+ feature :safe_dump # Configuration auto-loaded
184
+ end
185
+
186
+ # app/models/user/safe_dump_extensions.rb - Automatically discovered
187
+ class User
188
+ safe_dump_fields :name, :email # password excluded for security
189
+ end
190
+ ```
191
+
192
+ Extension files follow the pattern: `{model_name}/{feature_name}_*.rb`
174
193
 
175
194
  ### Self-Registering Features
176
195
 
@@ -201,7 +220,7 @@ class Customer < Familia::Horreum
201
220
  end
202
221
  ```
203
222
 
204
- This keeps complex models organized while maintaining Familia's clean, declarative style.
223
+ These approaches keep complex models organized while maintaining Familia's clean, declarative style. For detailed migration information, see the [migration guides](docs/migrating/).
205
224
 
206
225
  ## AI Development Assistance
207
226
 
@@ -4,10 +4,10 @@ Familia provides a comprehensive time utilities refinement that adds convenient
4
4
 
5
5
  ## Overview
6
6
 
7
- The `Familia::Refinements::TimeUtils` module extends Ruby's built-in classes with intuitive time manipulation methods:
7
+ The `Familia::Refinements::TimeLiterals` module extends Ruby's built-in classes with intuitive time manipulation methods:
8
8
 
9
9
  ```ruby
10
- using Familia::Refinements::TimeUtils
10
+ using Familia::Refinements::TimeLiterals
11
11
 
12
12
  2.hours #=> 7200 (seconds)
13
13
  "30m".in_seconds #=> 1800
@@ -40,7 +40,7 @@ This prevents subtle bugs where `12.months` and `1.year` would differ by ~5.8 ho
40
40
  ### Converting Numbers to Time Units
41
41
 
42
42
  ```ruby
43
- using Familia::Refinements::TimeUtils
43
+ using Familia::Refinements::TimeLiterals
44
44
 
45
45
  # Singular and plural forms work identically
46
46
  1.second #=> 1
@@ -140,7 +140,7 @@ timestamp.within?(1.hour) #=> false
140
140
  ### Cache Expiration
141
141
 
142
142
  ```ruby
143
- using Familia::Refinements::TimeUtils
143
+ using Familia::Refinements::TimeLiterals
144
144
 
145
145
  class CacheEntry < Familia::Horreum
146
146
  field :data
@@ -1,320 +1,86 @@
1
1
  # Migrating Guide: v2.0.0-pre13
2
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.
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
4
 
5
- ## Feature-Specific Autoloading System
5
+ ## Feature Autoloading System
6
6
 
7
- ### Overview
7
+ ### What Changed
8
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.
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
10
 
11
- ### Basic Usage
11
+ ### Basic Migration
12
12
 
13
- #### Before (Manual Configuration)
13
+ #### Before
14
14
  ```ruby
15
15
  # app/models/user.rb
16
16
  class User < Familia::Horreum
17
17
  field :name, :email, :password
18
18
 
19
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
20
+ safe_dump_fields :name, :email # Configuration mixed with model
22
21
  end
23
22
  ```
24
23
 
25
- #### After (Automatic Discovery)
24
+ #### After
26
25
  ```ruby
27
26
  # app/models/user.rb - Clean model definition
28
27
  class User < Familia::Horreum
29
28
  field :name, :email, :password
30
- feature :safe_dump # Triggers autoloading
29
+ feature :safe_dump # Configuration auto-loaded
31
30
  end
32
31
 
33
- # app/models/user/safe_dump_extensions.rb - Automatically loaded
32
+ # app/models/user/safe_dump_extensions.rb - Automatically discovered
34
33
  class User
35
- safe_dump_fields :name, :email # password excluded for security
34
+ safe_dump_fields :name, :email
36
35
  end
37
36
  ```
38
37
 
39
- ### File Naming Conventions
40
-
41
- The autoloading system follows these patterns for discovering extension files:
38
+ ### File Naming Convention
42
39
 
43
- #### Pattern: `{model_name}/{feature_name}_*.rb`
40
+ Extension files follow the pattern: `{model_name}/{feature_name}_*.rb`
44
41
 
45
- **Example Structures:**
46
42
  ```
47
43
  app/models/
48
- ├── user.rb # Main model
44
+ ├── user.rb
49
45
  ├── 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
- ]
46
+ │ ├── safe_dump_extensions.rb # SafeDump configuration
47
+ └── expiration_config.rb # Expiration settings
170
48
  ```
171
49
 
172
50
  ### Migration Steps
173
51
 
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
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
190
55
 
191
- Clean up your main model files:
56
+ ### Debugging
192
57
 
58
+ Enable debug output to troubleshoot autoloading:
193
59
  ```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
60
+ ENV['FAMILIA_DEBUG'] = '1' # Shows discovered and loaded files
233
61
  ```
234
62
 
235
- ### Benefits of the New System
63
+ Common issues:
64
+ - Files must follow `{feature_name}_*.rb` naming pattern
65
+ - Extension files should reopen the same class as your model
236
66
 
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
67
+ ## Architecture
241
68
 
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
69
+ The Feature Autoloading System consists of two key components:
246
70
 
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
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
251
76
 
252
- ### Debugging Autoloading
253
-
254
- #### Enable Debug Output
255
-
256
- ```ruby
257
- # In your application initialization
258
- ENV['FAMILIA_DEBUG'] = '1'
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
259
82
 
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
- ```
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.
318
84
 
319
85
  ## New Capabilities
320
86
 
@@ -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.
@@ -273,7 +273,7 @@ puts "LegacyModel fields after set_safe_dump_fields: #{LegacyModel.safe_dump_fie
273
273
  puts
274
274
  puts '=== Cleaning up test data ==='
275
275
  [User, Product, Order, Address, Customer, LegacyModel].each do |klass|
276
- klass.redis.del(klass.redis.keys("#{klass.name.downcase}:*"))
276
+ klass.dbclient.del(klass.dbclient.keys("#{klass.name.downcase}:*"))
277
277
  rescue StandardError => e
278
278
  puts "Error cleaning #{klass}: #{e.message}"
279
279
  end
data/lib/familia/base.rb CHANGED
@@ -15,7 +15,7 @@ module Familia
15
15
  #
16
16
  module Base
17
17
 
18
- using Familia::Refinements::TimeUtils
18
+ using Familia::Refinements::TimeLiterals
19
19
 
20
20
  @features_available = nil
21
21
  @feature_definitions = nil
@@ -16,7 +16,7 @@ module Familia
16
16
  include Familia::Base
17
17
  extend Familia::Features
18
18
 
19
- using Familia::Refinements::TimeUtils
19
+ using Familia::Refinements::TimeLiterals
20
20
 
21
21
  @registered_types = {}
22
22
  @valid_options = %i[class parent default_expiration default logical_database dbkey dbclient suffix prefix]
@@ -149,7 +149,7 @@ module Familia
149
149
  module Expiration
150
150
  @default_expiration = nil
151
151
 
152
- using Familia::Refinements::TimeUtils
152
+ using Familia::Refinements::TimeLiterals
153
153
 
154
154
  def self.included(base)
155
155
  Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
@@ -99,7 +99,7 @@ module Familia
99
99
  klass.define_method :"#{method_name}=" do |value|
100
100
  # Remove old mapping if extid is changing
101
101
  old_value = instance_variable_get(:"@#{field_name}")
102
- self.class.extid_lookup.del(old_value) if old_value && old_value != value && respond_to?(:identifier)
102
+ self.class.extid_lookup.remove_field(old_value) if old_value && old_value != value
103
103
 
104
104
  # Set the new value
105
105
  instance_variable_set(:"@#{field_name}", value)
@@ -153,7 +153,7 @@ module Familia
153
153
  find_by_id(primary_id)
154
154
  rescue Familia::NotFound
155
155
  # If the object was deleted but mapping wasn't cleaned up
156
- extid_lookup.del(extid)
156
+ extid_lookup.remove_field(extid)
157
157
  nil
158
158
  end
159
159
  end
@@ -236,7 +236,7 @@ module Familia
236
236
  def destroy!
237
237
  # Clean up extid mapping when object is destroyed
238
238
  current_extid = instance_variable_get(:@extid)
239
- self.class.extid_lookup.del(current_extid) if current_extid
239
+ self.class.extid_lookup.remove_field(current_extid) if current_extid
240
240
 
241
241
  super if defined?(super)
242
242
  end
@@ -246,7 +246,7 @@ module Familia
246
246
  #
247
247
  module Quantization
248
248
 
249
- using Familia::Refinements::TimeUtils
249
+ using Familia::Refinements::TimeLiterals
250
250
 
251
251
  def self.included(base)
252
252
  Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
@@ -29,7 +29,7 @@ module Familia
29
29
  class FieldType
30
30
  attr_reader :name, :options, :method_name, :fast_method_name, :on_conflict, :loggable
31
31
 
32
- using Familia::Refinements::TimeUtils
32
+ using Familia::Refinements::TimeLiterals
33
33
 
34
34
  # Initialize a new field type
35
35
  #
@@ -31,7 +31,7 @@ module Familia
31
31
  include Familia::Horreum::Core
32
32
  include Familia::Horreum::Settings
33
33
 
34
- using Familia::Refinements::TimeUtils
34
+ using Familia::Refinements::TimeLiterals
35
35
 
36
36
  # Singleton Class Context
37
37
  #
@@ -1,10 +1,41 @@
1
- # lib/familia/refinements/time_utils.rb
1
+ # lib/familia/refinements/time_literals.rb
2
2
 
3
3
  module Familia
4
4
  module Refinements
5
5
 
6
- # Familia::Refinements::TimeUtils
7
- module TimeUtils
6
+ # Familia::Refinements::TimeLiterals
7
+ #
8
+ # This module provides a set of refinements for `Numeric` and `String` to
9
+ # enable readable and expressive time duration and timestamp manipulation.
10
+ #
11
+ # The name "TimeLiterals" reflects its core purpose: to allow us to treat
12
+ # numeric values directly as "literals" of time units (e.g., `5.minutes`,
13
+ # `1.day`). It extends this concept to include conversions between these
14
+ # literal time quantities, parsing string representations of time
15
+ # durations, and performing common timestamp-based calculations
16
+ # in an intuitive manner.
17
+ #
18
+ # @example Expressing durations
19
+ # 5.minutes.ago #=> A Time object 5 minutes in the past
20
+ # 1.day.from_now #=> A Time object 1 day in the future
21
+ # (2.5).hours #=> 9000.0 (seconds)
22
+ #
23
+ # @example Converting between units
24
+ # 3600.in_hours #=> 1.0
25
+ # 86400.in_days #=> 1.0
26
+ #
27
+ # @example Parsing string durations
28
+ # "30m".in_seconds #=> 1800.0
29
+ # "2.5h".in_seconds #=> 9000.0
30
+ #
31
+ # @example Timestamp calculations
32
+ # timestamp = 2.days.ago.to_i
33
+ # timestamp.days_old #=> ~2.0
34
+ # timestamp.older_than?(1.day) #=> true
35
+ #
36
+ # @note `to_bytes` also lives here until we find it a better home!
37
+ #
38
+ module TimeLiterals
8
39
  # Time unit constants
9
40
  PER_MICROSECOND = 0.000001
10
41
  PER_MILLISECOND = 0.001
@@ -65,7 +96,7 @@ module Familia
65
96
  alias_method :month, :months
66
97
  alias_method :year, :years
67
98
 
68
- # Fun aliases
99
+ # Shortest aliases
69
100
  alias_method :ms, :milliseconds
70
101
  alias_method :μs, :microseconds
71
102
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  require_relative 'refinements/logger_trace'
4
4
  require_relative 'refinements/snake_case'
5
- require_relative 'refinements/time_utils'
5
+ require_relative 'refinements/time_literals'
data/lib/familia/utils.rb CHANGED
@@ -6,7 +6,7 @@ module Familia
6
6
  #
7
7
  module Utils
8
8
 
9
- using Familia::Refinements::TimeUtils
9
+ using Familia::Refinements::TimeLiterals
10
10
 
11
11
  # Joins array elements with Familia delimiter
12
12
  # @param val [Array] elements to join
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Familia
4
4
  # Version information for the Familia
5
- VERSION = '2.0.0.pre13'.freeze unless defined?(Familia::VERSION)
5
+ VERSION = '2.0.0.pre14'.freeze unless defined?(Familia::VERSION)
6
6
  end
@@ -1,7 +1,7 @@
1
1
  require_relative '../helpers/test_helpers'
2
2
 
3
3
  module RefinedContext
4
- using Familia::Refinements::TimeUtils
4
+ using Familia::Refinements::TimeLiterals
5
5
 
6
6
  # This helper evaluates code within the refined context using eval.
7
7
  # This works because eval executes the code as if it were written
@@ -1,7 +1,7 @@
1
1
  require_relative '../helpers/test_helpers'
2
2
 
3
3
  module RefinedContext
4
- using Familia::Refinements::TimeUtils
4
+ using Familia::Refinements::TimeLiterals
5
5
 
6
6
  def self.eval_in_refined_context(code)
7
7
  eval(code)
@@ -12,7 +12,7 @@ module RefinedContext
12
12
  end
13
13
  end
14
14
 
15
- # Test TimeUtils refinement
15
+ # Test TimeLiterals refinement
16
16
 
17
17
  ## Numeric#months - convert number to months in seconds
18
18
  result = RefinedContext.eval_in_refined_context("1.month")
@@ -34,7 +34,7 @@ RefinedContext.instance_eval_in_refined_context("2629746.in_months")
34
34
  #=> 1.0
35
35
 
36
36
  ## Numeric#in_years - convert seconds to years
37
- result = RefinedContext.eval_in_refined_context("#{Familia::Refinements::TimeUtils::PER_YEAR}.in_years")
37
+ result = RefinedContext.eval_in_refined_context("#{Familia::Refinements::TimeLiterals::PER_YEAR}.in_years")
38
38
  result.round(1)
39
39
  #=> 1.0
40
40
 
@@ -52,75 +52,75 @@ result.round(0)
52
52
  #=> 31556952.0
53
53
 
54
54
  ## Numeric#age_in - calculate age in months from timestamp (approximately 1 month ago)
55
- timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_MONTH
55
+ timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_MONTH
56
56
  result = RefinedContext.eval_in_refined_context("#{timestamp}.age_in(:months)")
57
57
  (result - 1.0).abs < 0.01
58
58
  #=> true
59
59
 
60
60
  ## Numeric#age_in - calculate age in years from timestamp (approximately 1 year ago)
61
- timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_YEAR
61
+ timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_YEAR
62
62
  result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.age_in(:years)")
63
63
  (result - 1.0).abs < 0.01
64
64
  #=> true
65
65
 
66
66
  ## Numeric#months_old - convenience method for age_in(:months)
67
- timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_MONTH
67
+ timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_MONTH
68
68
  result = RefinedContext.eval_in_refined_context("#{timestamp}.months_old")
69
69
  (result - 1.0).abs < 0.01
70
70
  #=> true
71
71
 
72
72
  ## Numeric#years_old - convenience method for age_in(:years)
73
- timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_YEAR
73
+ timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_YEAR
74
74
  result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.years_old")
75
75
  (result - 1.0).abs < 0.01
76
76
  #=> true
77
77
 
78
78
  ## Numeric#months_old - should NOT return seconds (the original bug)
79
- timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_MONTH
79
+ timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_MONTH
80
80
  result = RefinedContext.eval_in_refined_context("#{timestamp}.months_old")
81
81
  result.between?(0.9, 1.1) # Should be ~1 month, not millions of seconds
82
82
  #=> true
83
83
 
84
84
  ## Numeric#years_old - should NOT return seconds (the original bug)
85
- timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_YEAR
85
+ timestamp = Time.now.to_f - Familia::Refinements::TimeLiterals::PER_YEAR
86
86
  result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.years_old")
87
87
  result.between?(0.9, 1.1) # Should be ~1 year, not millions of seconds
88
88
  #=> true
89
89
 
90
90
  ## age_in with from_time parameter - months
91
- past_time = Time.now - (2 * Familia::Refinements::TimeUtils::PER_MONTH) # 2 months ago
92
- from_time = Time.now - Familia::Refinements::TimeUtils::PER_MONTH # 1 month ago
91
+ past_time = Time.now - (2 * Familia::Refinements::TimeLiterals::PER_MONTH) # 2 months ago
92
+ from_time = Time.now - Familia::Refinements::TimeLiterals::PER_MONTH # 1 month ago
93
93
  result = RefinedContext.eval_in_refined_context("#{past_time.to_f}.age_in(:months, #{from_time.to_f})")
94
94
  (result - 1.0).abs < 0.01
95
95
  #=> true
96
96
 
97
97
  ## age_in with from_time parameter - years
98
- past_time = Time.now - (2 * Familia::Refinements::TimeUtils::PER_YEAR) # 2 years ago
99
- from_time = Time.now - Familia::Refinements::TimeUtils::PER_YEAR # 1 year ago
98
+ past_time = Time.now - (2 * Familia::Refinements::TimeLiterals::PER_YEAR) # 2 years ago
99
+ from_time = Time.now - Familia::Refinements::TimeLiterals::PER_YEAR # 1 year ago
100
100
  result = RefinedContext.instance_eval_in_refined_context("#{past_time.to_f}.age_in(:years, #{from_time.to_f})")
101
101
  (result - 1.0).abs < 0.01
102
102
  #=> true
103
103
 
104
104
  ## Verify month constant is approximately correct (30.437 days)
105
105
  expected_seconds_per_month = 30.437 * 24 * 60 * 60
106
- Familia::Refinements::TimeUtils::PER_MONTH.round(0)
106
+ Familia::Refinements::TimeLiterals::PER_MONTH.round(0)
107
107
  #=> 2629746.0
108
108
 
109
109
  ## Verify year constant (365.2425 days - Gregorian year)
110
110
  expected_seconds_per_year = 365.2425 * 24 * 60 * 60
111
- Familia::Refinements::TimeUtils::PER_YEAR.round(0)
111
+ Familia::Refinements::TimeLiterals::PER_YEAR.round(0)
112
112
  #=> 31556952.0
113
113
 
114
114
  ## UNIT_METHODS contains months mapping
115
- Familia::Refinements::TimeUtils::UNIT_METHODS['months']
115
+ Familia::Refinements::TimeLiterals::UNIT_METHODS['months']
116
116
  #=> :months
117
117
 
118
118
  ## UNIT_METHODS contains mo mapping
119
- Familia::Refinements::TimeUtils::UNIT_METHODS['mo']
119
+ Familia::Refinements::TimeLiterals::UNIT_METHODS['mo']
120
120
  #=> :months
121
121
 
122
122
  ## UNIT_METHODS contains month mapping
123
- Familia::Refinements::TimeUtils::UNIT_METHODS['month']
123
+ Familia::Refinements::TimeLiterals::UNIT_METHODS['month']
124
124
  #=> :months
125
125
 
126
126
  ## Calendar consistency - 12 months equals 1 year (fix for inconsistency issue)
@@ -198,3 +198,29 @@ ExternalIdTest.extid_lookup[@test_obj.extid]
198
198
 
199
199
  # Cleanup test objects
200
200
  @test_obj.destroy! rescue nil
201
+
202
+ ## Test 1: Changing extid value (should work after bug fix)
203
+ bug_test_obj = ExternalIdTest.new(id: 'bug_test', name: 'Bug Test Object')
204
+ bug_test_obj.save
205
+ bug_test_obj.extid = 'new_extid_value'
206
+ bug_test_obj.extid
207
+ #=> "new_extid_value"
208
+
209
+ ## Test 2: find_by_extid with deleted object (should work after bug fix)
210
+ delete_test_obj = ExternalIdTest.new(id: 'delete_test', name: 'Delete Test')
211
+ delete_test_obj.save
212
+ test_extid = delete_test_obj.extid
213
+ # Delete the object directly from Redis to simulate cleanup scenario
214
+ ExternalIdTest.dbclient.del(delete_test_obj.dbkey)
215
+ # Now try to find by extid - this should clean up mapping and return nil
216
+ ExternalIdTest.find_by_extid(test_extid)
217
+ #=> nil
218
+
219
+ ## Test 3: destroy! method (should work after bug fix)
220
+ destroy_test_obj = ExternalIdTest.new(id: 'destroy_test', name: 'Destroy Test')
221
+ destroy_test_obj.save
222
+ destroy_extid = destroy_test_obj.extid
223
+ destroy_test_obj.destroy!
224
+ # Verify mapping was cleaned up
225
+ ExternalIdTest.extid_lookup.key?(destroy_extid)
226
+ #=> false
@@ -12,7 +12,7 @@ Familia.enable_database_logging = true
12
12
  Familia.enable_database_counter = true
13
13
 
14
14
  class Bone < Familia::Horreum
15
- using Familia::Refinements::TimeUtils
15
+ using Familia::Refinements::TimeLiterals
16
16
 
17
17
  identifier_field :token
18
18
  field :token
@@ -42,7 +42,7 @@ end
42
42
 
43
43
  class Customer < Familia::Horreum
44
44
 
45
- using Familia::Refinements::TimeUtils
45
+ using Familia::Refinements::TimeLiterals
46
46
 
47
47
  logical_database 15 # Use something other than the default DB
48
48
  default_expiration 5.years
@@ -106,7 +106,7 @@ end
106
106
  @c.custid = 'd@example.com'
107
107
 
108
108
  class Session < Familia::Horreum
109
- using Familia::Refinements::TimeUtils
109
+ using Familia::Refinements::TimeLiterals
110
110
 
111
111
  logical_database 14 # don't use Onetime's default DB
112
112
  default_expiration 180.minutes
@@ -130,7 +130,7 @@ end
130
130
  @s = Session.new
131
131
 
132
132
  class CustomDomain < Familia::Horreum
133
- using Familia::Refinements::TimeUtils
133
+ using Familia::Refinements::TimeLiterals
134
134
 
135
135
  feature :expiration
136
136
 
@@ -161,7 +161,7 @@ end
161
161
  @d.custid = @c.custid
162
162
 
163
163
  class Limiter < Familia::Horreum
164
- using Familia::Refinements::TimeUtils
164
+ using Familia::Refinements::TimeLiterals
165
165
 
166
166
  feature :expiration
167
167
  feature :quantization
@@ -243,7 +243,7 @@ end
243
243
 
244
244
  # Helper module for testing refinements in tryouts
245
245
  module RefinedContext
246
- using Familia::Refinements::TimeUtils
246
+ using Familia::Refinements::TimeLiterals
247
247
 
248
248
  def self.eval_in_refined_context(code)
249
249
  eval(code)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: familia
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre13
4
+ version: 2.0.0.pre14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -180,6 +180,7 @@ files:
180
180
  - docs/migrating/v2.0.0-pre11.md
181
181
  - docs/migrating/v2.0.0-pre12.md
182
182
  - docs/migrating/v2.0.0-pre13.md
183
+ - docs/migrating/v2.0.0-pre14.md
183
184
  - docs/migrating/v2.0.0-pre5.md
184
185
  - docs/migrating/v2.0.0-pre6.md
185
186
  - docs/migrating/v2.0.0-pre7.md
@@ -255,7 +256,7 @@ files:
255
256
  - lib/familia/refinements.rb
256
257
  - lib/familia/refinements/logger_trace.rb
257
258
  - lib/familia/refinements/snake_case.rb
258
- - lib/familia/refinements/time_utils.rb
259
+ - lib/familia/refinements/time_literals.rb
259
260
  - lib/familia/secure_identifier.rb
260
261
  - lib/familia/settings.rb
261
262
  - lib/familia/utils.rb