map 6.6.0 → 8.0.0
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 +7 -0
- data/LICENSE +1 -1
- data/README.md +268 -0
- data/Rakefile +140 -64
- data/images/giraffe.jpeg +0 -0
- data/images/map.png +0 -0
- data/lib/map/_lib.rb +85 -0
- data/lib/map/ordering.rb +126 -0
- data/lib/map.rb +151 -121
- data/map.gemspec +21 -12
- data/specs/001-ordered-map-module/checklists/requirements.md +62 -0
- data/specs/001-ordered-map-module/data-model.md +250 -0
- data/specs/001-ordered-map-module/plan.md +139 -0
- data/specs/001-ordered-map-module/quickstart.md +430 -0
- data/specs/001-ordered-map-module/research.md +189 -0
- data/specs/001-ordered-map-module/spec.md +108 -0
- data/specs/001-ordered-map-module/tasks.md +264 -0
- data/test/map_test.rb +21 -57
- metadata +39 -23
- data/a.rb +0 -10
- data/lib/map/integrations/active_record.rb +0 -140
- data/lib/map/params.rb +0 -79
- data/lib/map/struct.rb +0 -52
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# Developer Quickstart: Conditional Ordered Map Module
|
|
2
|
+
|
|
3
|
+
**Feature**: Conditional Ordered Map Module
|
|
4
|
+
**Branch**: `001-ordered-map-module`
|
|
5
|
+
**Last Updated**: 2025-11-10
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
This guide helps developers understand and work with the conditional ordering module architecture in map.rb.
|
|
10
|
+
|
|
11
|
+
## What Changed?
|
|
12
|
+
|
|
13
|
+
### Before (All Ruby Versions)
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# lib/map.rb
|
|
17
|
+
class Map < Hash
|
|
18
|
+
def keys
|
|
19
|
+
@keys ||= [] # Always maintained for ordering
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def []=(key, val)
|
|
23
|
+
key, val = convert(key, val)
|
|
24
|
+
keys.push(key) unless has_key?(key) # Always track
|
|
25
|
+
__set__(key, val)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# ... 10 other methods using @keys ...
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Problem**: Ruby 1.9+ already maintains Hash ordering, so `@keys` is redundant (memory waste).
|
|
33
|
+
|
|
34
|
+
### After (Version-Aware)
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# lib/map.rb
|
|
38
|
+
class Map < Hash
|
|
39
|
+
# Core methods work for Ruby 1.9+ (no @keys needed)
|
|
40
|
+
|
|
41
|
+
# Conditionally include ordering module for legacy Ruby
|
|
42
|
+
if RUBY_VERSION < '1.9' || ENV['MAP_FORCE_ORDERING']
|
|
43
|
+
require_relative 'map/ordering'
|
|
44
|
+
include Map::Ordering
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# lib/map/ordering.rb
|
|
49
|
+
module Map::Ordering
|
|
50
|
+
def self.included(base)
|
|
51
|
+
base.class_eval do
|
|
52
|
+
def self.allocate
|
|
53
|
+
super.instance_eval do
|
|
54
|
+
@keys = []
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def keys
|
|
62
|
+
@keys ||= []
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def []=(key, val)
|
|
66
|
+
key, val = convert(key, val)
|
|
67
|
+
keys.push(key) unless has_key?(key)
|
|
68
|
+
__set__(key, val)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# ... other ordering methods ...
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Solution**: Ruby 1.9+ uses native Hash ordering, Ruby < 1.9 includes module for manual ordering.
|
|
76
|
+
|
|
77
|
+
## Architecture
|
|
78
|
+
|
|
79
|
+
### Component Diagram
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
┌─────────────────────────────────────────────────────┐
|
|
83
|
+
│ Map Class │
|
|
84
|
+
│ (extends Hash) │
|
|
85
|
+
│ │
|
|
86
|
+
│ Core methods (work on Ruby 1.9+ with native Hash)│
|
|
87
|
+
│ - convert_key, convert_value │
|
|
88
|
+
│ - initialize, update, merge │
|
|
89
|
+
│ - get, set (use Hash methods) │
|
|
90
|
+
└─────────────────────────────────────────────────────┘
|
|
91
|
+
△
|
|
92
|
+
│
|
|
93
|
+
│ conditionally includes
|
|
94
|
+
│ (if RUBY_VERSION < '1.9')
|
|
95
|
+
│
|
|
96
|
+
┌─────────────────────────────────────────────────────┐
|
|
97
|
+
│ Map::Ordering Module │
|
|
98
|
+
│ │
|
|
99
|
+
│ Ordering-specific methods (override Hash methods):│
|
|
100
|
+
│ - allocate (class method via included hook) │
|
|
101
|
+
│ - keys, values, first, last │
|
|
102
|
+
│ - each, each_key, each_value, each_with_index │
|
|
103
|
+
│ - []=, delete │
|
|
104
|
+
│ │
|
|
105
|
+
│ Uses @keys array to maintain insertion order │
|
|
106
|
+
└─────────────────────────────────────────────────────┘
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Decision Tree
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
Map class loaded
|
|
113
|
+
│
|
|
114
|
+
├─ Is RUBY_VERSION < '1.9'?
|
|
115
|
+
│ ├─ YES → Include Map::Ordering
|
|
116
|
+
│ │ └─ Use @keys array for ordering
|
|
117
|
+
│ └─ NO ──┐
|
|
118
|
+
│ │
|
|
119
|
+
└─ Is ENV['MAP_FORCE_ORDERING'] set?
|
|
120
|
+
├─ YES → Include Map::Ordering (testing mode)
|
|
121
|
+
│ └─ Use @keys array for ordering
|
|
122
|
+
└─ NO → No module inclusion
|
|
123
|
+
└─ Use native Hash ordering
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Development Workflow
|
|
127
|
+
|
|
128
|
+
### Setting Up Your Environment
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Clone and checkout feature branch
|
|
132
|
+
git clone https://github.com/ahoward/map.git
|
|
133
|
+
cd map
|
|
134
|
+
git checkout 001-ordered-map-module
|
|
135
|
+
|
|
136
|
+
# Check Ruby version
|
|
137
|
+
ruby --version
|
|
138
|
+
# Should be 3.0+ per current gemspec
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Running Tests
|
|
142
|
+
|
|
143
|
+
#### Normal Mode (Ruby 1.9+ behavior)
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Run all tests using native Hash ordering
|
|
147
|
+
rake test
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Expected:
|
|
151
|
+
- All tests pass
|
|
152
|
+
- Map instances have NO `@keys` instance variable
|
|
153
|
+
- Ordering works via Hash#keys, Hash#each, etc.
|
|
154
|
+
|
|
155
|
+
#### Forced Ordering Mode (Ruby < 1.9 simulation)
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Force inclusion of ordering module
|
|
159
|
+
MAP_FORCE_ORDERING=1 rake test
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Expected:
|
|
163
|
+
- All tests pass
|
|
164
|
+
- Map instances HAVE `@keys` instance variable
|
|
165
|
+
- Ordering works via `@keys` array tracking
|
|
166
|
+
|
|
167
|
+
### Verifying Both Paths
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# Quick verification script
|
|
171
|
+
# test/verify_ordering.rb
|
|
172
|
+
|
|
173
|
+
require 'map'
|
|
174
|
+
|
|
175
|
+
def test_ordering_module_state
|
|
176
|
+
map = Map[:a, 1, :b, 2, :c, 3]
|
|
177
|
+
|
|
178
|
+
puts "Ruby Version: #{RUBY_VERSION}"
|
|
179
|
+
puts "Force Ordering: #{ENV['MAP_FORCE_ORDERING']}"
|
|
180
|
+
puts "Has @keys: #{map.instance_variables.include?(:@keys)}"
|
|
181
|
+
puts "Keys: #{map.keys.inspect}"
|
|
182
|
+
puts "Values: #{map.values.inspect}"
|
|
183
|
+
puts "First: #{map.first.inspect}"
|
|
184
|
+
puts "Last: #{map.last.inspect}"
|
|
185
|
+
|
|
186
|
+
# Verify ordering is maintained
|
|
187
|
+
raise "Ordering broken!" unless map.keys == ['a', 'b', 'c']
|
|
188
|
+
raise "Values broken!" unless map.values == [1, 2, 3]
|
|
189
|
+
|
|
190
|
+
puts "✅ All checks passed!"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
test_ordering_module_state
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Run both ways:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Test without ordering module
|
|
200
|
+
ruby test/verify_ordering.rb
|
|
201
|
+
# Should show: Has @keys: false
|
|
202
|
+
|
|
203
|
+
# Test with ordering module
|
|
204
|
+
MAP_FORCE_ORDERING=1 ruby test/verify_ordering.rb
|
|
205
|
+
# Should show: Has @keys: true
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Code Organization
|
|
209
|
+
|
|
210
|
+
### File Structure
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
lib/map.rb # Main Map class
|
|
214
|
+
# - Contains conditional include logic
|
|
215
|
+
# - Methods work for Ruby 1.9+ native ordering
|
|
216
|
+
|
|
217
|
+
lib/map/ordering.rb # NEW: Ordering module
|
|
218
|
+
# - All @keys-dependent methods
|
|
219
|
+
# - Included only when needed
|
|
220
|
+
|
|
221
|
+
lib/map/_lib.rb # Unchanged: utility methods
|
|
222
|
+
lib/map/options.rb # Unchanged: Map::Options subclass
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### What Goes Where?
|
|
226
|
+
|
|
227
|
+
**lib/map.rb** (Methods that work with native Hash ordering):
|
|
228
|
+
- `convert_key` - Key normalization (string/symbol)
|
|
229
|
+
- `convert_value` - Recursive Map conversion
|
|
230
|
+
- `initialize`, `update`, `merge` - Constructors and merging
|
|
231
|
+
- `get` (fetch with nested paths) - Doesn't depend on ordering
|
|
232
|
+
- `method_missing` - Attribute-style access
|
|
233
|
+
|
|
234
|
+
**lib/map/ordering.rb** (Methods that need `@keys` array):
|
|
235
|
+
- `allocate` - Initialize `@keys = []`
|
|
236
|
+
- `keys` - Return `@keys`
|
|
237
|
+
- `values` - Iterate `@keys` to build values array
|
|
238
|
+
- `[]=`, `delete` - Track keys in `@keys`
|
|
239
|
+
- `each`, `each_key`, `each_value`, `each_with_index` - Iterate via `@keys`
|
|
240
|
+
- `first`, `last` - Use `@keys.first`, `@keys.last`
|
|
241
|
+
|
|
242
|
+
## Common Development Tasks
|
|
243
|
+
|
|
244
|
+
### Adding a New Method to Map
|
|
245
|
+
|
|
246
|
+
**Question**: Should it go in `Map` or `Map::Ordering`?
|
|
247
|
+
|
|
248
|
+
**Decision tree**:
|
|
249
|
+
1. Does it depend on key ordering?
|
|
250
|
+
- NO → Add to `lib/map.rb`
|
|
251
|
+
- YES → Continue to #2
|
|
252
|
+
2. Does it need `@keys` array specifically?
|
|
253
|
+
- NO → Add to `lib/map.rb` (can use Hash methods)
|
|
254
|
+
- YES → Add to `lib/map/ordering.rb`
|
|
255
|
+
|
|
256
|
+
**Examples**:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
# Example 1: New method that doesn't care about ordering
|
|
260
|
+
# Add to lib/map.rb
|
|
261
|
+
def has_nested?(path)
|
|
262
|
+
get(path) != nil
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Example 2: New method that needs ordering but can use Hash methods
|
|
266
|
+
# Add to lib/map.rb (works on Ruby 1.9+)
|
|
267
|
+
def second
|
|
268
|
+
key = keys[1] # Hash#keys is ordered on 1.9+
|
|
269
|
+
[key, self[key]]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Example 3: New method that explicitly needs @keys
|
|
273
|
+
# Add to lib/map/ordering.rb
|
|
274
|
+
def reverse_each
|
|
275
|
+
@keys.reverse.each { |key| yield(key, self[key]) }
|
|
276
|
+
end
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Modifying an Ordering Method
|
|
280
|
+
|
|
281
|
+
If you need to modify a method in `Map::Ordering`:
|
|
282
|
+
|
|
283
|
+
1. Edit `lib/map/ordering.rb`
|
|
284
|
+
2. Test both paths:
|
|
285
|
+
```bash
|
|
286
|
+
# Ruby 1.9+ path (should still work without your change)
|
|
287
|
+
rake test
|
|
288
|
+
|
|
289
|
+
# Legacy path (should work with your change)
|
|
290
|
+
MAP_FORCE_ORDERING=1 rake test
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Debugging Ordering Issues
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
# Add to your test or debugging session
|
|
297
|
+
map = Map[:a, 1, :b, 2]
|
|
298
|
+
|
|
299
|
+
# Check which path is being used
|
|
300
|
+
if map.instance_variables.include?(:@keys)
|
|
301
|
+
puts "Using Map::Ordering module"
|
|
302
|
+
puts "@keys = #{map.instance_variable_get(:@keys).inspect}"
|
|
303
|
+
else
|
|
304
|
+
puts "Using native Hash ordering"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Verify ordering consistency
|
|
308
|
+
puts "Keys order: #{map.keys.inspect}"
|
|
309
|
+
puts "Each order: #{map.map { |k, v| k }.inspect}"
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Testing Strategy
|
|
313
|
+
|
|
314
|
+
### Test Categories
|
|
315
|
+
|
|
316
|
+
1. **Functional tests** (existing in `test/map_test.rb`):
|
|
317
|
+
- No modifications needed
|
|
318
|
+
- Run in both modes to verify identical behavior
|
|
319
|
+
|
|
320
|
+
2. **Internal state tests** (NEW tests to add):
|
|
321
|
+
- Verify `@keys` presence/absence based on mode
|
|
322
|
+
- Check memory footprint differences
|
|
323
|
+
- Validate synchronization on Ruby < 1.9 mode
|
|
324
|
+
|
|
325
|
+
3. **Performance benchmarks** (optional):
|
|
326
|
+
- Compare iteration speed with/without module
|
|
327
|
+
- Measure memory usage with/without `@keys`
|
|
328
|
+
|
|
329
|
+
### Writing New Tests
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
# In test/map_test.rb
|
|
333
|
+
|
|
334
|
+
Testing Map do
|
|
335
|
+
testing 'conditional ordering module inclusion' do
|
|
336
|
+
map = Map[:a, 1, :b, 2, :c, 3]
|
|
337
|
+
|
|
338
|
+
# Test behavior (should work in both modes)
|
|
339
|
+
assert{ map.keys == ['a', 'b', 'c'] }
|
|
340
|
+
assert{ map.values == [1, 2, 3] }
|
|
341
|
+
|
|
342
|
+
# Test internal state (mode-dependent)
|
|
343
|
+
if ENV['MAP_FORCE_ORDERING'] || RUBY_VERSION < '1.9'
|
|
344
|
+
assert{ map.instance_variables.include?(:@keys) }
|
|
345
|
+
else
|
|
346
|
+
assert{ !map.instance_variables.include?(:@keys) }
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Troubleshooting
|
|
353
|
+
|
|
354
|
+
### Problem: Tests pass normally but fail with MAP_FORCE_ORDERING=1
|
|
355
|
+
|
|
356
|
+
**Cause**: Likely a method is missing from `Map::Ordering` module
|
|
357
|
+
|
|
358
|
+
**Solution**:
|
|
359
|
+
1. Identify which test is failing
|
|
360
|
+
2. Check if the failing method uses `@keys`
|
|
361
|
+
3. Add missing method to `lib/map/ordering.rb`
|
|
362
|
+
|
|
363
|
+
### Problem: Memory usage isn't reduced on Ruby 3.x
|
|
364
|
+
|
|
365
|
+
**Cause**: Module is being included when it shouldn't be
|
|
366
|
+
|
|
367
|
+
**Solution**:
|
|
368
|
+
```ruby
|
|
369
|
+
# Verify module isn't included
|
|
370
|
+
map = Map.new
|
|
371
|
+
puts map.instance_variables.inspect
|
|
372
|
+
# Should be: [] (empty, no @keys)
|
|
373
|
+
|
|
374
|
+
# Check if module was mistakenly included
|
|
375
|
+
puts Map.ancestors.include?(Map::Ordering)
|
|
376
|
+
# Should be: false (on Ruby 1.9+)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Problem: Subclass behavior differs from Map
|
|
380
|
+
|
|
381
|
+
**Cause**: Ordering module methods not inherited properly
|
|
382
|
+
|
|
383
|
+
**Solution**:
|
|
384
|
+
```ruby
|
|
385
|
+
# Verify inheritance chain
|
|
386
|
+
puts Map::Options.ancestors
|
|
387
|
+
# Should include Map::Ordering if Map does
|
|
388
|
+
|
|
389
|
+
# Test with subclass
|
|
390
|
+
opts = Map::Options[:a, 1, :b, 2]
|
|
391
|
+
puts opts.keys.inspect
|
|
392
|
+
# Should be: ['a', 'b'] (ordered)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Performance Considerations
|
|
396
|
+
|
|
397
|
+
### Memory Impact
|
|
398
|
+
|
|
399
|
+
| Scenario | Before | After | Savings |
|
|
400
|
+
|----------|--------|-------|---------|
|
|
401
|
+
| Ruby 1.8.x | `@keys` array | `@keys` array | 0% (unchanged) |
|
|
402
|
+
| Ruby 1.9-2.x | `@keys` array | No `@keys` | ~25% per instance |
|
|
403
|
+
| Ruby 3.x | `@keys` array | No `@keys` | ~25% per instance |
|
|
404
|
+
|
|
405
|
+
### CPU Impact
|
|
406
|
+
|
|
407
|
+
| Operation | Ruby < 1.9 (with module) | Ruby >= 1.9 (native) | Impact |
|
|
408
|
+
|-----------|-------------------------|----------------------|---------|
|
|
409
|
+
| Insert | Track in `@keys` + Hash | Hash only | Negligible (one less array operation) |
|
|
410
|
+
| Iterate | Iterate `@keys` array | Iterate Hash | Negligible (both O(n)) |
|
|
411
|
+
| Delete | Remove from `@keys` + Hash | Hash only | Minor (array search removed) |
|
|
412
|
+
|
|
413
|
+
**Expected**: No measurable performance difference (< 5% regression per success criteria).
|
|
414
|
+
|
|
415
|
+
## Next Steps
|
|
416
|
+
|
|
417
|
+
After reviewing this quickstart:
|
|
418
|
+
|
|
419
|
+
1. Read the [data-model.md](data-model.md) for detailed state diagrams
|
|
420
|
+
2. Review [research.md](research.md) for technical decisions
|
|
421
|
+
3. Proceed to implementation using the `/speckit.tasks` command to generate implementation tasks
|
|
422
|
+
|
|
423
|
+
## Questions?
|
|
424
|
+
|
|
425
|
+
If you encounter issues or have questions:
|
|
426
|
+
|
|
427
|
+
1. Check the [data-model.md](data-model.md) for state model details
|
|
428
|
+
2. Review [research.md](research.md) for decision rationale
|
|
429
|
+
3. Examine existing tests in `test/map_test.rb` for examples
|
|
430
|
+
4. Run verification script to diagnose which path is active
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Research: Conditional Ordered Map Module
|
|
2
|
+
|
|
3
|
+
**Date**: 2025-11-10
|
|
4
|
+
**Feature**: Conditional Ordered Map Module
|
|
5
|
+
**Phase**: 0 - Research & Technical Decisions
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
This document captures research findings and technical decisions for extracting ordering-related code into a conditional module for the map.rb library.
|
|
10
|
+
|
|
11
|
+
## Key Research Areas
|
|
12
|
+
|
|
13
|
+
### 1. Ruby Hash Ordering History
|
|
14
|
+
|
|
15
|
+
**Decision**: Use Ruby version 1.9.0 as the cutoff for conditional module inclusion
|
|
16
|
+
|
|
17
|
+
**Rationale**:
|
|
18
|
+
- Ruby 1.8.x: Hash was **unordered** - iteration order was unpredictable and based on internal hash table structure
|
|
19
|
+
- Ruby 1.9.0+: Hash became **ordered by insertion** - maintains the order in which keys were added
|
|
20
|
+
- This change was formalized in Ruby 1.9 and has been maintained in all subsequent versions (2.x, 3.x)
|
|
21
|
+
- The change applies to all major Ruby implementations (MRI, JRuby, Rubinius, TruffleRuby) at their respective 1.9+ version equivalents
|
|
22
|
+
|
|
23
|
+
**Alternatives considered**:
|
|
24
|
+
- Using RUBY_VERSION string comparison: Rejected due to complexity with alternative Ruby implementations
|
|
25
|
+
- Using feature detection (checking if Hash preserves order): Rejected as overly complex for a well-known version boundary
|
|
26
|
+
- Using Ruby 2.0 as cutoff: Rejected as 1.9.x still had significant usage when map.rb was established
|
|
27
|
+
|
|
28
|
+
**References**:
|
|
29
|
+
- Ruby 1.9 release notes documented the Hash ordering change
|
|
30
|
+
- This is a well-established Ruby language feature with no ambiguity across implementations
|
|
31
|
+
|
|
32
|
+
### 2. Module Inclusion Strategy
|
|
33
|
+
|
|
34
|
+
**Decision**: Use conditional `include` at class definition time based on RUBY_VERSION constant
|
|
35
|
+
|
|
36
|
+
**Rationale**:
|
|
37
|
+
- Simple, explicit, and evaluated once at load time (zero runtime overhead)
|
|
38
|
+
- Ruby's RUBY_VERSION constant is available in all Ruby versions
|
|
39
|
+
- Module inclusion allows clean separation of ordering-specific methods
|
|
40
|
+
- Methods in the module override Hash superclass methods when included
|
|
41
|
+
|
|
42
|
+
**Implementation approach**:
|
|
43
|
+
```ruby
|
|
44
|
+
class Map < Hash
|
|
45
|
+
# ... existing code ...
|
|
46
|
+
|
|
47
|
+
# Conditionally include ordering module for Ruby < 1.9
|
|
48
|
+
if RUBY_VERSION < '1.9'
|
|
49
|
+
require_relative 'map/ordering'
|
|
50
|
+
include Map::Ordering
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Alternatives considered**:
|
|
56
|
+
- Runtime method checks: Rejected due to performance overhead on every method call
|
|
57
|
+
- Monkey-patching after class definition: Rejected as less maintainable and harder to test
|
|
58
|
+
- Separate Map class for each version: Rejected as it duplicates code and breaks the "single class" design
|
|
59
|
+
|
|
60
|
+
### 3. Methods to Extract into Ordering Module
|
|
61
|
+
|
|
62
|
+
**Decision**: Extract all methods that directly depend on the `@keys` instance variable
|
|
63
|
+
|
|
64
|
+
**Methods identified from lib/map.rb analysis**:
|
|
65
|
+
1. `Map.allocate` - Initializes `@keys = []`
|
|
66
|
+
2. `keys` - Returns `@keys` array
|
|
67
|
+
3. `[]=` (store) - Tracks key in `@keys.push(key)` on new insertions
|
|
68
|
+
4. `values` - Iterates over `@keys` to build values array
|
|
69
|
+
5. `first` - Returns `[keys.first, self[keys.first]]`
|
|
70
|
+
6. `last` - Returns `[keys.last, self[keys.last]]`
|
|
71
|
+
7. `each` (and `each_pair`) - Iterates over `@keys` array
|
|
72
|
+
8. `each_key` - Iterates over `@keys` array
|
|
73
|
+
9. `each_value` - Iterates over `@keys` to access values
|
|
74
|
+
10. `each_with_index` - Iterates over `@keys.each_with_index`
|
|
75
|
+
11. `delete` - Removes key from `@keys` array
|
|
76
|
+
|
|
77
|
+
**Rationale**:
|
|
78
|
+
- These methods explicitly use `@keys` for ordering guarantees
|
|
79
|
+
- In Ruby 1.9+, Hash superclass provides ordered versions of these methods
|
|
80
|
+
- Extracting to module allows clean fallback to Hash methods when module not included
|
|
81
|
+
|
|
82
|
+
**Alternatives considered**:
|
|
83
|
+
- Extracting only iterator methods: Rejected as incomplete - would still need `@keys` for `first`/`last`
|
|
84
|
+
- Keeping `@keys` for backward compatibility: Rejected as it defeats the memory optimization goal
|
|
85
|
+
|
|
86
|
+
### 4. Testing Strategy (KISS Approach)
|
|
87
|
+
|
|
88
|
+
**Decision**: Use environment variable `MAP_FORCE_ORDERING=1` to force inclusion of ordering module regardless of Ruby version
|
|
89
|
+
|
|
90
|
+
**Rationale**:
|
|
91
|
+
- Simple: Single environment variable toggle
|
|
92
|
+
- KISS: No complex test matrix infrastructure needed
|
|
93
|
+
- Effective: Allows testing both code paths on any Ruby version
|
|
94
|
+
- Non-invasive: Doesn't require test suite modifications
|
|
95
|
+
|
|
96
|
+
**Implementation**:
|
|
97
|
+
```ruby
|
|
98
|
+
# In lib/map.rb
|
|
99
|
+
if RUBY_VERSION < '1.9' || ENV['MAP_FORCE_ORDERING']
|
|
100
|
+
require_relative 'map/ordering'
|
|
101
|
+
include Map::Ordering
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Test execution**:
|
|
106
|
+
```bash
|
|
107
|
+
# Test normal path (Ruby 1.9+ without ordering module)
|
|
108
|
+
rake test
|
|
109
|
+
|
|
110
|
+
# Test ordering module path (forced inclusion)
|
|
111
|
+
MAP_FORCE_ORDERING=1 rake test
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Alternatives considered**:
|
|
115
|
+
- Separate test files for each path: Rejected as it duplicates test code
|
|
116
|
+
- Docker containers with multiple Ruby versions: Rejected as over-engineered for this simple need
|
|
117
|
+
- Rake tasks that modify source before testing: Rejected as fragile and error-prone
|
|
118
|
+
- RSpec shared examples: Rejected as map.rb uses custom testing framework, not RSpec
|
|
119
|
+
|
|
120
|
+
### 5. Marshal Compatibility Considerations
|
|
121
|
+
|
|
122
|
+
**Decision**: Accept potential Marshal incompatibility across Ruby versions as documented limitation
|
|
123
|
+
|
|
124
|
+
**Rationale**:
|
|
125
|
+
- Maps created on Ruby 1.8.x will have `@keys` instance variable
|
|
126
|
+
- Maps created on Ruby 1.9+ will NOT have `@keys` instance variable
|
|
127
|
+
- Marshal.load of cross-version dumps may produce Maps with inconsistent internal state
|
|
128
|
+
- This is acceptable because:
|
|
129
|
+
- Marshal is typically used within same runtime environment
|
|
130
|
+
- Cross-version Marshal compatibility is rarely needed in practice
|
|
131
|
+
- Users can work around by converting to Hash, then back to Map if needed
|
|
132
|
+
|
|
133
|
+
**Mitigation**:
|
|
134
|
+
- Document this limitation in CHANGELOG and upgrade notes
|
|
135
|
+
- Provide helper method to normalize Maps if cross-version Marshal support is needed
|
|
136
|
+
|
|
137
|
+
**Alternatives considered**:
|
|
138
|
+
- Custom marshal_dump/marshal_load: Rejected as it adds complexity for an edge case
|
|
139
|
+
- Always including @keys for Marshal compatibility: Rejected as it defeats the optimization
|
|
140
|
+
- Raising error on cross-version load: Rejected as too defensive
|
|
141
|
+
|
|
142
|
+
### 6. Alternative Ruby Implementation Compatibility
|
|
143
|
+
|
|
144
|
+
**Decision**: Trust that alternative implementations (JRuby, Rubinius, TruffleRuby) follow MRI Hash ordering behavior at their version boundaries
|
|
145
|
+
|
|
146
|
+
**Rationale**:
|
|
147
|
+
- JRuby 1.9+ mode guarantees Hash ordering (matches MRI 1.9 behavior)
|
|
148
|
+
- Rubinius follows MRI compatibility at version equivalents
|
|
149
|
+
- TruffleRuby explicitly aims for MRI compatibility
|
|
150
|
+
- RUBY_VERSION constant reflects the Ruby version being emulated
|
|
151
|
+
|
|
152
|
+
**Risk assessment**: Low - Hash ordering is part of the Ruby spec since 1.9, all major implementations comply
|
|
153
|
+
|
|
154
|
+
**Alternatives considered**:
|
|
155
|
+
- Platform-specific detection: Rejected as unnecessary complexity
|
|
156
|
+
- Feature-based detection (test if Hash preserves order): Rejected as over-engineered
|
|
157
|
+
|
|
158
|
+
## Technical Decisions Summary
|
|
159
|
+
|
|
160
|
+
| Decision Point | Choice | Rationale |
|
|
161
|
+
|----------------|--------|-----------|
|
|
162
|
+
| Version cutoff | Ruby 1.9.0 | Well-documented Hash ordering introduction |
|
|
163
|
+
| Inclusion mechanism | Conditional `include` at load time | Zero runtime overhead, simple and clear |
|
|
164
|
+
| Methods to extract | All `@keys`-dependent methods (11 methods) | Complete separation, enables Hash method delegation |
|
|
165
|
+
| Testing approach | Environment variable `MAP_FORCE_ORDERING=1` | KISS principle, easy to use, no test modifications |
|
|
166
|
+
| Marshal compatibility | Accept limitation, document workaround | Edge case doesn't justify complexity |
|
|
167
|
+
| Alt Ruby impls | Trust RUBY_VERSION constant | All major implementations comply with spec |
|
|
168
|
+
|
|
169
|
+
## Implementation Risks & Mitigations
|
|
170
|
+
|
|
171
|
+
| Risk | Impact | Likelihood | Mitigation |
|
|
172
|
+
|------|--------|-----------|------------|
|
|
173
|
+
| Ruby version detection fails on unknown implementation | High | Very Low | RUBY_VERSION is universal Ruby constant |
|
|
174
|
+
| Tests fail on older Ruby due to syntax | High | Low | Use Ruby 1.8-compatible syntax in ordering module |
|
|
175
|
+
| Performance regression on Ruby 1.9+ | Medium | Very Low | No code changes to 1.9+ path, only omission of module |
|
|
176
|
+
| Subclasses of Map break | High | Low | Test with Map::Options subclass, verify inheritance |
|
|
177
|
+
| Marshal incompatibility causes production issues | Medium | Very Low | Document limitation, provide conversion helper |
|
|
178
|
+
|
|
179
|
+
## Next Steps (Phase 1)
|
|
180
|
+
|
|
181
|
+
Based on research findings, Phase 1 will generate:
|
|
182
|
+
|
|
183
|
+
1. **data-model.md**: Document the internal state model (with/without `@keys` based on Ruby version)
|
|
184
|
+
2. **contracts/**: N/A for this feature (no external APIs, internal refactoring only)
|
|
185
|
+
3. **quickstart.md**: Developer guide for working with the new module structure
|
|
186
|
+
|
|
187
|
+
## Open Questions
|
|
188
|
+
|
|
189
|
+
None - all research areas resolved with clear decisions.
|