mongory 0.4.0 → 0.6.1

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -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 +9 -19
  9. data/lib/mongory/converters/converted.rb +81 -0
  10. data/lib/mongory/converters/data_converter.rb +18 -7
  11. data/lib/mongory/converters/key_converter.rb +43 -19
  12. data/lib/mongory/converters/value_converter.rb +24 -19
  13. data/lib/mongory/converters.rb +1 -0
  14. data/lib/mongory/matchers/abstract_matcher.rb +94 -32
  15. data/lib/mongory/matchers/abstract_multi_matcher.rb +16 -45
  16. data/lib/mongory/matchers/and_matcher.rb +38 -10
  17. data/lib/mongory/matchers/array_record_matcher.rb +54 -28
  18. data/lib/mongory/matchers/elem_match_matcher.rb +13 -9
  19. data/lib/mongory/matchers/eq_matcher.rb +12 -7
  20. data/lib/mongory/matchers/every_matcher.rb +20 -9
  21. data/lib/mongory/matchers/exists_matcher.rb +15 -14
  22. data/lib/mongory/matchers/field_matcher.rb +58 -38
  23. data/lib/mongory/matchers/gt_matcher.rb +15 -7
  24. data/lib/mongory/matchers/gte_matcher.rb +15 -7
  25. data/lib/mongory/matchers/hash_condition_matcher.rb +54 -26
  26. data/lib/mongory/matchers/in_matcher.rb +20 -13
  27. data/lib/mongory/matchers/literal_matcher.rb +42 -48
  28. data/lib/mongory/matchers/lt_matcher.rb +15 -7
  29. data/lib/mongory/matchers/lte_matcher.rb +15 -7
  30. data/lib/mongory/matchers/ne_matcher.rb +12 -7
  31. data/lib/mongory/matchers/nin_matcher.rb +20 -12
  32. data/lib/mongory/matchers/not_matcher.rb +9 -5
  33. data/lib/mongory/matchers/or_matcher.rb +42 -13
  34. data/lib/mongory/matchers/present_matcher.rb +14 -15
  35. data/lib/mongory/matchers/regex_matcher.rb +37 -22
  36. data/lib/mongory/matchers/size_matcher.rb +50 -0
  37. data/lib/mongory/matchers.rb +1 -1
  38. data/lib/mongory/query_builder.rb +89 -27
  39. data/lib/mongory/query_matcher.rb +40 -13
  40. data/lib/mongory/query_operator.rb +1 -1
  41. data/lib/mongory/utils/context.rb +41 -0
  42. data/lib/mongory/utils/debugger.rb +6 -4
  43. data/lib/mongory/utils.rb +1 -0
  44. data/lib/mongory/version.rb +1 -1
  45. data/lib/mongory.rb +3 -3
  46. data/mongory.gemspec +3 -3
  47. metadata +11 -9
  48. data/lib/mongory/matchers/README.md +0 -57
  49. data/lib/mongory/matchers/abstract_operator_matcher.rb +0 -46
@@ -18,16 +18,20 @@ module Mongory
18
18
  #
19
19
  # @see HashConditionMatcher
20
20
  class ElemMatchMatcher < HashConditionMatcher
21
- # Matches true if any element in the array satisfies the condition.
22
- # Falls back to false if the input is not an array.
23
-
24
- # @param collection [Object] the input to be tested
25
- # @return [Boolean] whether any element matches
26
- def match(collection)
27
- return false unless collection.is_a?(Array)
21
+ # Creates a raw Proc that performs the element matching operation.
22
+ # The Proc checks if any element in the array matches the condition.
23
+ #
24
+ # @return [Proc] a Proc that performs the element matching operation
25
+ def raw_proc
26
+ super_proc = super
27
+ need_convert = @context.need_convert
28
+ data_converter = Mongory.data_converter
28
29
 
29
- collection.any? do |record|
30
- super(Mongory.data_converter.convert(record))
30
+ Proc.new do |collection|
31
+ collection.any? do |record|
32
+ record = data_converter.convert(record) if need_convert
33
+ super_proc.call(record)
34
+ end
31
35
  end
