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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +37 -7
  4. data/examples/benchmark-rails.rb +52 -0
  5. data/examples/benchmark.rb +79 -18
  6. data/lib/generators/mongory/matcher/matcher_generator.rb +1 -1
  7. data/lib/mongory/converters/abstract_converter.rb +22 -32
  8. data/lib/mongory/converters/condition_converter.rb +7 -4
  9. data/lib/mongory/converters/data_converter.rb +18 -7
  10. data/lib/mongory/converters/key_converter.rb +43 -19
  11. data/lib/mongory/converters/value_converter.rb +24 -19
  12. data/lib/mongory/matchers/abstract_matcher.rb +94 -32
  13. data/lib/mongory/matchers/abstract_multi_matcher.rb +16 -45
  14. data/lib/mongory/matchers/and_matcher.rb +38 -10
  15. data/lib/mongory/matchers/array_record_matcher.rb +54 -28
  16. data/lib/mongory/matchers/elem_match_matcher.rb +13 -9
  17. data/lib/mongory/matchers/eq_matcher.rb +12 -7
  18. data/lib/mongory/matchers/every_matcher.rb +20 -9
  19. data/lib/mongory/matchers/exists_matcher.rb +15 -14
  20. data/lib/mongory/matchers/field_matcher.rb +58 -38
  21. data/lib/mongory/matchers/gt_matcher.rb +15 -7
  22. data/lib/mongory/matchers/gte_matcher.rb +15 -7
  23. data/lib/mongory/matchers/hash_condition_matcher.rb +47 -25
  24. data/lib/mongory/matchers/in_matcher.rb +12 -10
  25. data/lib/mongory/matchers/literal_matcher.rb +42 -48
  26. data/lib/mongory/matchers/lt_matcher.rb +15 -7
  27. data/lib/mongory/matchers/lte_matcher.rb +15 -7
  28. data/lib/mongory/matchers/ne_matcher.rb +12 -7
  29. data/lib/mongory/matchers/nin_matcher.rb +12 -9
  30. data/lib/mongory/matchers/not_matcher.rb +9 -5
  31. data/lib/mongory/matchers/or_matcher.rb +42 -13
  32. data/lib/mongory/matchers/present_matcher.rb +14 -15
  33. data/lib/mongory/matchers/regex_matcher.rb +37 -22
  34. data/lib/mongory/matchers.rb +0 -1
  35. data/lib/mongory/query_builder.rb +88 -26
  36. data/lib/mongory/query_matcher.rb +39 -12
  37. data/lib/mongory/query_operator.rb +1 -1
  38. data/lib/mongory/utils/context.rb +41 -0
  39. data/lib/mongory/utils/debugger.rb +6 -4
  40. data/lib/mongory/utils.rb +1 -0
  41. data/lib/mongory/version.rb +1 -1
  42. data/lib/mongory.rb +3 -3
  43. data/mongory.gemspec +3 -3
  44. metadata +9 -10
  45. data/lib/mongory/matchers/README.md +0 -57
  46. data/lib/mongory/matchers/abstract_operator_matcher.rb +0 -46
  47. data/lib/mongory-rb.rb +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76d9dbc1cf7704258a68da00e955a295c66d6f0c53b1bcfa8becf505315daa14
4
- data.tar.gz: 6492e6f3371fc402153abc0bb0368490a15f88f483ccdfce0912dc2249d9efe5
3
+ metadata.gz: c030da605cb94ba20f4be31a03e37a5b95277b1c2b93b48622080521752acbfa
4
+ data.tar.gz: 3577f0da8e823b74fa25459bb0a66dcb2609ff1eef366ae0ce08402a22d8c049
5
5
  SHA512:
6
- metadata.gz: 9a8139f2288b78943b1e4d0f68433d8383d359877bef277dd760e2dc08486fedf3ebccb8d914a243e0f836ff61f7d307d00df8dbd5187e260b57f3cc7eadc0d0
7
- data.tar.gz: 6317bad45fcc0ceb4d5b96316319090b6ca72e75db3ae34fd77fd799c8e1d4324d0cbb5a06b413eab2a74c89ca6494e4fc26b4ac082bfc2a2720bad373eb86b4
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) # ~2.5ms
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'}]) # ~3.2ms
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) # ~24.5ms
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'}]) # ~31.5ms
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) # ~242.5ms
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'}]) # ~323.0ms
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/koten0224/mongory-rb/blob/main/CODE_OF_CONDUCT.md).
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
@@ -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
- 5.times do
25
- result = Benchmark.measure do
26
- records.mongory.where(:age.gte => 18).to_a
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
- 5.times do
34
- result = Benchmark.measure do
35
- records.mongory.where(
36
- :$or => [
37
- { :age.gte => 18 },
38
- { status: 'active' }
39
- ]
40
- ).to_a
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::AbstractOperatorMatcher
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
- @fallback = Proc.new { |*| self }
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
- @registries.each do |registry|
47
- next unless target.is_a?(registry.klass)
45
+ convert_strategy = @convert_strategy_map[target.class] ||= find_strategy(target)
48
46
 
49
- return exec_convert(target, other, &registry.exec)
50
- end
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
- exec_convert(target, other, &@fallback)
53
+ def fallback(target, _)
54
+ target
53
55
  end
54
56
 
55
- # Internal dispatch logic to apply a matching converter.
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 match
58
- # @param other [Object] optional extra data
59
- # @yield fallback block if no converter is found
60
- # @return [Object]
61
- def exec_convert(target, other, &block)
62
- if other == NOTHING
63
- target.instance_exec(&block)
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, :exec_convert
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
- def default_registrations
17
- register(Symbol, :to_s)
18
- register(Date, :to_s)
19
- register(Time, :iso8601)
20
- register(DateTime, :iso8601)
21
- register(String, :itself)
22
- register(Integer, :itself)
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 registers rules for
10
- # strings, symbols, and includes a fallback handler.
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") #=> { "user" => { "name" => value } }
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
- # fallback if key type is unknown — returns { self => value }
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
- # - `:"a.b.c" => v` becomes `{ "a" => { "b" => { "c" => v } } }`
34
- register(Symbol) do |other|
35
- convert_string_key.call(to_s, other)
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
- # - `:"a.b.c".present => true` becomes `{ "a" => { "b" => { "c" => { "$present" => true } } } }`
39
- register(QueryOperator, :__expr_part__)
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
- # Sets a fallback using DataConverter for unsupported types.
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
- # @return [void]
23
- def initialize
24
- super
25
- @fallback = Proc.new do
26
- Mongory.data_converter.convert(self)
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 default_registrations
31
- v_convert = method(:convert)
32
- register(Array) do
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
- register(String, :itself)
43
- register(Integer, :itself)
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