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
@@ -7,23 +7,49 @@ module Mongory
7
7
  # It defines a common interface (`#match?`) and provides shared behavior
8
8
  # such as condition storage, optional conversion handling, and debugging output.
9
9
  #
10
- # Subclasses are expected to implement `#match(record)` to define their matching logic.
10
+ # Each matcher is responsible for evaluating a specific type of condition against
11
+ # a record value. The base class provides infrastructure for:
12
+ # - Condition validation
13
+ # - Value conversion
14
+ # - Debug output
15
+ # - Proc caching
11
16
  #
12
- # This class also supports caching of lazily-built matchers via `define_matcher`.
17
+ # @abstract Subclasses must implement {#match} to define their matching logic
13
18
  #
14
- # @abstract
19
+ # @example Basic matcher implementation
20
+ # class MyMatcher < AbstractMatcher
21
+ # def match(record)
22
+ # record == @condition
23
+ # end
24
+ # end
25
+ #
26
+ # @example Using a matcher
27
+ # matcher = MyMatcher.build(42)
28
+ # matcher.match?(42) #=> true
29
+ # matcher.match?(43) #=> false
30
+ #
31
+ # @see Mongory::Matchers for the full list of available matchers
15
32
  class AbstractMatcher
16
33
  include Utils
17
34
 
18
35
  singleton_class.alias_method :build, :new
36
+
19
37
  # Sentinel value used to represent missing keys when traversing nested hashes.
38
+ # This is used instead of nil to distinguish between missing keys and nil values.
39
+ #
40
+ # @api private
20
41
  KEY_NOT_FOUND = SingletonBuilder.new('KEY_NOT_FOUND')
21
42
 
22
43
  # Defines a lazily-evaluated matcher accessor with instance-level caching.
44
+ # This is used to create cached accessors for submatcher instances.
23
45
  #
24
46
  # @param name [Symbol] the name of the matcher (e.g., :collection)
25
47
  # @yield the block that constructs the matcher instance
26
48
  # @return [void]
49
+ # @example
50
+ # define_matcher(:array_matcher) do
51
+ # ArrayMatcher.build(@condition)
52
+ # end
27
53
  def self.define_matcher(name, &block)
28
54
  define_instance_cache_method(:"#{name}_matcher", &block)
29
55
  end
@@ -31,7 +57,13 @@ module Mongory
31
57
  # @return [Object] the raw condition this matcher was initialized with
32
58
  attr_reader :condition
33
59
 
34
- # @return [String] a unique key representing this matcher instance, used for deduplication
60
+ # @return [Context] the query context containing configuration and current record
61
+ attr_reader :context
62
+
63
+ # Returns a unique key representing this matcher instance.
64
+ # Used for deduplication in multi-matchers.
65
+ #
66
+ # @return [String] a unique key for this matcher instance
35
67
  # @see AbstractMultiMatcher#matchers
36
68
  def uniq_key
37
69
  "#{self.class}:condition:#{@condition.class}:#{@condition}"
@@ -40,56 +72,82 @@ module Mongory
40
72
  # Initializes the matcher with a raw condition.
41
73
  #
42
74
  # @param condition [Object] the condition to match against
43
- def initialize(condition)
75
+ # @param context [Context] the query context containing configuration
76
+ def initialize(condition, context: Context.new)
44
77
  @condition = condition
78
+ @context = context
45
79
 
46
80
  check_validity!
47
81
  end
48
82
 
49
- # Performs the actual match logic.
50
- # Subclasses must override this method.
51
- #
52
- # @param record [Object] the input record to test
53
- # @return [Boolean] whether the record matches the condition
54
- def match(record); end
55
-
56
83
  # Matches the given record against the condition.
84
+ # This method handles error cases and uses the cached proc for performance.
57
85
  #
58
86
  # @param record [Object] the input record
59
- # @return [Boolean]
87
+ # @return [Boolean] whether the record matches the condition
60
88
  def match?(record)
61
- match(record)
89
+ to_proc.call(record)
62
90
  rescue StandardError
63
91
  false
64
92
  end
65
93
 
66
- # Provides an alias to `#match?` for internal delegation.
67
- alias_method :regular_match, :match?
94
+ # Converts the matcher into a Proc that can be used for matching.
95
+ # The Proc is cached for better performance.
96
+ #
97
+ # @return [Proc] a Proc that can be used to match records
98
+ def cached_proc
99
+ @cached_proc ||= raw_proc
100
+ end
68
101
 