32
36
  end
33
37
 
@@ -4,7 +4,7 @@ module Mongory
4
4
  module Matchers
5
5
  # EqMatcher matches values using the equality operator `==`.
6
6
  #
7
- # It inherits from AbstractOperatorMatcher and defines its operator as `:==`.
7
+ # It inherits from AbstractMatcher and defines its operator as `:==`.
8
8
  #
9
9
  # Used for conditions like:
10
10
  # - { age: { '$eq' => 30 } }
@@ -22,13 +22,18 @@ module Mongory
22
22
  #
23
23
  # @note Equality behavior depends on how `==` is implemented for the given objects.
24
24
  #
25
- # @see AbstractOperatorMatcher
26
- class EqMatcher < AbstractOperatorMatcher
27
- # Returns the Ruby equality operator to be used in matching.
25
+ # @see AbstractMatcher
26
+ class EqMatcher < AbstractMatcher
27
+ # Creates a raw Proc that performs the equality check.
28
+ # The Proc uses the `==` operator to compare values.
28
29
  #
29
- # @return [Symbol] the equality operator symbol
30
- def operator
31
- :==
30
+ # @return [Proc] a Proc that performs the equality check
31
+ def raw_proc
32
+ condition = @condition
33
+
34
+ Proc.new do |record|
35
+ record == condition
36
+ end
32
37
  end
33
38
  end
34
39
 
@@ -15,20 +15,31 @@ module Mongory
15
15
  #
16
16
  # @see HashConditionMatcher
17
17
  class EveryMatcher < HashConditionMatcher
18
- # Matches true if all element in the array satisfies the condition.
19
- # Falls back to false if the input is not an array.
18
+ # Creates a raw Proc that performs the element matching operation.
19
+ # The Proc checks if all elements in the array match the condition.
20
+ #
21
+ # @return [Proc] A proc that performs element matching with context awareness
22
+ # @note The proc includes error handling and context-based record conversion
23
+ def raw_proc
24
+ super_proc = super
25
+ need_convert = @context.need_convert
26
+ data_converter = Mongory.data_converter
20
27
 
21
- # @param collection [Object] the input to be tested
22
- # @return [Boolean] whether all element matches
23
- def match(collection)
24
- return false unless collection.is_a?(Array)
25
- return false if collection.empty?
28
+ Proc.new do |collection|
29
+ next false unless collection.is_a?(Array)
30
+ next false if collection.empty?
26
31
 
27
- collection.all? do |record|
28
- super(Mongory.data_converter.convert(record))
32
+ collection.all? do |record|
33
+ record = data_converter.convert(record) if need_convert
34
+ super_proc.call(record)
35
+ end
29
36
  end
30
37
  end
31
38
 
39
+ # Ensures the condition is a Hash.
40
+ #
41
+ # @raise [Mongory::TypeError] if the condition is not a Hash
42
+ # @return [void]
32
43
  def check_validity!
33
44
  raise TypeError, '$every needs a Hash.' unless @condition.is_a?(Hash)
34
45
 
@@ -17,21 +17,20 @@ module Mongory
17
17
  # matcher = ExistsMatcher.build(false)
18
18
  # matcher.match?(KEY_NOT_FOUND) #=> true
19
19
  #
20
- # @see AbstractOperatorMatcher
21
- class ExistsMatcher < AbstractOperatorMatcher
22
- # Converts the raw record value into a boolean indicating presence.
20
+ # @see AbstractMatcher
21
+ class ExistsMatcher < AbstractMatcher
22
+ # Creates a raw Proc that performs the existence check.
23
+ # The Proc checks if the record exists and compares it to the condition.
23
24
  #
24
- # @param record [Object] the value associated with the field
25
- # @return [Boolean] true if the key exists, false otherwise
26
- def preprocess(record)
27
- record != KEY_NOT_FOUND
28
- end
25
+ # @return [Proc] A proc that performs existence check with error handling
26
+ def raw_proc
27
+ condition = @condition
29
28
 
