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
@@ -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
|
-
#
|
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
|
-
#
|
17
|
+
# @abstract Subclasses must implement {#match} to define their matching logic
|
13
18
|
#
|
14
|
-
# @
|
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 [
|
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
|
-
|
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
|
-
|
89
|
+
to_proc.call(record)
|
62
90
|
rescue StandardError
|
63
91
|
false
|
64
92
|
end
|
65
93
|
|
66
|
-
#
|
67
|
-
|
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
|
-
|
70
|
-
|
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
|
-
# @
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
result =
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
125
|
+
result.is_a?(Exception) ? false : result
|
85
126
|
end
|
127
|
+
end
|
86
128
|
|
87
|
-
|
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
|
-
# @
|
35
|
-
|
36
|
-
|
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
|
5
|
+
# ArrayRecordMatcher handles matching against array-type records.
|
6
6
|
#
|
7
|
-
# This matcher
|
8
|
-
#
|
9
|
-
#
|
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
|
-
#
|
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
|
16
|
-
# matcher = ArrayRecordMatcher.build(
|
17
|
-
# matcher.match?([
|
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
|
-
#
|
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
|
-
# @
|
22
|
-
#
|
23
|
+
# @example Empty conditions
|
24
|
+
# matcher = ArrayRecordMatcher.build([])
|
25
|
+
# matcher.match?(record) #=> false (uses FALSE_PROC)
|
23
26
|
#
|
24
|
-
# @see
|
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<
|
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]
|
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
|
-
#
|
22
|
-
#
|
23
|
-
|
24
|
-
# @
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
30
|
-
|
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
|
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
|
26
|
-
class EqMatcher <
|
27
|
-
#
|
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 [
|
30
|
-
def
|
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
|
-
#
|
19
|
-
#
|
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
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
28
|
-
|
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
|
21
|
-
class ExistsMatcher <
|
22
|
-
#
|
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
|
-
# @
|
25
|
-
|
26
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
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
|
|