69
- # Evaluates the match with debugging output.
70
- # Increments indent level and prints visual result with colors.
102
+ alias_method :to_proc, :cached_proc
103
+
104
+ # Creates a debug-enabled version of the matcher proc.
105
+ # This version includes tracing and error handling.
71
106
  #
72
- # @param record [Object] the input record to test
73
- # @return [Boolean] whether the match succeeded
74
- def debug_match(record)
75
- result = nil
76
-
77
- Debugger.instance.with_indent do
78
- result = begin
79
- match(record)
80
- rescue StandardError => e
81
- e
107
+ # @return [Proc] a debug-enabled version of the matcher proc
108
+ def debug_proc
109
+ return @debug_proc if defined?(@debug_proc)
110
+
111
+ raw_proc = raw_proc()
112
+ @debug_proc = Proc.new do |record|
113
+ result = nil
114
+
115
+ Debugger.instance.with_indent do
116
+ result = begin
117
+ raw_proc.call(record)
118
+ rescue StandardError => e
119
+ e
120
+ end
121
+
122
+ debug_display(record, result)
82
123
  end
83
124
 
84
- debug_display(record, result)
125
+ result.is_a?(Exception) ? false : result
85
126
  end
127
+ end
86
128
 
87
- result.is_a?(Exception) ? false : result
129
+ # Creates a raw Proc from the match method.
130
+ # This is used internally by to_proc and can be overridden by subclasses
131
+ # to provide custom matching behavior.
132
+ #
133
+ # @return [Proc] the raw Proc implementation of the match method
134
+ def raw_proc
135
+ method(:match).to_proc
88
136
  end
89
137
 
138
+ # Performs the actual match logic.
139
+ # Subclasses must override this method.
140
+ #
141
+ # @abstract
142
+ # @param record [Object] the input record to test
143
+ # @return [Boolean] whether the record matches the condition
144
+ def match(record); end
145
+
90
146
  # Validates the condition (no-op by default).
91
147
  # Override in subclasses to raise error if invalid.
92
148
  #
149
+ # @abstract
150
+ # @raise [TypeError] if the condition is invalid
93
151
  # @return [void]
94
152
  def check_validity!; end
95
153
 
@@ -108,7 +166,7 @@ module Mongory
108
166
  # Returns a single-line string representing this matcher in the tree output.
109
167
  # Format: `<MatcherType>: <condition.inspect>`
110
168
  #
111
- # @return [String]
169
+ # @return [String] a formatted title for tree display
112
170
  def tree_title
113
171
  matcher_name = self.class.name.split('::').last.sub('Matcher', '')
114
172
  "#{matcher_name}: #{@condition.inspect}"
@@ -131,7 +189,7 @@ module Mongory
131
189
  # Uses ANSI escape codes to highlight matched vs. mismatched records.
132
190
  #
133
191
  # @param record [Object] the record being tested
134
- # @param result [Boolean] whether the match succeeded
192
+ # @param result [Boolean, Exception] whether the match succeeded or an error occurred
135
193
  # @return [String] the formatted output string
136
194
  def debug_display(record, result)
137
195
  "#{self.class.name.split('::').last} #{colored_result(result)}, " \
@@ -139,6 +197,10 @@ module Mongory
139
197
  "record: #{record.inspect}"
140
198
  end
141
199
 
200
+ # Formats the match result with ANSI color codes for terminal output.
201
+ #
202
+ # @param result [Boolean, Exception] the match result or error
203
+ # @return [String] the colored result string
142
204
  def colored_result(result)
143
205
  if result.is_a?(Exception)
144
206
  "\e[45;97m#{result}\e[0m"
@@ -16,6 +16,14 @@ module Mongory
16
16
  # @abstract
17
17
  # @see AbstractMatcher
18
18
  class AbstractMultiMatcher < AbstractMatcher
19
+ # A Proc that always returns true, used as a default for empty AND conditions
20
+ # @return [Proc] A proc that always returns true
21
+ TRUE_PROC = Proc.new { |_| true }
22
+
23
+ # A Proc that always returns false, used as a default for empty OR conditions
24
+ # @return [Proc] A proc that always returns false
25
+ FALSE_PROC = Proc.new { |_| false }
26
+
19
27
  # Enables auto-unwrap logic.