30
- # Uses Ruby's equality operator to compare presence against expected boolean.
31
- #
32
- # @return [Symbol] the comparison operator
33
- def operator
34
- :==
29
+ Proc.new do |record|
30
+ # Check if the record is nil or KEY_NOT_FOUND
31
+ # and compare it to the condition.
32
+ (record != KEY_NOT_FOUND) == condition
33
+ end
35
34
  end
36
35
 
37
36
  # Ensures that the condition value is a valid boolean.
@@ -39,7 +38,9 @@ module Mongory
39
38
  # @raise [TypeError] if condition is not true or false
40
39
  # @return [void]
41
40
  def check_validity!
42
- raise TypeError, '$exists needs a boolean' unless BOOLEAN_VALUES.include?(@condition)
41
+ return if [true, false].include?(@condition)
42
+
43
+ raise TypeError, "$exists needs a boolean, but got #{@condition.inspect}"
43
44
  end
44
45
  end
45
46
 
@@ -2,18 +2,22 @@
2
2
 
3
3
  module Mongory
4
4
  module Matchers
5
- # FieldMatcher is responsible for extracting a value from a record
6
- # using a field (or index) and then delegating the match to LiteralMatcher logic.
5
+ # FieldMatcher handles field-level matching by extracting and comparing field values.
7
6
  #
8
- # It handles nested access in structures like Hashes or Arrays, and guards
9
- # against types that should not be dig into (e.g., String, Symbol, Proc).
7
+ # This matcher is responsible for:
8
+ # 1. Extracting field values from records using dot notation
9
+ # 2. Converting extracted values if needed
10
+ # 3. Delegating the actual comparison to a submatcher
10
11
  #
11
- # This matcher is typically used when the query refers to a specific field,
12
- # like `{ age: { :$gte => 18 } }` where `:age` is passed as the dig field.
12
+ # It supports:
13
+ # - Hash records with string/symbol keys
14
+ # - Array records with numeric indices
15
+ # - Objects that respond to `[]`
13
16
  #
14
- # @example
15
- # matcher = FieldMatcher.build(:age, { :$gte => 18 })
16
- # matcher.match?({ age: 20 }) #=> true
17
+ # @example Basic field matching
18
+ # matcher = FieldMatcher.build('age', 30)
19
+ # matcher.match?({ 'age' => 30 }) #=> true
20
+ # matcher.match?({ age: 30 }) #=> true
17
21
  #
18
22
  # @see LiteralMatcher
19
23
  class FieldMatcher < LiteralMatcher
@@ -29,16 +33,18 @@ module Mongory
29
33
  ::Symbol
30
34
  ].freeze
31
35
 
32
- # Initializes the matcher with a target field and condition.
36
+ # Initializes a new field matcher.
33
37
  #
34
- # @param field [Object] the field (or index) used to dig into the record
35
- # @param condition [Object] the condition to match against the extracted value
36
- def initialize(field, condition)
38
+ # @param field [String, Symbol] the field to match against
39
+ # @param condition [Object] the condition to match with
40
+ # @param context [Context] the query context
41
+ def initialize(field, condition, context: Context.new)
37
42
  @field = field
38
- super(condition)
43
+ super(condition, context: context)
39
44
  end
40
45
 
41
- # Performs field-based matching against the given record.
46
+ # Creates a raw Proc that performs the field matching operation.
47
+ # The Proc extracts the field value and delegates to the submatcher.
42
48
  #
43
49
  # This method first ensures the record is structurally eligible for field extraction—
44
50
  # it must be a Hash, Array, or respond to `[]`. If the structure does not allow for
@@ -54,9 +60,6 @@ module Mongory
54
60
  # Once the value is extracted, it is passed through the data converter
55
61
  # and matched against the condition via the superclass.
56
62
  #
57
- # @param record [Object] the input data structure to be matched
58
- # @return [Boolean] true if the extracted field value matches the condition; false otherwise
59
- #
60
63
  # @example Matching a Hash with a nil field value
61
64
  # matcher = Mongory::QueryMatcher.new(a: nil)
62
65
  # matcher.match?({ a: nil }) # => true
@@ -72,27 +75,44 @@ module Mongory
72
75
  # @example Hash with symbol key, matcher uses string key
