mongory 0.3.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +84 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +246 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +517 -0
- data/Rakefile +12 -0
- data/examples/README.md +41 -0
- data/examples/benchmark.rb +44 -0
- data/lib/generators/mongory/install/install_generator.rb +42 -0
- data/lib/generators/mongory/install/templates/initializer.rb.erb +83 -0
- data/lib/generators/mongory/matcher/matcher_generator.rb +56 -0
- data/lib/generators/mongory/matcher/templates/matcher.rb.erb +92 -0
- data/lib/generators/mongory/matcher/templates/matcher_spec.rb.erb +17 -0
- data/lib/mongory/converters/abstract_converter.rb +122 -0
- data/lib/mongory/converters/condition_converter.rb +74 -0
- data/lib/mongory/converters/data_converter.rb +26 -0
- data/lib/mongory/converters/key_converter.rb +63 -0
- data/lib/mongory/converters/value_converter.rb +47 -0
- data/lib/mongory/converters.rb +7 -0
- data/lib/mongory/matchers/README.md +57 -0
- data/lib/mongory/matchers/abstract_matcher.rb +153 -0
- data/lib/mongory/matchers/abstract_multi_matcher.rb +109 -0
- data/lib/mongory/matchers/abstract_operator_matcher.rb +46 -0
- data/lib/mongory/matchers/and_matcher.rb +65 -0
- data/lib/mongory/matchers/array_record_matcher.rb +88 -0
- data/lib/mongory/matchers/elem_match_matcher.rb +47 -0
- data/lib/mongory/matchers/eq_matcher.rb +37 -0
- data/lib/mongory/matchers/every_matcher.rb +41 -0
- data/lib/mongory/matchers/exists_matcher.rb +48 -0
- data/lib/mongory/matchers/field_matcher.rb +123 -0
- data/lib/mongory/matchers/gt_matcher.rb +29 -0
- data/lib/mongory/matchers/gte_matcher.rb +29 -0
- data/lib/mongory/matchers/hash_condition_matcher.rb +55 -0
- data/lib/mongory/matchers/in_matcher.rb +52 -0
- data/lib/mongory/matchers/literal_matcher.rb +123 -0
- data/lib/mongory/matchers/lt_matcher.rb +29 -0
- data/lib/mongory/matchers/lte_matcher.rb +29 -0
- data/lib/mongory/matchers/ne_matcher.rb +29 -0
- data/lib/mongory/matchers/nin_matcher.rb +51 -0
- data/lib/mongory/matchers/not_matcher.rb +32 -0
- data/lib/mongory/matchers/or_matcher.rb +60 -0
- data/lib/mongory/matchers/present_matcher.rb +52 -0
- data/lib/mongory/matchers/regex_matcher.rb +61 -0
- data/lib/mongory/matchers.rb +176 -0
- data/lib/mongory/mongoid.rb +19 -0
- data/lib/mongory/query_builder.rb +187 -0
- data/lib/mongory/query_matcher.rb +66 -0
- data/lib/mongory/query_operator.rb +28 -0
- data/lib/mongory/rails.rb +15 -0
- data/lib/mongory/utils/debugger.rb +123 -0
- data/lib/mongory/utils/rails_patch.rb +22 -0
- data/lib/mongory/utils/singleton_builder.rb +31 -0
- data/lib/mongory/utils.rb +75 -0
- data/lib/mongory/version.rb +5 -0
- data/lib/mongory-rb.rb +3 -0
- data/lib/mongory.rb +116 -0
- data/mongory.gemspec +40 -0
- data/sig/mongory.rbs +4 -0
- metadata +108 -0
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# AbstractMatcher is the base class for all matchers in Mongory.
|
6
|
+
#
|
7
|
+
# It defines a common interface (`#match?`) and provides shared behavior
|
8
|
+
# such as condition storage, optional conversion handling, and debugging output.
|
9
|
+
#
|
10
|
+
# Subclasses are expected to implement `#match(record)` to define their matching logic.
|
11
|
+
#
|
12
|
+
# This class also supports caching of lazily-built matchers via `define_matcher`.
|
13
|
+
#
|
14
|
+
# @abstract
|
15
|
+
class AbstractMatcher
|
16
|
+
include Utils
|
17
|
+
|
18
|
+
singleton_class.alias_method :build, :new
|
19
|
+
# Sentinel value used to represent missing keys when traversing nested hashes.
|
20
|
+
KEY_NOT_FOUND = SingletonBuilder.new('KEY_NOT_FOUND')
|
21
|
+
|
22
|
+
# Defines a lazily-evaluated matcher accessor with instance-level caching.
|
23
|
+
#
|
24
|
+
# @param name [Symbol] the name of the matcher (e.g., :collection)
|
25
|
+
# @yield the block that constructs the matcher instance
|
26
|
+
# @return [void]
|
27
|
+
def self.define_matcher(name, &block)
|
28
|
+
define_instance_cache_method(:"#{name}_matcher", &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Object] the raw condition this matcher was initialized with
|
32
|
+
attr_reader :condition
|
33
|
+
|
34
|
+
# @return [String] a unique key representing this matcher instance, used for deduplication
|
35
|
+
# @see AbstractMultiMatcher#matchers
|
36
|
+
def uniq_key
|
37
|
+
"#{self.class}:condition:#{@condition.class}:#{@condition}"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Initializes the matcher with a raw condition.
|
41
|
+
#
|
42
|
+
# @param condition [Object] the condition to match against
|
43
|
+
def initialize(condition)
|
44
|
+
@condition = condition
|
45
|
+
|
46
|
+
check_validity!
|
47
|
+
end
|
48
|
+
|
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
|
+
# Matches the given record against the condition.
|
57
|
+
#
|
58
|
+
# @param record [Object] the input record
|
59
|
+
# @return [Boolean]
|
60
|
+
def match?(record)
|
61
|
+
match(record)
|
62
|
+
rescue StandardError
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
# Provides an alias to `#match?` for internal delegation.
|
67
|
+
alias_method :regular_match, :match?
|
68
|
+
|
69
|
+
# Evaluates the match with debugging output.
|
70
|
+
# Increments indent level and prints visual result with colors.
|
71
|
+
#
|
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
|
82
|
+
end
|
83
|
+
|
84
|
+
debug_display(record, result)
|
85
|
+
end
|
86
|
+
|
87
|
+
result.is_a?(Exception) ? false : result
|
88
|
+
end
|
89
|
+
|
90
|
+
# Validates the condition (no-op by default).
|
91
|
+
# Override in subclasses to raise error if invalid.
|
92
|
+
#
|
93
|
+
# @return [void]
|
94
|
+
def check_validity!; end
|
95
|
+
|
96
|
+
# Recursively prints the matcher structure into a formatted tree.
|
97
|
+
# Supports indentation and branching layout using prefix symbols.
|
98
|
+
#
|
99
|
+
# @param prefix [String] tree prefix (indentation + lines)
|
100
|
+
# @param is_last [Boolean] whether this node is the last among siblings
|
101
|
+
# @return [void]
|
102
|
+
def render_tree(prefix = '', is_last: true)
|
103
|
+
puts "#{prefix}#{is_last ? '└─ ' : '├─ '}#{tree_title}\n"
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
# Returns a single-line string representing this matcher in the tree output.
|
109
|
+
# Format: `<MatcherType>: <condition.inspect>`
|
110
|
+
#
|
111
|
+
# @return [String]
|
112
|
+
def tree_title
|
113
|
+
matcher_name = self.class.name.split('::').last.sub('Matcher', '')
|
114
|
+
"#{matcher_name}: #{@condition.inspect}"
|
115
|
+
end
|
116
|
+
|
117
|
+
# Normalizes the record before matching.
|
118
|
+
#
|
119
|
+
# If the record is the KEY_NOT_FOUND sentinel (representing a missing field),
|
120
|
+
# it is converted to `nil` so matchers can interpret it consistently.
|
121
|
+
# Other values are returned as-is.
|
122
|
+
#
|
123
|
+
# @param record [Object] the record value to normalize
|
124
|
+
# @return [Object, nil] the normalized record
|
125
|
+
# @see Mongory::KEY_NOT_FOUND
|
126
|
+
def normalize(record)
|
127
|
+
record == KEY_NOT_FOUND ? nil : record
|
128
|
+
end
|
129
|
+
|
130
|
+
# Formats a debug string for match output.
|
131
|
+
# Uses ANSI escape codes to highlight matched vs. mismatched records.
|
132
|
+
#
|
133
|
+
# @param record [Object] the record being tested
|
134
|
+
# @param result [Boolean] whether the match succeeded
|
135
|
+
# @return [String] the formatted output string
|
136
|
+
def debug_display(record, result)
|
137
|
+
"#{self.class.name.split('::').last} #{colored_result(result)}, " \
|
138
|
+
"condition: #{@condition.inspect}, " \
|
139
|
+
"record: #{record.inspect}"
|
140
|
+
end
|
141
|
+
|
142
|
+
def colored_result(result)
|
143
|
+
if result.is_a?(Exception)
|
144
|
+
"\e[45;97m#{result}\e[0m"
|
145
|
+
elsif result
|
146
|
+
"\e[30;42mMatched\e[0m"
|
147
|
+
else
|
148
|
+
"\e[30;41mDismatch\e[0m"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# AbstractMultiMatcher is an abstract base class for matchers that operate
|
6
|
+
# on multiple subconditions. It provides a generic match loop that applies
|
7
|
+
# a logical operator (e.g., `all?`, `any?`) over a list of sub-matchers.
|
8
|
+
#
|
9
|
+
# Subclasses must define two methods:
|
10
|
+
# - `#build_sub_matcher`: how to construct a matcher from each condition
|
11
|
+
# - `#operator`: which enumerator method to use (e.g., :all?, :any?)
|
12
|
+
#
|
13
|
+
# Sub-matchers are cached using `define_instance_cache_method` to prevent
|
14
|
+
# repeated construction.
|
15
|
+
#
|
16
|
+
# @abstract
|
17
|
+
# @see AbstractMatcher
|
18
|
+
class AbstractMultiMatcher < AbstractMatcher
|
19
|
+
# Enables auto-unwrap logic.
|
20
|
+
# When used, `.build` may unwrap to first matcher if only one is present.
|
21
|
+
#
|
22
|
+
# @private
|
23
|
+
# @return [void]
|
24
|
+
def self.enable_unwrap!
|
25
|
+
@enable_unwrap = true
|
26
|
+
singleton_class.alias_method :build, :build_or_unwrap
|
27
|
+
end
|
28
|
+
|
29
|
+
private_class_method :enable_unwrap!
|
30
|
+
|
31
|
+
# Builds a matcher and conditionally unwraps it.
|
32
|
+
#
|
33
|
+
# @param args [Array] arguments passed to the constructor
|
34
|
+
# @return [AbstractMatcher]
|
35
|
+
def self.build_or_unwrap(*args)
|
36
|
+
matcher = new(*args)
|
37
|
+
return matcher unless @enable_unwrap
|
38
|
+
|
39
|
+
matcher = matcher.matchers.first if matcher.matchers.count == 1
|
40
|
+
matcher
|
41
|
+
end
|
42
|
+
|
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
|
+
# Recursively checks all submatchers for validity.
|
86
|
+
#
|
87
|
+
# @return [void]
|
88
|
+
def check_validity!
|
89
|
+
matchers.each(&:check_validity!)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Overrides base render_tree to recursively print all submatchers.
|
93
|
+
# Each child matcher will be displayed under this multi-matcher node.
|
94
|
+
#
|
95
|
+
# @param prefix [String] current line prefix for tree alignment
|
96
|
+
# @param is_last [Boolean] whether this node is the last sibling
|
97
|
+
# @return [void]
|
98
|
+
def render_tree(prefix = '', is_last: true)
|
99
|
+
super
|
100
|
+
|
101
|
+
new_prefix = "#{prefix}#{is_last ? ' ' : '│ '}"
|
102
|
+
last_index = matchers.count - 1
|
103
|
+
matchers.each_with_index do |matcher, index|
|
104
|
+
matcher.render_tree(new_prefix, is_last: index == last_index)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# AbstractOperatorMatcher is a base class for matchers that apply a binary
|
6
|
+
# operator (e.g., `==`, `<`, `>` etc.) between the record and the condition.
|
7
|
+
#
|
8
|
+
# This class assumes that the match logic consists of:
|
9
|
+
# preprocess(record).send(operator, condition)
|
10
|
+
# and provides a fallback behavior for invalid comparisons.
|
11
|
+
#
|
12
|
+
# Subclasses must implement `#operator` and may override `#preprocess`
|
13
|
+
# to normalize or cast the record before comparison.
|
14
|
+
#
|
15
|
+
# @abstract
|
16
|
+
# @see AbstractMatcher
|
17
|
+
class AbstractOperatorMatcher < AbstractMatcher
|
18
|
+
# A list of Boolean values used for type guarding in some subclasses.
|
19
|
+
BOOLEAN_VALUES = [true, false].freeze
|
20
|
+
|
21
|
+
# Applies the binary operator to the preprocessed record and condition.
|
22
|
+
# If an error is raised (e.g., undefined comparison), the match fails.
|
23
|
+
#
|
24
|
+
# @param record [Object] the input record to test
|
25
|
+
# @return [Boolean] the result of record <operator> condition
|
26
|
+
def match(record)
|
27
|
+
preprocess(record).send(operator, @condition)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Hook for subclasses to transform the record before comparison.
|
31
|
+
# Default behavior normalizes KEY_NOT_FOUND to nil.
|
32
|
+
#
|
33
|
+
# @param record [Object] the raw record value
|
34
|
+
# @return [Object] the transformed value
|
35
|
+
def preprocess(record)
|
36
|
+
normalize(record)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the Ruby operator symbol to be used in comparison.
|
40
|
+
# Must be implemented by subclasses (e.g., :==, :<, :>=)
|
41
|
+
#
|
42
|
+
# @return [Symbol] the comparison operator
|
43
|
+
def operator; end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# AndMatcher implements the `$and` logical operator.
|
6
|
+
#
|
7
|
+
# It evaluates an array of subconditions and returns true only if *all* of them match.
|
8
|
+
#
|
9
|
+
# Unlike other matchers, AndMatcher flattens the underlying matcher tree by
|
10
|
+
# delegating each subcondition to a `HashConditionMatcher`, and further extracting
|
11
|
+
# all inner matchers. Duplicate matchers are deduplicated by `uniq_key`.
|
12
|
+
#
|
13
|
+
# This allows the matcher trace (`.explain`) to render as a flat list of independent conditions.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# matcher = AndMatcher.build([
|
17
|
+
# { age: { :$gte => 18 } },
|
18
|
+
# { name: /foo/ }
|
19
|
+
# ])
|
20
|
+
# matcher.match?(record) #=> true if both match
|
21
|
+
#
|
22
|
+
# @see AbstractMultiMatcher
|
23
|
+
class AndMatcher < AbstractMultiMatcher
|
24
|
+
# Constructs a HashConditionMatcher for each subcondition.
|
25
|
+
# Conversion is disabled to avoid double-processing.
|
26
|
+
enable_unwrap!
|
27
|
+
|
28
|
+
# Returns the flattened list of all matchers from each subcondition.
|
29
|
+
#
|
30
|
+
# Each condition is passed to a HashConditionMatcher, then recursively flattened.
|
31
|
+
# All matchers are then deduplicated using `uniq_key`.
|
32
|
+
#
|
33
|
+
# @return [Array<AbstractMatcher>]
|
34
|
+
# @see AbstractMatcher#uniq_key
|
35
|
+
define_instance_cache_method(:matchers) do
|
36
|
+
@condition.flat_map do |condition|
|
37
|
+
HashConditionMatcher.new(condition).matchers
|
38
|
+
end.uniq(&:uniq_key)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Combines submatcher results using `:all?`.
|
42
|
+
#
|
43
|
+
# @return [Symbol]
|
44
|
+
def operator
|
45
|
+
:all?
|
46
|
+
end
|
47
|
+
|
48
|
+
# Ensures the condition is an array of hashes.
|
49
|
+
#
|
50
|
+
# @raise [Mongory::TypeError] if not valid
|
51
|
+
# @return [void]
|
52
|
+
def check_validity!
|
53
|
+
raise TypeError, '$and needs an array' unless @condition.is_a?(Array)
|
54
|
+
|
55
|
+
@condition.each do |sub_condition|
|
56
|
+
raise TypeError, '$and needs an array of hash' unless sub_condition.is_a?(Hash)
|
57
|
+
end
|
58
|
+
|
59
|
+
super
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
register(:and, '$and', AndMatcher)
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# ArrayRecordMatcher matches records where the record itself is an Array.
|
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.
|
10
|
+
#
|
11
|
+
# @example Match when any element equals the expected value
|
12
|
+
# matcher = ArrayRecordMatcher.build(42)
|
13
|
+
# matcher.match?([10, 42, 99]) #=> true
|
14
|
+
#
|
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
|
18
|
+
#
|
19
|
+
# This matcher is automatically invoked by LiteralMatcher when the record value is an array.
|
20
|
+
#
|
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
|
+
#
|
24
|
+
# @see Mongory::Matchers::InMatcher
|
25
|
+
# @see Mongory::Matchers::LiteralMatcher
|
26
|
+
class ArrayRecordMatcher < AbstractMultiMatcher
|
27
|
+
enable_unwrap!
|
28
|
+
# Builds an array of matchers to evaluate the given condition against an array record.
|
29
|
+
#
|
30
|
+
# This method returns multiple matchers that will be evaluated using `:any?` logic:
|
31
|
+
# - An equality matcher for exact array match
|
32
|
+
# - A hash condition matcher if the condition is a hash
|
33
|
+
# - An `$elemMatch` matcher for element-wise comparison
|
34
|
+
#
|
35
|
+
# @return [Array<Mongory::Matchers::AbstractMatcher>] an array of matcher instances
|
36
|
+
define_instance_cache_method(:matchers) do
|
37
|
+
result = []
|
38
|
+
result << EqMatcher.build(@condition) if @condition.is_a?(Array)
|
39
|
+
result << case @condition
|
40
|
+
when Hash
|
41
|
+
HashConditionMatcher.build(parsed_condition)
|
42
|
+
when Regexp
|
43
|
+
ElemMatchMatcher.build('$regex' => @condition)
|
44
|
+
else
|
45
|
+
ElemMatchMatcher.build('$eq' => @condition)
|
46
|
+
end
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
# Combines results using `:any?` for multi-match logic.
|
51
|
+
#
|
52
|
+
# @return [Symbol]
|
53
|
+
def operator
|
54
|
+
:any?
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Parses the original condition hash into a normalized structure suitable for HashConditionMatcher.
|
60
|
+
#
|
61
|
+
# This method classifies keys in the condition hash as:
|
62
|
+
# - Numeric (integers or numeric strings): treated as index-based field matchers
|
63
|
+
# - Operator keys (e.g., `$size`, `$type`): retained at the top level
|
64
|
+
# - All other keys: grouped under a `$elemMatch` clause for element-wise comparison
|
65
|
+
#
|
66
|
+
# @return [Hash] a normalized condition hash, potentially containing `$elemMatch`
|
67
|
+
def parsed_condition
|
68
|
+
h_parsed = {}
|
69
|
+
h_elem_match = {}
|
70
|
+
@condition.each_pair do |key, value|
|
71
|
+
case key
|
72
|
+
when Integer, /^-?\d+$/
|
73
|
+
h_parsed[key.to_i] = value
|
74
|
+
when '$elemMatch'
|
75
|
+
h_elem_match.merge!(value)
|
76
|
+
when *Matchers.operators
|
77
|
+
h_parsed[key] = value
|
78
|
+
else
|
79
|
+
h_elem_match[key] = value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
h_parsed['$elemMatch'] = h_elem_match if is_present?(h_elem_match)
|
84
|
+
h_parsed
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# ElemMatchMatcher implements the logic for Mongo-style `$elemMatch`.
|
6
|
+
#
|
7
|
+
# It is used to determine if *any* element in an array matches the given condition.
|
8
|
+
#
|
9
|
+
# This matcher delegates element-wise comparison to HashConditionMatcher,
|
10
|
+
# allowing nested conditions to be applied recursively.
|
11
|
+
#
|
12
|
+
# Typically used internally by ArrayRecordMatcher when dealing with
|
13
|
+
# non-indexed hash-style subconditions.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# matcher = ElemMatchMatcher.build({ status: 'active' })
|
17
|
+
# matcher.match?([{ status: 'inactive' }, { status: 'active' }]) #=> true
|
18
|
+
#
|
19
|
+
# @see HashConditionMatcher
|
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)
|
28
|
+
|
29
|
+
collection.any? do |record|
|
30
|
+
super(Mongory.data_converter.convert(record))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Ensures the condition is a Hash.
|
35
|
+
#
|
36
|
+
# @raise [Mongory::TypeError] if the condition is not a Hash
|
37
|
+
# @return [void]
|
38
|
+
def check_validity!
|
39
|
+
raise TypeError, '$elemMatch needs a Hash.' unless @condition.is_a?(Hash)
|
40
|
+
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
register(:elem_match, '$elemMatch', ElemMatchMatcher)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# EqMatcher matches values using the equality operator `==`.
|
6
|
+
#
|
7
|
+
# It inherits from AbstractOperatorMatcher and defines its operator as `:==`.
|
8
|
+
#
|
9
|
+
# Used for conditions like:
|
10
|
+
# - { age: { '$eq' => 30 } }
|
11
|
+
# - { name: "Alice" } (implicit fallback)
|
12
|
+
#
|
13
|
+
# This matcher supports any Ruby object that implements `#==`.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# matcher = EqMatcher.build(42)
|
17
|
+
# matcher.match?(42) #=> true
|
18
|
+
# matcher.match?("42") #=> false
|
19
|
+
#
|
20
|
+
# @note This matcher is also used as the fallback for non-operator literal values,
|
21
|
+
# such as `{ name: "Alice" }`, when no other specialized matcher is applicable.
|
22
|
+
#
|
23
|
+
# @note Equality behavior depends on how `==` is implemented for the given objects.
|
24
|
+
#
|
25
|
+
# @see AbstractOperatorMatcher
|
26
|
+
class EqMatcher < AbstractOperatorMatcher
|
27
|
+
# Returns the Ruby equality operator to be used in matching.
|
28
|
+
#
|
29
|
+
# @return [Symbol] the equality operator symbol
|
30
|
+
def operator
|
31
|
+
:==
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
register(:eq, '$eq', EqMatcher)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# EveryMatcher implements the logic for Mongo-style `$every` which is not really support in MongoDB.
|
6
|
+
#
|
7
|
+
# It is used to determine if *all* element in an array matches the given condition.
|
8
|
+
#
|
9
|
+
# This matcher delegates element-wise comparison to HashConditionMatcher,
|
10
|
+
# allowing nested conditions to be applied recursively.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# matcher = EveryMatcher.build({ status: 'active' })
|
14
|
+
# matcher.match?([{ status: 'inactive' }, { status: 'active' }]) #=> false
|
15
|
+
#
|
16
|
+
# @see HashConditionMatcher
|
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.
|
20
|
+
|
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?
|
26
|
+
|
27
|
+
collection.all? do |record|
|
28
|
+
super(Mongory.data_converter.convert(record))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def check_validity!
|
33
|
+
raise TypeError, '$every needs a Hash.' unless @condition.is_a?(Hash)
|
34
|
+
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
register(:every, '$every', EveryMatcher)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# ExistsMatcher implements the `$exists` operator, which checks whether a key exists.
|
6
|
+
#
|
7
|
+
# It transforms the presence (or absence) of a field into a boolean value,
|
8
|
+
# then compares it to the condition using the `==` operator.
|
9
|
+
#
|
10
|
+
# This matcher ensures the condition is strictly a boolean (`true` or `false`).
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# matcher = ExistsMatcher.build(true)
|
14
|
+
# matcher.match?(42) #=> true
|
15
|
+
# matcher.match?(KEY_NOT_FOUND) #=> false
|
16
|
+
#
|
17
|
+
# matcher = ExistsMatcher.build(false)
|
18
|
+
# matcher.match?(KEY_NOT_FOUND) #=> true
|
19
|
+
#
|
20
|
+
# @see AbstractOperatorMatcher
|
21
|
+
class ExistsMatcher < AbstractOperatorMatcher
|
22
|
+
# Converts the raw record value into a boolean indicating presence.
|
23
|
+
#
|
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
|
29
|
+
|
30
|
+
# Uses Ruby's equality operator to compare presence against expected boolean.
|
31
|
+
#
|
32
|
+
# @return [Symbol] the comparison operator
|
33
|
+
def operator
|
34
|
+
:==
|
35
|
+
end
|
36
|
+
|
37
|
+
# Ensures that the condition value is a valid boolean.
|
38
|
+
#
|
39
|
+
# @raise [TypeError] if condition is not true or false
|
40
|
+
# @return [void]
|
41
|
+
def check_validity!
|
42
|
+
raise TypeError, '$exists needs a boolean' unless BOOLEAN_VALUES.include?(@condition)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
register(:exists, '$exists', ExistsMatcher)
|
47
|
+
end
|
48
|
+
end
|