20
28
  # When used, `.build` may unwrap to first matcher if only one is present.
21
29
  #
@@ -29,61 +37,24 @@ module Mongory
29
37
  private_class_method :enable_unwrap!
30
38
 
31
39
  # Builds a matcher and conditionally unwraps it.
40
+ # If unwrapping is enabled and there is only one submatcher,
41
+ # returns that submatcher instead of the multi-matcher wrapper.
32
42
  #
33
43
  # @param args [Array] arguments passed to the constructor
34
- # @return [AbstractMatcher]
35
- def self.build_or_unwrap(*args)
36
- matcher = new(*args)
44
+ # @param context [Context] the query context
45
+ # @return [AbstractMatcher] the constructed matcher or its unwrapped submatcher
46
+ def self.build_or_unwrap(*args, context: Context.new)
47
+ matcher = new(*args, context: context)
37
48
  return matcher unless @enable_unwrap
38
49
 
39
50
  matcher = matcher.matchers.first if matcher.matchers.count == 1
40
51
  matcher
41
52
  end
42
53
 
43
- # Performs matching over all sub-matchers using the specified operator.
44
- # The input record may be preprocessed first (e.g., for normalization).
45
- #
46
- # @param record [Object] the record to match
47
- # @return [Boolean] whether the combined result of sub-matchers satisfies the condition
48
- def match(record)
49
- record = preprocess(record)
50
- matchers.send(operator) do |matcher|
51
- matcher.match?(record)
52
- end
53
- end
54
-
55
- # Lazily builds and caches the array of sub-matchers.
56
- # Subclasses provide the implementation of `#build_sub_matcher`.
57
- # Duplicate matchers (by uniq_key) are removed to avoid redundancy.
58
- #
59
- # @return [Array<AbstractMatcher>] list of sub-matchers
60
- define_instance_cache_method(:matchers) do
61
- @condition.map(&method(:build_sub_matcher)).uniq(&:uniq_key)
62
- end
63
-
64
- # Optional hook for subclasses to transform the input record before matching.
65
- # Default implementation returns the record unchanged.
66
- #
67
- # @param record [Object] the input record
68
- # @return [Object] the transformed or original record
69
- def preprocess(record)
70
- record
71
- end
72
-
73
- # Abstract method to define how each subcondition should be turned into a matcher.
74
- #
75
- # @param args [Array] the inputs needed to construct a matcher
76
- # @return [AbstractMatcher] a matcher instance for the subcondition
77
- def build_sub_matcher(*args); end
78
-
79
- # Abstract method to specify the combining operator for sub-matchers.
80
- # Must return a valid enumerable method name (e.g., :all?, :any?).
81
- #
82
- # @return [Symbol] the operator method to apply over matchers
83
- def operator; end
84
-
85
54
  # Recursively checks all submatchers for validity.
55
+ # Raises an error if any submatcher is invalid.
86
56
  #
57
+ # @raise [Mongory::TypeError] if any submatcher is invalid
87
58
  # @return [void]
88
59
  def check_validity!
89
60
  matchers.each(&:check_validity!)
@@ -5,6 +5,7 @@ module Mongory
5
5
  # AndMatcher implements the `$and` logical operator.
6
6
  #
7
7
  # It evaluates an array of subconditions and returns true only if *all* of them match.
8
+ # For empty conditions, it returns true (using TRUE_PROC), following MongoDB's behavior.
8
9
  #
9
10
  # Unlike other matchers, AndMatcher flattens the underlying matcher tree by
10
11
  # delegating each subcondition to a `HashConditionMatcher`, and further extracting
@@ -12,39 +13,66 @@ module Mongory
12
13
  #
13
14
  # This allows the matcher trace (`.explain`) to render as a flat list of independent conditions.
14
15
  #
15
- # @example
16
+ # @example Basic usage
16
17
  # matcher = AndMatcher.build([
17
18
  # { age: { :$gte => 18 } },
18
19
  # { name: /foo/ }
19
20
  # ])
20
21
  # matcher.match?(record) #=> true if both match
21
22
  #
23
+ # @example Empty conditions
24
+ # matcher = AndMatcher.build([])
25
+ # matcher.match?(record) #=> true (uses TRUE_PROC)
26
+ #
22
27
  # @see AbstractMultiMatcher