73
76
  # matcher = Mongory::QueryMatcher.new('a' => 123)
74
77
  # matcher.match?({ a: 123 }) # => true
75
- def match(record)
76
- sub_record =
77
- case record
78
- when Hash
79
- record.fetch(@field) do
80
- record.fetch(@field.to_sym, KEY_NOT_FOUND)
81
- end
82
- when Array
83
- record.fetch(@field, KEY_NOT_FOUND)
84
- when KEY_NOT_FOUND, *CLASSES_NOT_ALLOW_TO_DIG
85
- return false
86
- else
87
- return false unless record.respond_to?(:[])
88
78
 
89
- record[@field]
90
- end
79
+ # Creates a raw Proc that performs the field-based matching operation.
80
+ # The Proc extracts the field value and delegates matching to the superclass.
81
+ #
82
+ # @return [Proc] A proc that performs field-based matching with context awareness
83
+ # @note The proc handles field extraction and delegates matching to the superclass
84
+ def raw_proc
85
+ super_proc = super
86
+ field = @field
87
+ need_convert = @context.need_convert
88
+ data_converter = Mongory.data_converter
89
+
90
+ Proc.new do |record|
91
+ sub_record =
92
+ case record
93
+ when Hash
94
+ record.fetch(field) do
95
+ record.fetch(field.to_sym, KEY_NOT_FOUND)
96
+ end
97
+ when Array
98
+ record.fetch(field, KEY_NOT_FOUND)
99
+ when KEY_NOT_FOUND, *CLASSES_NOT_ALLOW_TO_DIG
100
+ next false
101
+ else
102
+ next false unless record.respond_to?(:[])
91
103
 
92
- super(Mongory.data_converter.convert(sub_record))
104
+ record[field]
105
+ end
106
+
107
+ sub_record = data_converter.convert(sub_record) if need_convert
108
+ super_proc.call(sub_record)
109
+ end
93
110
  end
94
111
 
95
- # @return [String] a deduplication field used for matchers inside multi-match constructs
112
+ # Returns a unique key for this matcher, including the field name.
113
+ # Used for deduplication in multi-matchers.
114
+ #
115
+ # @return [String] a unique key for this matcher
96
116
  # @see AbstractMultiMatcher#matchers
97
117
  def uniq_key
98
118
  super + "field:#{@field}"
@@ -100,9 +120,9 @@ module Mongory
100
120
 
101
121
  private
102
122
 
103
- # Returns a single-line summary of the dig matcher including the field and condition.
123
+ # Returns a single-line summary of the field matcher including the field and condition.
104
124
  #
105
- # @return [String]
125
+ # @return [String] a formatted title for tree display
106
126
  def tree_title
107
127
  "Field: #{@field.inspect} to match: #{@condition.inspect}"
108
128
  end
@@ -111,7 +131,7 @@ module Mongory
111
131
  #
112
132
  # @param record [Object] the input record
113
133
  # @param result [Boolean] match result
114
- # @return [String] formatted debug string
134
+ # @return [String] formatted debug string with highlighted field
115
135
  def debug_display(record, result)
116
136
  "#{self.class.name.split('::').last} #{colored_result(result)}, " \
117
137
  "condition: #{@condition.inspect}, " \
@@ -6,7 +6,7 @@ module Mongory
6
6
  #
7
7
  # It returns true if the record is strictly greater than the condition.
8
8
  #
9
- # Inherits core logic from AbstractOperatorMatcher, including
9
+ # Inherits core logic from AbstractMatcher, including
10
10
  # error handling and optional preprocessing.
11
11
  #
12
12
  # @example
@@ -14,13 +14,21 @@ module Mongory
14
14
  # matcher.match?(15) #=> true
15
15
  # matcher.match?(10) #=> false
16
16
  #
17
- # @see AbstractOperatorMatcher
18
- class GtMatcher < AbstractOperatorMatcher
19
- # Returns the Ruby `>` operator symbol for comparison.
17
+ # @see AbstractMatcher
18
+ class GtMatcher < AbstractMatcher
19
+ # Creates a raw Proc that performs the greater-than comparison.
20
+ # The Proc uses the `>` operator to compare values.
20
21
  #
