mongory 0.3.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +65 -0
- data/README.md +37 -7
- data/examples/benchmark-rails.rb +52 -0
- data/examples/benchmark.rb +79 -18
- data/lib/generators/mongory/matcher/matcher_generator.rb +1 -1
- data/lib/mongory/converters/abstract_converter.rb +22 -32
- data/lib/mongory/converters/condition_converter.rb +7 -4
- data/lib/mongory/converters/data_converter.rb +18 -7
- data/lib/mongory/converters/key_converter.rb +43 -19
- data/lib/mongory/converters/value_converter.rb +24 -19
- data/lib/mongory/matchers/abstract_matcher.rb +94 -32
- data/lib/mongory/matchers/abstract_multi_matcher.rb +16 -45
- data/lib/mongory/matchers/and_matcher.rb +38 -10
- data/lib/mongory/matchers/array_record_matcher.rb +54 -28
- data/lib/mongory/matchers/elem_match_matcher.rb +13 -9
- data/lib/mongory/matchers/eq_matcher.rb +12 -7
- data/lib/mongory/matchers/every_matcher.rb +20 -9
- data/lib/mongory/matchers/exists_matcher.rb +15 -14
- data/lib/mongory/matchers/field_matcher.rb +58 -38
- data/lib/mongory/matchers/gt_matcher.rb +15 -7
- data/lib/mongory/matchers/gte_matcher.rb +15 -7
- data/lib/mongory/matchers/hash_condition_matcher.rb +47 -25
- data/lib/mongory/matchers/in_matcher.rb +12 -10
- data/lib/mongory/matchers/literal_matcher.rb +42 -48
- data/lib/mongory/matchers/lt_matcher.rb +15 -7
- data/lib/mongory/matchers/lte_matcher.rb +15 -7
- data/lib/mongory/matchers/ne_matcher.rb +12 -7
- data/lib/mongory/matchers/nin_matcher.rb +12 -9
- data/lib/mongory/matchers/not_matcher.rb +9 -5
- data/lib/mongory/matchers/or_matcher.rb +42 -13
- data/lib/mongory/matchers/present_matcher.rb +14 -15
- data/lib/mongory/matchers/regex_matcher.rb +37 -22
- data/lib/mongory/matchers.rb +0 -1
- data/lib/mongory/query_builder.rb +88 -26
- data/lib/mongory/query_matcher.rb +39 -12
- data/lib/mongory/query_operator.rb +1 -1
- data/lib/mongory/utils/context.rb +41 -0
- data/lib/mongory/utils/debugger.rb +6 -4
- data/lib/mongory/utils.rb +1 -0
- data/lib/mongory/version.rb +1 -1
- data/lib/mongory.rb +3 -3
- data/mongory.gemspec +3 -3
- metadata +9 -10
- data/lib/mongory/matchers/README.md +0 -57
- data/lib/mongory/matchers/abstract_operator_matcher.rb +0 -46
- data/lib/mongory-rb.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c030da605cb94ba20f4be31a03e37a5b95277b1c2b93b48622080521752acbfa
|
4
|
+
data.tar.gz: 3577f0da8e823b74fa25459bb0a66dcb2609ff1eef366ae0ce08402a22d8c049
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 723cdce8afcd41e7f69148934890ac3bc133e297bda01e412b69e491ece2fdfe1b8b27c87244c6417ce4905d18fb8d3082b4217c979b2f6ec1e65bcc71ec0091
|
7
|
+
data.tar.gz: eaa426a3780cc094c0dfd0e5e168fbce4c94db61f504fefa1a44efed5d569bc76a6caf348f5170c46c92916b0ceb384105757b72cbb43e96fdc16208abf7715f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,70 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [0.6.0] - 2025-04-23
|
4
|
+
|
5
|
+
### Major Changes
|
6
|
+
- Refactored matcher system to use Proc-based implementation
|
7
|
+
- Introduced Context system for better state management
|
8
|
+
- Optimized empty condition handling with TRUE_PROC and FALSE_PROC
|
9
|
+
|
10
|
+
### Breaking Changes
|
11
|
+
- Removed `match` method in favor of `raw_proc`
|
12
|
+
- Changed converter behavior to be executed within Proc
|
13
|
+
- Modified fallback behavior in converters
|
14
|
+
|
15
|
+
### Features
|
16
|
+
- Added Context system for better configuration management
|
17
|
+
- Improved performance with Proc caching
|
18
|
+
- Enhanced error handling in matchers
|
19
|
+
- Better support for complex query conditions
|
20
|
+
|
21
|
+
### Performance
|
22
|
+
- Optimized empty condition handling
|
23
|
+
- Reduced memory usage with Proc caching
|
24
|
+
- Improved execution speed with Proc-based implementation
|
25
|
+
|
26
|
+
### Internal
|
27
|
+
- Refactored matcher system architecture
|
28
|
+
- Improved code organization and maintainability
|
29
|
+
- Enhanced test coverage
|
30
|
+
|
31
|
+
## [0.5.0] - 2025-04-22
|
32
|
+
|
33
|
+
### Added
|
34
|
+
- Added fast mode implementation for optimized query performance
|
35
|
+
- Added Proc-based matching for efficient record filtering
|
36
|
+
- Added error handling in fast mode execution
|
37
|
+
|
38
|
+
### Changed
|
39
|
+
- Optimized query execution with compiled Proc objects
|
40
|
+
- Improved memory efficiency in record matching
|
41
|
+
- Enhanced error handling in query execution
|
42
|
+
|
43
|
+
### Fixed
|
44
|
+
- Fixed potential performance bottlenecks in record matching
|
45
|
+
- Fixed error handling in query execution
|
46
|
+
|
47
|
+
## [0.4.0] - 2025-04-20
|
48
|
+
|
49
|
+
### ✨ Added
|
50
|
+
- Support regex matching in array fields via `ArrayRecordMatcher`.
|
51
|
+
- Example: `tags: /vip/` will now match any element inside an array.
|
52
|
+
- Publicize `MatcherGenerator#update_initializer` to allow external CLI or test usage.
|
53
|
+
|
54
|
+
### 🧹 Changed
|
55
|
+
- Refactored `ValueConverter` to use direct method reference instead of dynamic caching logic.
|
56
|
+
|
57
|
+
### 📝 Documentation
|
58
|
+
- Improved matcher generator template documentation.
|
59
|
+
- Added usage examples and matcher tree explanation.
|
60
|
+
- Fixed `Mongory::Debugger` references to `Mongory.debugger`.
|
61
|
+
|
62
|
+
### 💥 Breaking
|
63
|
+
- Renamed gem from `mongory-rb` to `mongory`, affecting gemspec and file references.
|
64
|
+
- `mongory.gemspec` now used in place of `mongory-rb.gemspec`.
|
65
|
+
|
66
|
+
# Changelog
|
67
|
+
|
3
68
|
## v0.3.0
|
4
69
|
|
5
70
|
### New Features
|
data/README.md
CHANGED
@@ -245,6 +245,22 @@ Internally, the query is compiled into a matcher tree using the `QueryMatcher` a
|
|
245
245
|
| `nin` | Checks if a value is not in a set | `nin(age: [18, 19, 20])` |
|
246
246
|
| `limit` | Limits the number of records returned. This method executes immediately and affects subsequent conditions. | `limit(2)` |
|
247
247
|
| `pluck` | Extracts selected fields from matching records | `pluck(:name)` |
|
248
|
+
| `with_context` | Sets a custom context for the query. Useful for controlling data conversion and sharing configuration across matchers. | `with_context(merchant: merchant)` |
|
249
|
+
|
250
|
+
#### Context Configuration
|
251
|
+
|
252
|
+
The `with_context` method allows you to customize the query execution environment:
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
# Share configuration across matchers
|
256
|
+
records.mongory
|
257
|
+
.with_context(custom_option: true)
|
258
|
+
.where(:status => 'active')
|
259
|
+
.where(:age.gte => 18)
|
260
|
+
```
|
261
|
+
|
262
|
+
This will share a mutatable, but stable context object to all matchers in matcher tree.
|
263
|
+
To get your custom option, using `@context.config` in your custom matcher.
|
248
264
|
|
249
265
|
### Debugging Queries
|
250
266
|
|
@@ -313,30 +329,37 @@ The debug output includes:
|
|
313
329
|
1. **Memory Usage**
|
314
330
|
- Mongory operates entirely in memory
|
315
331
|
- Consider your data size and memory constraints
|
332
|
+
- Proc-based implementation reduces memory usage
|
333
|
+
- Context system provides better memory management
|
316
334
|
|
317
335
|
2. **Query Optimization**
|
318
336
|
- Complex conditions are evaluated in sequence
|
319
337
|
- Use `explain` to analyze query performance
|
338
|
+
- Empty conditions are optimized with cached Procs
|
339
|
+
- Context system allows fine-grained control over conversion
|
320
340
|
|
321
341
|
3. **Benchmarks**
|
322
342
|
```ruby
|
323
343
|
# Simple query (1000 records)
|
324
|
-
records.mongory.where(:age.gte => 18) # ~
|
344
|
+
records.mongory.where(:age.gte => 18) # ~0.8ms
|
325
345
|
|
326
346
|
# Complex query (1000 records)
|
327
|
-
records.mongory.where(:$or => [{:age.gte => 18}, {:status => 'active'}]) # ~
|
347
|
+
records.mongory.where(:$or => [{:age.gte => 18}, {:status => 'active'}]) # ~0.9ms
|
328
348
|
|
329
349
|
# Simple query (10000 records)
|
330
|
-
records.mongory.where(:age.gte => 18) # ~
|
350
|
+
records.mongory.where(:age.gte => 18) # ~6.5ms
|
331
351
|
|
332
352
|
# Complex query (10000 records)
|
333
|
-
records.mongory.where(:$or => [{:age.gte => 18}, {:status => 'active'}]) # ~
|
353
|
+
records.mongory.where(:$or => [{:age.gte => 18}, {:status => 'active'}]) # ~7.5ms
|
334
354
|
|
335
355
|
# Simple query (100000 records)
|
336
|
-
records.mongory.where(:age.gte => 18) # ~
|
356
|
+
records.mongory.where(:age.gte => 18) # ~64.7ms
|
337
357
|
|
338
358
|
# Complex query (100000 records)
|
339
|
-
records.mongory.where(:$or => [{:age.gte => 18}, {:status => 'active'}]) # ~
|
359
|
+
records.mongory.where(:$or => [{:age.gte => 18}, {:status => 'active'}]) # ~75.0ms
|
360
|
+
|
361
|
+
# Complex query with fast mode (100000 records)
|
362
|
+
records.mongory.where(:$or => [{:age.gte => 18}, {:status => 'active'}]).fast # ~42.8ms, about 3x of plain ruby
|
340
363
|
```
|
341
364
|
|
342
365
|
Note: Performance varies based on:
|
@@ -435,6 +458,9 @@ end
|
|
435
458
|
# Use limit to restrict result set
|
436
459
|
records.mongory.limit(100).where(:age.gte => 18)
|
437
460
|
|
461
|
+
# Use fast mode for better performance
|
462
|
+
records.mongory.where(:age.gte => 18).fast
|
463
|
+
|
438
464
|
# Use explain to analyze complex queries
|
439
465
|
query = records.mongory.where(:$or => [...])
|
440
466
|
query.explain
|
@@ -457,10 +483,14 @@ end
|
|
457
483
|
1. **Data Size**
|
458
484
|
- Suitable for small to medium datasets
|
459
485
|
- Large datasets may impact performance
|
486
|
+
- Proc-based implementation helps with memory usage
|
487
|
+
- Context system provides better resource management
|
460
488
|
|
461
489
|
2. **Query Complexity**
|
462
490
|
- Complex queries may affect performance
|
463
491
|
- Not all MongoDB operators are supported
|
492
|
+
- Proc-based implementation improves complex query performance
|
493
|
+
- Context system allows better control over query execution
|
464
494
|
|
465
495
|
3. **Memory Usage**
|
466
496
|
- All operations are performed in memory
|
@@ -510,7 +540,7 @@ Please ensure your code adheres to the project's style guide and that all tests
|
|
510
540
|
|
511
541
|
## Code of Conduct
|
512
542
|
|
513
|
-
Everyone interacting in the Mongory-rb project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/
|
543
|
+
Everyone interacting in the Mongory-rb project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/mongoryhq/mongory-rb/blob/main/CODE_OF_CONDUCT.md).
|
514
544
|
|
515
545
|
## License
|
516
546
|
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/all'
|
4
|
+
require 'benchmark'
|
5
|
+
|
6
|
+
puts 'Ruby String#empty?'
|
7
|
+
result = Benchmark.measure do
|
8
|
+
1_000_000.times do
|
9
|
+
''.empty?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
puts result
|
13
|
+
|
14
|
+
puts 'Rails String#blank?'
|
15
|
+
result = Benchmark.measure do
|
16
|
+
1_000_000.times do
|
17
|
+
''.blank?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
puts result
|
21
|
+
|
22
|
+
puts 'Ruby Array#empty?'
|
23
|
+
result = Benchmark.measure do
|
24
|
+
1_000_000.times do
|
25
|
+
[].empty?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
puts result
|
29
|
+
|
30
|
+
puts 'Rails Array#blank?'
|
31
|
+
result = Benchmark.measure do
|
32
|
+
1_000_000.times do
|
33
|
+
[].blank?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
puts result
|
37
|
+
|
38
|
+
puts 'Ruby Hash#empty?'
|
39
|
+
result = Benchmark.measure do
|
40
|
+
1_000_000.times do
|
41
|
+
{}.empty?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
puts result
|
45
|
+
|
46
|
+
puts 'Rails Hash#blank?'
|
47
|
+
result = Benchmark.measure do
|
48
|
+
1_000_000.times do
|
49
|
+
{}.blank?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
puts result
|
data/examples/benchmark.rb
CHANGED
@@ -7,8 +7,23 @@ require 'benchmark'
|
|
7
7
|
Mongory.register(Array)
|
8
8
|
Mongory.enable_symbol_snippets!
|
9
9
|
|
10
|
+
def gc_handler
|
11
|
+
# GC.disable
|
12
|
+
before = GC.stat
|
13
|
+
yield
|
14
|
+
after = GC.stat
|
15
|
+
# GC.enable
|
16
|
+
|
17
|
+
created = after[:total_allocated_objects] - before[:total_allocated_objects]
|
18
|
+
freed = after[:total_freed_objects] - before[:total_freed_objects]
|
19
|
+
alive = after[:heap_live_slots] - before[:heap_live_slots]
|
20
|
+
|
21
|
+
puts "Created: #{created}" # 分配了幾個 object
|
22
|
+
puts "Freed: #{freed}" # 中途 GC 掃掉幾個(若 GC 有觸發)
|
23
|
+
puts "Net alive: #{alive}" # 最後還活著的物件數
|
24
|
+
end
|
10
25
|
# Test with different data sizes
|
11
|
-
[1000, 10_000, 100_000].each do |size|
|
26
|
+
[20, 1000, 10_000, 100_000].each do |size|
|
12
27
|
puts "\nTesting with #{size} records:"
|
13
28
|
|
14
29
|
# Generate test data
|
@@ -19,26 +34,72 @@ Mongory.enable_symbol_snippets!
|
|
19
34
|
}
|
20
35
|
end
|
21
36
|
|
22
|
-
# Simple query test
|
23
|
-
puts "\nSimple query (#{size} records):"
|
24
|
-
|
25
|
-
|
26
|
-
|
37
|
+
# Simple query (Plain Ruby) test
|
38
|
+
puts "\nSimple query (Plain Ruby) (#{size} records):"
|
39
|
+
gc_handler do
|
40
|
+
5.times do
|
41
|
+
result = Benchmark.measure do
|
42
|
+
records.select { |r| r.key?('age') && r['age'] >= 18 }
|
43
|
+
end
|
44
|
+
puts result
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Simple query (Mongory) test
|
49
|
+
puts "\nSimple query (Mongory) (#{size} records):"
|
50
|
+
gc_handler do
|
51
|
+
5.times do
|
52
|
+
result = Benchmark.measure do
|
53
|
+
records.mongory.where(:age.gte => 18).to_a
|
54
|
+
end
|
55
|
+
puts result
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Complex query (Plain Ruby) test
|
60
|
+
puts "\nComplex query (Plain Ruby) (#{size} records):"
|
61
|
+
gc_handler do
|
62
|
+
5.times do
|
63
|
+
result = Benchmark.measure do
|
64
|
+
records.select do |r|
|
65
|
+
next false unless r.key?('age') && r.key?('status')
|
66
|
+
|
67
|
+
r['age'] >= 18 || r['status'] == 'active'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
puts result
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Complex query (Mongory) test
|
75
|
+
puts "\nComplex query (Mongory) (#{size} records):"
|
76
|
+
gc_handler do
|
77
|
+
5.times do
|
78
|
+
result = Benchmark.measure do
|
79
|
+
records.mongory.where(
|
80
|
+
:$or => [
|
81
|
+
{ :age.gte => 18 },
|
82
|
+
{ status: 'active' }
|
83
|
+
]
|
84
|
+
).to_a
|
85
|
+
end
|
86
|
+
puts result
|
27
87
|
end
|
28
|
-
puts result
|
29
88
|
end
|
30
89
|
|
31
|
-
# Complex query test
|
32
|
-
puts "\nComplex query (#{size} records):"
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
90
|
+
# Complex query (Mongory) test
|
91
|
+
puts "\nComplex query (Mongory) fast mode (#{size} records):"
|
92
|
+
gc_handler do
|
93
|
+
5.times do
|
94
|
+
result = Benchmark.measure do
|
95
|
+
records.mongory.where(
|
96
|
+
:$or => [
|
97
|
+
{ :age.gte => 18 },
|
98
|
+
{ status: 'active' }
|
99
|
+
]
|
100
|
+
).fast.to_a
|
101
|
+
end
|
102
|
+
puts result
|
41
103
|
end
|
42
|
-
puts result
|
43
104
|
end
|
44
105
|
end
|
@@ -14,7 +14,7 @@ module Mongory
|
|
14
14
|
# # spec/mongory/matchers/class_in_matcher_spec.rb
|
15
15
|
# # config/initializers/mongory.rb (if not exists)
|
16
16
|
#
|
17
|
-
# @see Mongory::Matchers::
|
17
|
+
# @see Mongory::Matchers::AbstractMatcher
|
18
18
|
class MatcherGenerator < Rails::Generators::NamedBase
|
19
19
|
source_root File.expand_path('templates', __dir__)
|
20
20
|
|
@@ -33,8 +33,7 @@ module Mongory
|
|
33
33
|
def initialize
|
34
34
|
super(self.class.to_s)
|
35
35
|
@registries = []
|
36
|
-
@
|
37
|
-
execute_once_only!(:default_registrations)
|
36
|
+
@convert_strategy_map = {}.compare_by_identity
|
38
37
|
end
|
39
38
|
|
40
39
|
# Applies the registered conversion to the given target object.
|
@@ -43,27 +42,32 @@ module Mongory
|
|
43
42
|
# @param other [Object] optional secondary value
|
44
43
|
# @return [Object] converted result
|
45
44
|
def convert(target, other = NOTHING)
|
46
|
-
@
|
47
|
-
next unless target.is_a?(registry.klass)
|
45
|
+
convert_strategy = @convert_strategy_map[target.class] ||= find_strategy(target)
|
48
46
|
|
49
|
-
|
50
|
-
|
47
|
+
return fallback(target, other) if convert_strategy == NOTHING
|
48
|
+
return target.instance_exec(&convert_strategy) if other == NOTHING
|
49
|
+
|
50
|
+
target.instance_exec(other, &convert_strategy)
|
51
|
+
end
|
51
52
|
|
52
|
-
|
53
|
+
def fallback(target, _)
|
54
|
+
target
|
53
55
|
end
|
54
56
|
|
55
|
-
#
|
57
|
+
# Finds the appropriate conversion strategy for the target object.
|
58
|
+
# Searches through registered rules and returns the first matching one,
|
59
|
+
# or the fallback strategy if no match is found.
|
56
60
|
#
|
57
|
-
# @param target [Object] the object to
|
58
|
-
# @
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
else
|
65
|
-
target.instance_exec(other, &block)
|
61
|
+
# @param target [Object] the object to find a strategy for
|
62
|
+
# @return [Proc] the conversion strategy to use
|
63
|
+
def find_strategy(target)
|
64
|
+
@registries.each do |registry|
|
65
|
+
next unless target.is_a?(registry.klass)
|
66
|
+
|
67
|
+
return registry.exec
|
66
68
|
end
|
69
|
+
|
70
|
+
NOTHING
|
67
71
|
end
|
68
72
|
|
69
73
|
# Opens a configuration block to register more converters.
|
@@ -98,25 +102,11 @@ module Mongory
|
|
98
102
|
register(klass) { |*args, &bl| send(converter, *args, &bl) }
|
99
103
|
elsif block.is_a?(Proc)
|
100
104
|
@registries.unshift(Registry.new(klass, block))
|
105
|
+
@convert_strategy_map[klass] = block
|
101
106
|
else
|
102
107
|
raise 'Support Symbol and block only.'
|
103
108
|
end
|
104
109
|
end
|
105
|
-
|
106
|
-
# Executes the given method only once by undefining it after execution.
|
107
|
-
#
|
108
|
-
# @param method_sym [Symbol] method name to execute once
|
109
|
-
# @return [void]
|
110
|
-
def execute_once_only!(method_sym)
|
111
|
-
send(method_sym)
|
112
|
-
singleton_class.undef_method(method_sym)
|
113
|
-
end
|
114
|
-
|
115
|
-
# Defines default class-to-converter registrations.
|
116
|
-
# Should be overridden by subclasses.
|
117
|
-
#
|
118
|
-
# @return [void]
|
119
|
-
def default_registrations; end
|
120
110
|
end
|
121
111
|
end
|
122
112
|
end
|
@@ -16,8 +16,9 @@ module Mongory
|
|
16
16
|
#
|
17
17
|
class ConditionConverter < AbstractConverter
|
18
18
|
# Converts a flat condition hash into a nested structure.
|
19
|
+
# Applies both key and value conversion, and merges overlapping keys.
|
19
20
|
#
|
20
|
-
# @param condition [Hash]
|
21
|
+
# @param condition [Hash] the flat condition hash to convert
|
21
22
|
# @return [Hash] the transformed nested condition
|
22
23
|
def convert(condition)
|
23
24
|
result = {}
|
@@ -30,8 +31,10 @@ module Mongory
|
|
30
31
|
end
|
31
32
|
|
32
33
|
# Provides a block that merges values for overlapping keys in a deep way.
|
34
|
+
# When both values are hashes, recursively merges them.
|
35
|
+
# Otherwise, uses the second value.
|
33
36
|
#
|
34
|
-
# @return [Proc]
|
37
|
+
# @return [Proc] a block for deep merging hash values
|
35
38
|
def deep_merge_block
|
36
39
|
@deep_merge_block ||= Proc.new do |_, a, b|
|
37
40
|
if a.is_a?(Hash) && b.is_a?(Hash)
|
@@ -45,7 +48,7 @@ module Mongory
|
|
45
48
|
# @note Singleton instance, not configurable after initialization
|
46
49
|
# Returns the key converter used to transform condition keys.
|
47
50
|
#
|
48
|
-
# @return [AbstractConverter]
|
51
|
+
# @return [AbstractConverter] the key converter instance
|
49
52
|
def key_converter
|
50
53
|
KeyConverter.instance
|
51
54
|
end
|
@@ -68,7 +71,7 @@ module Mongory
|
|
68
71
|
value_converter.freeze
|
69
72
|
end
|
70
73
|
|
71
|
-
undef_method :register
|
74
|
+
undef_method :register
|
72
75
|
end
|
73
76
|
end
|
74
77
|
end
|
@@ -13,13 +13,24 @@ module Mongory
|
|
13
13
|
# DataConverter.instance.convert(:status) #=> "status"
|
14
14
|
#
|
15
15
|
class DataConverter < AbstractConverter
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
alias_method :super_convert, :convert
|
17
|
+
|
18
|
+
# Converts a value into its standardized form based on its type.
|
19
|
+
# Handles common primitive types with predefined conversion rules.
|
20
|
+
#
|
21
|
+
# @param target [Object] the value to convert
|
22
|
+
# @return [Object] the converted value
|
23
|
+
def convert(target)
|
24
|
+
case target
|
25
|
+
when String, Integer, Hash, Array
|
26
|
+
target
|
27
|
+
when Symbol, Date
|
28
|
+
target.to_s
|
29
|
+
when Time, DateTime
|
30
|
+
target.iso8601
|
31
|
+
else
|
32
|
+
super_convert(target)
|
33
|
+
end
|
23
34
|
end
|
24
35
|
end
|
25
36
|
end
|
@@ -6,8 +6,13 @@ module Mongory
|
|
6
6
|
# It normalizes symbol keys into string paths, splits dotted keys,
|
7
7
|
# and delegates to the appropriate converter logic.
|
8
8
|
#
|
9
|
-
# This class inherits from AbstractConverter and
|
10
|
-
#
|
9
|
+
# This class inherits from AbstractConverter and provides specialized
|
10
|
+
# handling for different key types:
|
11
|
+
# - String keys with dots are split into nested paths
|
12
|
+
# - Symbol keys are converted to strings
|
13
|
+
# - QueryOperator instances are handled via their DSL hooks
|
14
|
+
# - Other types fall back to the parent converter
|
15
|
+
#
|
11
16
|
# Used by ConditionConverter to build query structures from flat input.
|
12
17
|
#
|
13
18
|
# - `"a.b.c" => v` becomes `{ "a" => { "b" => { "c" => v } } }`
|
@@ -15,31 +20,45 @@ module Mongory
|
|
15
20
|
# - QueryOperator dispatches to internal DSL hook
|
16
21
|
#
|
17
22
|
# @example Convert a dotted string key
|
18
|
-
# KeyConverter.instance.convert("user.name"
|
23
|
+
# KeyConverter.instance.convert("user.name", "John")
|
24
|
+
# # => { "user" => { "name" => "John" } }
|
25
|
+
#
|
26
|
+
# @example Convert a symbol key
|
27
|
+
# KeyConverter.instance.convert(:status, "active")
|
28
|
+
# # => { "status" => "active" }
|
19
29
|
#
|
30
|
+
# @see AbstractConverter
|
20
31
|
class KeyConverter < AbstractConverter
|
21
|
-
|
22
|
-
def initialize
|
23
|
-
super
|
24
|
-
@fallback = ->(x) { { self => x } }
|
25
|
-
end
|
26
|
-
|
27
|
-
def default_registrations
|
28
|
-
convert_string_key = method(:convert_string_key)
|
29
|
-
register(String) do |value|
|
30
|
-
convert_string_key.call(self, value)
|
31
|
-
end
|
32
|
+
alias_method :super_convert, :convert
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
34
|
+
# Converts a key into its normalized form based on its type.
|
35
|
+
# Handles strings, symbols, and QueryOperator instances.
|
36
|
+
# Falls back to parent converter for other types.
|
37
|
+
#
|
38
|
+
# @param target [Object] the key to convert
|
39
|
+
# @param other [Object] the value associated with the key
|
40
|
+
# @return [Hash] the converted key-value pair
|
41
|
+
def convert(target, other)
|
42
|
+
case target
|
43
|
+
when String
|
44
|
+
convert_string_key(target, other)
|
45
|
+
when Symbol
|
46
|
+
convert_string_key(target.to_s, other)
|
47
|
+
when QueryOperator
|
48
|
+
# Handle special case for QueryOperator
|
49
|
+
convert_string_key(*target.__expr_part__(other).first)
|
50
|
+
else
|
51
|
+
super_convert(target, other)
|
36
52
|
end
|
53
|
+
end
|
37
54
|
|
38
|
-
|
39
|
-
|
55
|
+
def fallback(target, other)
|
56
|
+
{ target => other }
|
40
57
|
end
|
41
58
|
|
42
59
|
# Converts a dotted string key into nested hash form.
|
60
|
+
# Splits the key on dots and builds a nested structure.
|
61
|
+
# Handles escaped dots in the key.
|
43
62
|
#
|
44
63
|
# @param key [String] the dotted key string, e.g. "a.b.c"
|
45
64
|
# @param value [Object] the value to assign at the deepest level
|
@@ -55,6 +74,11 @@ module Mongory
|
|
55
74
|
ret
|
56
75
|
end
|
57
76
|
|
77
|
+
# Normalizes a key by unescaping escaped dots.
|
78
|
+
# This allows for literal dots in field names.
|
79
|
+
#
|
80
|
+
# @param key [String] the key to normalize
|
81
|
+
# @return [String] the normalized key
|
58
82
|
def normalize_key(key)
|
59
83
|
key.gsub(/\\\./, '.')
|
60
84
|
end
|
@@ -17,30 +17,35 @@ module Mongory
|
|
17
17
|
# ValueConverter.instance.convert(/foo/) #=> { "$regex" => "foo" }
|
18
18
|
#
|
19
19
|
class ValueConverter < AbstractConverter
|
20
|
-
|
20
|
+
alias_method :super_convert, :convert
|
21
|
+
|
22
|
+
# Converts a value into its standardized form based on its type.
|
23
|
+
# Handles arrays, hashes, regex, and basic types.
|
21
24
|
#
|
22
|
-
# @
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
# @param target [Object] the value to convert
|
26
|
+
# @return [Object] the converted value
|
27
|
+
def convert(target)
|
28
|
+
case target
|
29
|
+
when String, Integer, Regexp
|
30
|
+
target
|
31
|
+
when Array
|
32
|
+
target.map { |x| convert(x) }
|
33
|
+
when Hash
|
34
|
+
condition_converter.convert(target)
|
35
|
+
else
|
36
|
+
super_convert(target)
|
27
37
|
end
|
28
38
|
end
|
29
39
|
|
30
|
-
def
|
31
|
-
|
32
|
-
|
33
|
-
map { |x| v_convert.call(x) }
|
34
|
-
end
|
35
|
-
|
36
|
-
# - Hashes are interpreted as nested condition trees
|
37
|
-
# using ConditionConverter
|
38
|
-
register(Hash) do
|
39
|
-
Mongory.condition_converter.convert(self)
|
40
|
-
end
|
40
|
+
def fallback(target, *)
|
41
|
+
Mongory.data_converter.convert(target)
|
42
|
+
end
|
41
43
|
|
42
|
-
|
43
|
-
|
44
|
+
# Returns the condition converter instance.
|
45
|
+
#
|
46
|
+
# @return [ConditionConverter] the condition converter instance
|
47
|
+
def condition_converter
|
48
|
+
@condition_converter ||= Mongory.condition_converter
|
44
49
|
end
|
45
50
|
end
|
46
51
|
end
|