23
28
  class AndMatcher < AbstractMultiMatcher
24
29
  # Constructs a HashConditionMatcher for each subcondition.
25
30
  # Conversion is disabled to avoid double-processing.
26
31
  enable_unwrap!
27
32
 
33
+ # Creates a raw Proc that performs the AND operation.
34
+ # The Proc combines all subcondition Procs and returns true only if all match.
35
+ # For empty conditions, returns TRUE_PROC.
36
+ #
37
+ # @return [Proc] a Proc that performs the AND operation
38
+ def raw_proc
39
+ return TRUE_PROC if matchers.empty?
40
+
41
+ combine_procs(*matchers.map(&:to_proc))
42
+ end
43
+
44
+ # Recursively combines multiple matcher procs with AND logic.
45
+ # This method optimizes the combination of multiple matchers by building
46
+ # a balanced tree of AND operations.
47
+ #
48
+ # @param left [Proc] The left matcher proc to combine
49
+ # @param rest [Array<Proc>] The remaining matcher procs to combine
50
+ # @return [Proc] A new proc that combines all matchers with AND logic
51
+ # @example
52
+ # combine_procs(proc1, proc2, proc3)
53
+ # #=> proc { |record| proc1.call(record) && proc2.call(record) && proc3.call(record) }
54
+ def combine_procs(left, *rest)
55
+ return left if rest.empty?
56
+
57
+ right = combine_procs(*rest)
58
+ Proc.new do |record|
59
+ left.call(record) && right.call(record)
60
+ end
61
+ end
62
+
28
63
  # Returns the flattened list of all matchers from each subcondition.
29
64
  #
30
65
  # Each condition is passed to a HashConditionMatcher, then recursively flattened.
31
66
  # All matchers are then deduplicated using `uniq_key`.
32
67
  #
33
- # @return [Array<AbstractMatcher>]
68
+ # @return [Array<AbstractMatcher>] A flattened, deduplicated list of matchers
34
69
  # @see AbstractMatcher#uniq_key
35
70
  define_instance_cache_method(:matchers) do
36
71
  @condition.flat_map do |condition|
37
- HashConditionMatcher.new(condition).matchers
72
+ HashConditionMatcher.new(condition, context: @context).matchers
38
73
  end.uniq(&:uniq_key)
39
74
  end
40
75
 
41
- # Combines submatcher results using `:all?`.
42
- #
43
- # @return [Symbol]
44
- def operator
45
- :all?
46
- end
47
-
48
76
  # Ensures the condition is an array of hashes.
49
77
  #
50
78
  # @raise [Mongory::TypeError] if not valid
@@ -2,29 +2,62 @@
2
2
 
3
3
  module Mongory
4
4
  module Matchers
5
- # ArrayRecordMatcher matches records where the record itself is an Array.
5
+ # ArrayRecordMatcher handles matching against array-type records.
6
6
  #
7
- # This matcher checks whether any element of the record array satisfies the expected condition.
8
- # It is typically used when the record is a collection of values, and the query condition
9
- # is either a scalar value or a subcondition matcher.
7
+ # This matcher is used when a field value is an array and needs to be matched
8
+ # against a condition. It supports both exact array matching and element-wise
9
+ # comparison through `$elemMatch`.
10
10
  #
11
- # @example Match when any element equals the expected value
12
- # matcher = ArrayRecordMatcher.build(42)
13
- # matcher.match?([10, 42, 99]) #=> true
11
+ # For empty conditions, it returns false (using FALSE_PROC).
14
12
  #
15
- # @example Match using a nested matcher (e.g. condition is a hash)
16
- # matcher = ArrayRecordMatcher.build({ '$gt' => 10 })
17
- # matcher.match?([5, 20, 3]) #=> true
13
+ # @example Match exact array
14
+ # matcher = ArrayRecordMatcher.build([1, 2, 3])
15
+ # matcher.match?([1, 2, 3]) #=> true
16
+ # matcher.match?([1, 2]) #=> false
18
17
  #
19
- # This matcher is automatically invoked by LiteralMatcher when the record value is an array.
18
+ # @example Match with hash condition
19
+ # matcher = ArrayRecordMatcher.build({ '$gt' => 5 })
20
+ # matcher.match?([3, 6, 9]) #=> true (6 and 9 match)
21
+ # matcher.match?([1, 2, 3]) #=> false
20
22
  #