21
- # @return [Symbol] the greater-than operator
22
- def operator
23
- :>
22
+ # @return [Proc] A proc that performs greater-than comparison with error handling
23
+ # @note The proc includes error handling for invalid comparisons
24
+ def raw_proc
25
+ condition = @condition
26
+
27
+ Proc.new do |record|
28
+ record > condition
29
+ rescue StandardError
30
+ false
31
+ end
24
32
  end
25
33
  end
26
34
 
@@ -6,7 +6,7 @@ module Mongory
6
6
  #
7
7
  # It returns true if the record is greater than or equal to the condition value.
8
8
  #
9
- # Inherits comparison logic and error safety from AbstractOperatorMatcher.
9
+ # Inherits comparison logic and error safety from AbstractMatcher.
10
10
  #
11
11
  # @example
12
12
  # matcher = GteMatcher.build(10)
@@ -14,13 +14,21 @@ module Mongory
14
14
  # matcher.match?(11) #=> true
15
15
  # matcher.match?(9) #=> false
16
16
  #
17
- # @see AbstractOperatorMatcher
18
- class GteMatcher < AbstractOperatorMatcher
19
- # Returns the Ruby `>=` operator symbol for comparison.
17
+ # @see AbstractMatcher
18
+ class GteMatcher < AbstractMatcher
19
+ # Creates a raw Proc that performs the greater-than-or-equal comparison.
20
+ # The Proc uses the `>=` operator to compare values.
20
21
  #
21
- # @return [Symbol] the greater-than-or-equal operator
22
- def operator
23
- :>=
22
+ # @return [Proc] A proc that performs greater-than-or-equal comparison with error handling
23
+ # @note The proc includes error handling for invalid comparisons
24
+ def raw_proc
25
+ condition = @condition
26
+
27
+ Proc.new do |record|
28
+ record >= condition
29
+ rescue StandardError
30
+ false
31
+ end
24
32
  end
25
33
  end
26
34
 
@@ -9,46 +9,74 @@ module Mongory
9
9
  #
10
10
  # Each subcondition is matched independently using the `:all?` strategy, meaning
11
11
  # all subconditions must match for the entire HashConditionMatcher to succeed.
12
+ # For empty conditions, it returns true (using TRUE_PROC).
12
13
  #
13
14
  # This matcher plays a central role in dispatching symbolic query conditions
14
15
  # to the appropriate field or operator matcher.
15
16
  #
16
- # @example
17
+ # @example Basic field matching
17
18
  # matcher = HashConditionMatcher.build({ age: { :$gt => 30 }, active: true })
18
19
  # matcher.match?(record) #=> true only if all subconditions match
19
20
  #
21
+ # @example Empty conditions
22
+ # matcher = HashConditionMatcher.build({})
23
+ # matcher.match?(record) #=> true (uses TRUE_PROC)
24
+ #
20
25
  # @see AbstractMultiMatcher
21
26
  class HashConditionMatcher < AbstractMultiMatcher
22
27
  enable_unwrap!