21
- # @note This is distinct from `$in` or `$nin`, where the **condition** is an array.
22
- # Here, the **record** is the array being matched against.
23
+ # @example Empty conditions
24
+ # matcher = ArrayRecordMatcher.build([])
25
+ # matcher.match?(record) #=> false (uses FALSE_PROC)
23
26
  #
24
- # @see Mongory::Matchers::InMatcher
25
- # @see Mongory::Matchers::LiteralMatcher
27
+ # @see AbstractMultiMatcher
26
28
  class ArrayRecordMatcher < AbstractMultiMatcher
27
29
  enable_unwrap!
30
+
31
+ # Creates a raw Proc that performs the array matching operation.
32
+ # The Proc checks if any element in the array matches the condition.
33
+ # For empty conditions, returns FALSE_PROC.
34
+ #
35
+ # @return [Proc] a Proc that performs the array matching operation
36
+ def raw_proc
37
+ return FALSE_PROC if matchers.empty?
38
+
39
+ combine_procs(*matchers.map(&:to_proc))
40
+ end
41
+
42
+ # Recursively combines multiple matcher procs with OR logic.
43
+ # This method optimizes the combination of multiple matchers by building
44
+ # a balanced tree of OR operations.
45
+ #
46
+ # @param left [Proc] The left matcher proc to combine
47
+ # @param rest [Array<Proc>] The remaining matcher procs to combine
48
+ # @return [Proc] A new proc that combines all matchers with OR logic
49
+ # @example
50
+ # combine_procs(proc1, proc2, proc3)
51
+ # #=> proc { |record| proc1.call(record) || proc2.call(record) || proc3.call(record) }
52
+ def combine_procs(left, *rest)
53
+ return left if rest.empty?
54
+
55
+ right = combine_procs(*rest)
56
+ Proc.new do |record|
57
+ left.call(record) || right.call(record)
58
+ end
59
+ end
60
+
28
61
  # Builds an array of matchers to evaluate the given condition against an array record.
29
62
  #
30
63
  # This method returns multiple matchers that will be evaluated using `:any?` logic:
@@ -32,28 +65,21 @@ module Mongory
32
65
  # - A hash condition matcher if the condition is a hash
33
66
  # - An `$elemMatch` matcher for element-wise comparison
34
67
  #
35
- # @return [Array<Mongory::Matchers::AbstractMatcher>] an array of matcher instances
68
+ # @return [Array<AbstractMatcher>] An array of matcher instances
36
69
  define_instance_cache_method(:matchers) do
37
70
  result = []
38
- result << EqMatcher.build(@condition) if @condition.is_a?(Array)
71
+ result << EqMatcher.build(@condition, context: @context) if @condition.is_a?(Array)
39
72
  result << case @condition
40
73
  when Hash
41
- HashConditionMatcher.build(parsed_condition)
74
+ HashConditionMatcher.build(parsed_condition, context: @context)
42
75
  when Regexp
43
- ElemMatchMatcher.build('$regex' => @condition)
76
+ ElemMatchMatcher.build({ '$regex' => @condition }, context: @context)
44
77
  else
45
- ElemMatchMatcher.build('$eq' => @condition)
78
+ ElemMatchMatcher.build({ '$eq' => @condition }, context: @context)
46
79
  end
47
80
  result
48
81
  end
49
82
 
50
- # Combines results using `:any?` for multi-match logic.
51
- #
52
- # @return [Symbol]
53
- def operator
54
- :any?
55
- end
56
-
57
83
  private
58
84
 
59
85
  # Parses the original condition hash into a normalized structure suitable for HashConditionMatcher.
@@ -63,7 +89,7 @@ module Mongory
63
89
  # - Operator keys (e.g., `$size`, `$type`): retained at the top level
64
90
  # - All other keys: grouped under a `$elemMatch` clause for element-wise comparison
65
91
  #
66
- # @return [Hash] a normalized condition hash, potentially containing `$elemMatch`
92
+ # @return [Hash] A normalized condition hash, potentially containing `$elemMatch`
67
93
  def parsed_condition
68
94
  h_parsed = {}
69
95
  h_elem_match = {}
@@ -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