23
- # Constructs the appropriate submatcher for a key-value pair.
24
- # If the key is a registered operator, dispatches to the corresponding matcher.
25
- # Otherwise, assumes the key is a field path and uses FieldMatcher.
26
-
27
- # @see FieldMatcher
28
- # @see Matchers.lookup
29
- # @param key [String] the condition key (either an operator or field name)
30
- # @param value [Object] the condition value
31
- # @return [AbstractMatcher] a matcher instance
32
- def build_sub_matcher(key, value)
33
- case key
34
- when *Matchers.operators
35
- # If the key is a recognized operator, use the corresponding matcher
36
- # to handle the value.
37
- # This allows for nested conditions like { :$and => [{ age: { :$gt => 30 } }] }
38
- # or { :$or => [{ name: 'John' }, { age: { :$lt => 25 } }] }
39
- # The operator matcher is built using the value.
40
- Matchers.lookup(key).build(value)
41
- else
42
- FieldMatcher.build(key, value)
28
+
29
+ # Creates a raw Proc that performs the hash condition matching operation.
30
+ # The Proc combines all submatcher Procs and returns true only if all match.
31
+ # For empty conditions, returns TRUE_PROC.
32
+ #
33
+ # @return [Proc] a Proc that performs the hash condition matching operation
34
+ def raw_proc
35
+ return TRUE_PROC if matchers.empty?
36
+
37
+ combine_procs(*matchers.map(&:to_proc))
38
+ end
39
+
40
+ # Recursively combines multiple matcher procs with AND logic.
41
+ # This method optimizes the combination of multiple matchers by building
42
+ # a balanced tree of AND operations.
43
+ #
44
+ # @param left [Proc] The left matcher proc to combine
45
+ # @param rest [Array<Proc>] The remaining matcher procs to combine
46
+ # @return [Proc] A new proc that combines all matchers with AND logic
47
+ # @example
48
+ # combine_procs(proc1, proc2, proc3)
49
+ # #=> proc { |record| proc1.call(record) && proc2.call(record) && proc3.call(record) }
50
+ def combine_procs(left, *rest)
51
+ return left if rest.empty?
52
+
53
+ right = combine_procs(*rest)
54
+ Proc.new do |record|
55
+ left.call(record) && right.call(record)
43
56
  end
44
57
  end
45
58
 
46
- # Specifies the matching strategy for all subconditions.
47
- # Uses `:all?`, meaning all conditions must be satisfied.
59
+ # Returns the list of matchers for each key-value pair in the condition.
60
+ #
61
+ # For each pair:
62
+ # - If the key is a registered operator, uses the corresponding matcher
63
+ # - Otherwise, wraps the value in a FieldMatcher for field path matching
48
64
  #
49
- # @return [Symbol] the combining operator method
50
- def operator
51
- :all?
65
+ # @return [Array<AbstractMatcher>] List of matchers for each condition
66
+ define_instance_cache_method(:matchers) do
67
+ @condition.map do |key, value|
68
+ if (matcher_class = Matchers.lookup(key))
69
+ matcher_class.build(value, context: @context)
70
+ else
71
+ FieldMatcher.build(key, value, context: @context)
72
+ end
73
+ end
74
+ end
75
+
76
+ def check_validity!
77
+ return super if @condition.is_a?(Hash)
78
+
79
+ raise TypeError, 'condition needs a Hash.'
52
80
  end
53
81
  end
54
82
  end
@@ -24,26 +24,33 @@ module Mongory
24
24
  #
25
25
  # @see AbstractMatcher
26
26
  class InMatcher < AbstractMatcher
27
- # Matches if any element of the record appears in the condition array.
28
- # Converts record to an array before intersecting.
27
+ # Creates a raw Proc that performs the in-matching operation.
28
+ # The Proc checks if any element of the record is in the condition array.
29
29
  #
30
- # @param record [Object] the record value to test
31
- # @return [Boolean] whether any values intersect
32
- def match(record)
33
- record = normalize(record)
34
- if record.is_a?(Array)
35
- is_present?(@condition & record)
36
- else
37
- @condition.include?(record)
30
+ # @return [Proc] a Proc that performs the in-matching operation
31
+ def raw_proc
32
+ condition = @condition
33
+
34
+ Proc.new do |record|
35
+ if record.is_a?(Array)
36
+ return false if condition.is_a?(Range)
37
+
38
+ is_present?(condition & record)
39
+ else
40
+ condition.include?(record)
41
+ end
38
42
  end
39
43
  end
40
44
 
41
- # Ensures the condition is an array.
45
+ # Ensures the condition is an array or range.
42
46
  #
43
- # @raise [TypeError] if condition is not an array
47
+ # @raise [TypeError] if condition is not an array nor a range
44
48
  # @return [void]
45
49
  def check_validity!
46
- raise TypeError, '$in needs an array' unless @condition.is_a?(Array)
50
+ return if @condition.is_a?(Array)
51
+ return if @condition.is_a?(Range)
52
+
53
+ raise TypeError, '$in needs an array or range'
47
54
  end
48
55
  end
49
56