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,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
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.
|
7
|
+
#
|
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).
|
10
|
+
#
|
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.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# matcher = FieldMatcher.build(:age, { :$gte => 18 })
|
16
|
+
# matcher.match?({ age: 20 }) #=> true
|
17
|
+
#
|
18
|
+
# @see LiteralMatcher
|
19
|
+
class FieldMatcher < LiteralMatcher
|
20
|
+
# A list of classes that should never be used for value digging.
|
21
|
+
# These typically respond to `#[]` but are semantically invalid for this context.
|
22
|
+
CLASSES_NOT_ALLOW_TO_DIG = [
|
23
|
+
::String,
|
24
|
+
::Integer,
|
25
|
+
::Proc,
|
26
|
+
::Method,
|
27
|
+
::MatchData,
|
28
|
+
::Thread,
|
29
|
+
::Symbol
|
30
|
+
].freeze
|
31
|
+
|
32
|
+
# Initializes the matcher with a target field and condition.
|
33
|
+
#
|
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)
|
37
|
+
@field = field
|
38
|
+
super(condition)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Performs field-based matching against the given record.
|
42
|
+
#
|
43
|
+
# This method first ensures the record is structurally eligible for field extraction—
|
44
|
+
# it must be a Hash, Array, or respond to `[]`. If the structure does not allow for
|
45
|
+
# field access (e.g., nil, primitive values, or unsupported types), the match returns false.
|
46
|
+
#
|
47
|
+
# The field value is then extracted using the following rules:
|
48
|
+
# - If the record is a Hash, it attempts to fetch using the field key,
|
49
|
+
# falling back to symbolized key if needed.
|
50
|
+
# - If the record is an Array, it fetches by index.
|
51
|
+
# - If the record does not support `[]` or is disallowed for dig operations,
|
52
|
+
# the match returns false immediately.
|
53
|
+
#
|
54
|
+
# Once the value is extracted, it is passed through the data converter
|
55
|
+
# and matched against the condition via the superclass.
|
56
|
+
#
|
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
|
+
# @example Matching a Hash with a nil field value
|
61
|
+
# matcher = Mongory::QueryMatcher.new(a: nil)
|
62
|
+
# matcher.match?({ a: nil }) # => true
|
63
|
+
#
|
64
|
+
# @example Record is nil (structure not diggable)
|
65
|
+
# matcher = Mongory::QueryMatcher.new(a: nil)
|
66
|
+
# matcher.match?(nil) # => false
|
67
|
+
#
|
68
|
+
# @example Matching against an Array by index
|
69
|
+
# matcher = Mongory::QueryMatcher.new(0 => /abc/)
|
70
|
+
# matcher.match?(['abcdef']) # => true
|
71
|
+
#
|
72
|
+
# @example Hash with symbol key, matcher uses string key
|
73
|
+
# matcher = Mongory::QueryMatcher.new('a' => 123)
|
74
|
+
# 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
|
+
|
89
|
+
record[@field]
|
90
|
+
end
|
91
|
+
|
92
|
+
super(Mongory.data_converter.convert(sub_record))
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [String] a deduplication field used for matchers inside multi-match constructs
|
96
|
+
# @see AbstractMultiMatcher#matchers
|
97
|
+
def uniq_key
|
98
|
+
super + "field:#{@field}"
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# Returns a single-line summary of the dig matcher including the field and condition.
|
104
|
+
#
|
105
|
+
# @return [String]
|
106
|
+
def tree_title
|
107
|
+
"Field: #{@field.inspect} to match: #{@condition.inspect}"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Custom display logic for debugging, including colored field highlighting.
|
111
|
+
#
|
112
|
+
# @param record [Object] the input record
|
113
|
+
# @param result [Boolean] match result
|
114
|
+
# @return [String] formatted debug string
|
115
|
+
def debug_display(record, result)
|
116
|
+
"#{self.class.name.split('::').last} #{colored_result(result)}, " \
|
117
|
+
"condition: #{@condition.inspect}, " \
|
118
|
+
"\e[30;47mfield: #{@field.inspect}\e[0m, " \
|
119
|
+
"record: #{record.inspect.gsub(@field.inspect, "\e[30;47m#{@field.inspect}\e[0m")}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# GtMatcher implements the `$gt` (greater than) operator.
|
6
|
+
#
|
7
|
+
# It returns true if the record is strictly greater than the condition.
|
8
|
+
#
|
9
|
+
# Inherits core logic from AbstractOperatorMatcher, including
|
10
|
+
# error handling and optional preprocessing.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# matcher = GtMatcher.build(10)
|
14
|
+
# matcher.match?(15) #=> true
|
15
|
+
# matcher.match?(10) #=> false
|
16
|
+
#
|
17
|
+
# @see AbstractOperatorMatcher
|
18
|
+
class GtMatcher < AbstractOperatorMatcher
|
19
|
+
# Returns the Ruby `>` operator symbol for comparison.
|
20
|
+
#
|
21
|
+
# @return [Symbol] the greater-than operator
|
22
|
+
def operator
|
23
|
+
:>
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
register(:gt, '$gt', GtMatcher)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# GteMatcher implements the `$gte` (greater than or equal) operator.
|
6
|
+
#
|
7
|
+
# It returns true if the record is greater than or equal to the condition value.
|
8
|
+
#
|
9
|
+
# Inherits comparison logic and error safety from AbstractOperatorMatcher.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# matcher = GteMatcher.build(10)
|
13
|
+
# matcher.match?(10) #=> true
|
14
|
+
# matcher.match?(11) #=> true
|
15
|
+
# matcher.match?(9) #=> false
|
16
|
+
#
|
17
|
+
# @see AbstractOperatorMatcher
|
18
|
+
class GteMatcher < AbstractOperatorMatcher
|
19
|
+
# Returns the Ruby `>=` operator symbol for comparison.
|
20
|
+
#
|
21
|
+
# @return [Symbol] the greater-than-or-equal operator
|
22
|
+
def operator
|
23
|
+
:>=
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
register(:gte, '$gte', GteMatcher)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# HashConditionMatcher is responsible for handling field-level query conditions.
|
6
|
+
#
|
7
|
+
# It receives a Hash of key-value pairs and delegates each one to an appropriate matcher
|
8
|
+
# based on whether the key is a recognized operator or a data field path.
|
9
|
+
#
|
10
|
+
# Each subcondition is matched independently using the `:all?` strategy, meaning
|
11
|
+
# all subconditions must match for the entire HashConditionMatcher to succeed.
|
12
|
+
#
|
13
|
+
# This matcher plays a central role in dispatching symbolic query conditions
|
14
|
+
# to the appropriate field or operator matcher.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# matcher = HashConditionMatcher.build({ age: { :$gt => 30 }, active: true })
|
18
|
+
# matcher.match?(record) #=> true only if all subconditions match
|
19
|
+
#
|
20
|
+
# @see AbstractMultiMatcher
|
21
|
+
class HashConditionMatcher < AbstractMultiMatcher
|
22
|
+
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)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Specifies the matching strategy for all subconditions.
|
47
|
+
# Uses `:all?`, meaning all conditions must be satisfied.
|
48
|
+
#
|
49
|
+
# @return [Symbol] the combining operator method
|
50
|
+
def operator
|
51
|
+
:all?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# InMatcher implements the `$in` operator.
|
6
|
+
#
|
7
|
+
# It checks whether the record matches any value in the condition array.
|
8
|
+
# If the record is an array, it succeeds if any item overlaps with the condition.
|
9
|
+
# If the record is a single value (including `nil`), it matches if it is included in the condition.
|
10
|
+
#
|
11
|
+
# @example Match single value
|
12
|
+
# matcher = InMatcher.build([1, 2, 3])
|
13
|
+
# matcher.match?(2) #=> true
|
14
|
+
# matcher.match?(5) #=> false
|
15
|
+
#
|
16
|
+
# @example Match nil
|
17
|
+
# matcher = InMatcher.build([nil])
|
18
|
+
# matcher.match?(nil) #=> true
|
19
|
+
#
|
20
|
+
# @example Match with array
|
21
|
+
# matcher = InMatcher.build([2, 4])
|
22
|
+
# matcher.match?([1, 2, 3]) #=> true
|
23
|
+
# matcher.match?([5, 6]) #=> false
|
24
|
+
#
|
25
|
+
# @see AbstractMatcher
|
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.
|
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)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Ensures the condition is an array.
|
42
|
+
#
|
43
|
+
# @raise [TypeError] if condition is not an array
|
44
|
+
# @return [void]
|
45
|
+
def check_validity!
|
46
|
+
raise TypeError, '$in needs an array' unless @condition.is_a?(Array)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
register(:in, '$in', InMatcher)
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# LiteralMatcher is responsible for handling raw literal values in query conditions.
|
6
|
+
#
|
7
|
+
# This matcher dispatches logic based on the type of the literal value,
|
8
|
+
# such as nil, Array, Regexp, Hash, etc., and delegates to the appropriate specialized matcher.
|
9
|
+
#
|
10
|
+
# It is used when the query condition is a direct literal and not an operator or nested query.
|
11
|
+
#
|
12
|
+
# @example Supported usages
|
13
|
+
# { name: "Alice" } # String literal
|
14
|
+
# { age: 18 } # Numeric literal
|
15
|
+
# { active: true } # Boolean literal
|
16
|
+
# { tags: [1, 2, 3] } # Array literal → ArrayRecordMatcher
|
17
|
+
# { email: /@gmail\\.com/i } # Regexp literal → RegexMatcher
|
18
|
+
# { info: nil } # nil literal → nil_matcher (matches null or missing)
|
19
|
+
#
|
20
|
+
# @note This matcher is commonly dispatched from HashConditionMatcher or FieldMatcher
|
21
|
+
# when the condition is a simple literal value, not an operator hash.
|
22
|
+
#
|
23
|
+
# === Supported literal types:
|
24
|
+
# - String
|
25
|
+
# - Integer / Float
|
26
|
+
# - Symbol
|
27
|
+
# - TrueClass / FalseClass
|
28
|
+
# - NilClass → delegates to nil_matcher
|
29
|
+
# - Regexp → delegates to RegexMatcher
|
30
|
+
# - Array → delegates to ArrayRecordMatcher
|
31
|
+
# - Hash → delegates to HashConditionMatcher (if treated as sub-query)
|
32
|
+
# - Other unrecognized values → fallback to equality match (==)
|
33
|
+
#
|
34
|
+
# === Excluded types (handled by other matchers):
|
35
|
+
# - Operator hashes like `{ "$gt" => 5 }` → handled by OperatorMatcher
|
36
|
+
# - Nested paths like `"a.b.c"` → handled by FieldMatcher
|
37
|
+
# - Query combinators like `$or`, `$and`, `$not` → handled by corresponding matchers
|
38
|
+
#
|
39
|
+
# @see Mongory::Matchers::RegexMatcher
|
40
|
+
# @see Mongory::Matchers::OrMatcher
|
41
|
+
# @see Mongory::Matchers::ArrayRecordMatcher
|
42
|
+
# @see Mongory::Matchers::HashConditionMatcher
|
43
|
+
class LiteralMatcher < AbstractMatcher
|
44
|
+
# Matches the given record against the condition.
|
45
|
+
#
|
46
|
+
# @param record [Object] the record to be matched
|
47
|
+
# @return [Boolean] whether the record satisfies the condition
|
48
|
+
def match(record)
|
49
|
+
case record
|
50
|
+
when Array
|
51
|
+
array_record_matcher.match?(record)
|
52
|
+
else
|
53
|
+
dispatched_matcher.match?(record)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Selects and returns the appropriate matcher instance for a given literal condition.
|
58
|
+
#
|
59
|
+
# This method analyzes the type of the raw condition (e.g., Hash, Regexp, nil)
|
60
|
+
# and returns a dedicated matcher instance accordingly:
|
61
|
+
#
|
62
|
+
# - Hash → dispatches to `HashConditionMatcher`
|
63
|
+
# - Regexp → dispatches to `RegexMatcher`
|
64
|
+
# - nil → dispatches to an `OrMatcher` that emulates MongoDB's `{ field: nil }` behavior
|
65
|
+
#
|
66
|
+
# For all other literal types, this method returns `EqMatcher`, and fallback equality matching will be used.
|
67
|
+
#
|
68
|
+
# This matcher is cached after the first invocation using `define_instance_cache_method`
|
69
|
+
# to avoid unnecessary re-instantiation.
|
70
|
+
#
|
71
|
+
# @see Mongory::Matchers::HashConditionMatcher
|
72
|
+
# @see Mongory::Matchers::RegexMatcher
|
73
|
+
# @see Mongory::Matchers::OrMatcher
|
74
|
+
# @see Mongory::Matchers::EqMatcher
|
75
|
+
# @return [AbstractMatcher] the matcher used for non-array literal values
|
76
|
+
# @!method dispatched_matcher
|
77
|
+
define_matcher(:dispatched) do
|
78
|
+
case @condition
|
79
|
+
when Hash
|
80
|
+
HashConditionMatcher.build(@condition)
|
81
|
+
when Regexp
|
82
|
+
RegexMatcher.build(@condition)
|
83
|
+
when nil
|
84
|
+
OrMatcher.build([
|
85
|
+
{ '$exists' => false },
|
86
|
+
{ '$eq' => nil }
|
87
|
+
])
|
88
|
+
else
|
89
|
+
EqMatcher.build(@condition)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Lazily defines the collection matcher for array records.
|
94
|
+
#
|
95
|
+
# @see ArrayRecordMatcher
|
96
|
+
# @return [ArrayRecordMatcher] the matcher used to match array-type records
|
97
|
+
# @!method array_record_matcher
|
98
|
+
define_matcher(:array_record) do
|
99
|
+
ArrayRecordMatcher.build(@condition)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Validates the nested condition matcher, if applicable.
|
103
|
+
#
|
104
|
+
# @return [void]
|
105
|
+
def check_validity!
|
106
|
+
dispatched_matcher.check_validity!
|
107
|
+
end
|
108
|
+
|
109
|
+
# Outputs the matcher tree by selecting either collection or condition matcher.
|
110
|
+
# Delegates `render_tree` to whichever submatcher was active.
|
111
|
+
#
|
112
|
+
# @param prefix [String]
|
113
|
+
# @param is_last [Boolean]
|
114
|
+
# @return [void]
|
115
|
+
def render_tree(prefix = '', is_last: true)
|
116
|
+
super
|
117
|
+
|
118
|
+
target_matcher = @array_record_matcher || dispatched_matcher
|
119
|
+
target_matcher.render_tree("#{prefix}#{is_last ? ' ' : '│ '}", is_last: true)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# LtMatcher implements the `$lt` (less than) operator.
|
6
|
+
#
|
7
|
+
# It returns true if the record is strictly less than the condition value.
|
8
|
+
#
|
9
|
+
# This matcher inherits from AbstractOperatorMatcher and uses the `<` operator.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# matcher = LtMatcher.build(10)
|
13
|
+
# matcher.match?(9) #=> true
|
14
|
+
# matcher.match?(10) #=> false
|
15
|
+
# matcher.match?(11) #=> false
|
16
|
+
#
|
17
|
+
# @see AbstractOperatorMatcher
|
18
|
+
class LtMatcher < AbstractOperatorMatcher
|
19
|
+
# Returns the Ruby `<` operator symbol for comparison.
|
20
|
+
#
|
21
|
+
# @return [Symbol] the less-than operator
|
22
|
+
def operator
|
23
|
+
:<
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
register(:lt, '$lt', LtMatcher)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# LteMatcher implements the `$lte` (less than or equal to) operator.
|
6
|
+
#
|
7
|
+
# It returns true if the record is less than or equal to the condition value.
|
8
|
+
#
|
9
|
+
# This matcher inherits from AbstractOperatorMatcher and uses the `<=` operator.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# matcher = LteMatcher.build(10)
|
13
|
+
# matcher.match?(9) #=> true
|
14
|
+
# matcher.match?(10) #=> true
|
15
|
+
# matcher.match?(11) #=> false
|
16
|
+
#
|
17
|
+
# @see AbstractOperatorMatcher
|
18
|
+
class LteMatcher < AbstractOperatorMatcher
|
19
|
+
# Returns the Ruby `<=` operator symbol for comparison.
|
20
|
+
#
|
21
|
+
# @return [Symbol] the less-than-or-equal operator
|
22
|
+
def operator
|
23
|
+
:<=
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
register(:lte, '$lte', LteMatcher)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# NeMatcher implements the `$ne` (not equal) operator.
|
6
|
+
#
|
7
|
+
# It returns true if the record is *not equal* to the condition.
|
8
|
+
#
|
9
|
+
# This matcher inherits its logic from AbstractOperatorMatcher
|
10
|
+
# and uses Ruby's `!=` operator for comparison.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# matcher = NeMatcher.build(42)
|
14
|
+
# matcher.match?(41) #=> true
|
15
|
+
# matcher.match?(42) #=> false
|
16
|
+
#
|
17
|
+
# @see AbstractOperatorMatcher
|
18
|
+
class NeMatcher < AbstractOperatorMatcher
|
19
|
+
# Returns the Ruby `!=` operator symbol for comparison.
|
20
|
+
#
|
21
|
+
# @return [Symbol] the not-equal operator
|
22
|
+
def operator
|
23
|
+
:!=
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
register(:ne, '$ne', NeMatcher)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# NinMatcher implements the `$nin` (not in) operator.
|
6
|
+
#
|
7
|
+
# It succeeds only if the record does not match any value in the condition array.
|
8
|
+
# If the record is an array, it fails if any element overlaps with the condition.
|
9
|
+
# If the record is a single value (including `nil`), it fails if it is included in the condition.
|
10
|
+
#
|
11
|
+
# @example Match single value
|
12
|
+
# matcher = NinMatcher.build([1, 2, 3])
|
13
|
+
# matcher.match?(4) #=> true
|
14
|
+
# matcher.match?(2) #=> false
|
15
|
+
#
|
16
|
+
# @example Match nil
|
17
|
+
# matcher = NinMatcher.build([nil])
|
18
|
+
# matcher.match?(nil) #=> false
|
19
|
+
#
|
20
|
+
# @example Match with array
|
21
|
+
# matcher = NinMatcher.build([2, 4])
|
22
|
+
# matcher.match?([1, 3, 5]) #=> true
|
23
|
+
# matcher.match?([4, 5]) #=> false
|
24
|
+
#
|
25
|
+
# @see AbstractMatcher
|
26
|
+
class NinMatcher < AbstractMatcher
|
27
|
+
# Matches true if the record has no elements in common with the condition array.
|
28
|
+
#
|
29
|
+
# @param record [Object] the value to be tested
|
30
|
+
# @return [Boolean] whether the record is disjoint from the condition array
|
31
|
+
def match(record)
|
32
|
+
record = normalize(record)
|
33
|
+
if record.is_a?(Array)
|
34
|
+
is_blank?(@condition & record)
|
35
|
+
else
|
36
|
+
!@condition.include?(record)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Ensures the condition is a valid array.
|
41
|
+
#
|
42
|
+
# @raise [TypeError] if the condition is not an array
|
43
|
+
# @return [void]
|
44
|
+
def check_validity!
|
45
|
+
raise TypeError, '$nin needs an array' unless @condition.is_a?(Array)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
register(:nin, '$nin', NinMatcher)
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# NotMatcher implements the `$not` logical operator.
|
6
|
+
#
|
7
|
+
# It returns true if the wrapped matcher fails, effectively inverting the result.
|
8
|
+
#
|
9
|
+
# It delegates to LiteralMatcher and simply negates the outcome.
|
10
|
+
#
|
11
|
+
# This allows constructs like:
|
12
|
+
# { age: { :$not => { :$gte => 30 } } }
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# matcher = NotMatcher.build({ :$gte => 10 })
|
16
|
+
# matcher.match?(5) #=> true
|
17
|
+
# matcher.match?(15) #=> false
|
18
|
+
#
|
19
|
+
# @see LiteralMatcher
|
20
|
+
class NotMatcher < LiteralMatcher
|
21
|
+
# Inverts the result of LiteralMatcher#match.
|
22
|
+
#
|
23
|
+
# @param record [Object] the value to test
|
24
|
+
# @return [Boolean] whether the negated condition is satisfied
|
25
|
+
def match(record)
|
26
|
+
!super(record)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
register(:not, '$not', NotMatcher)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# OrMatcher implements the `$or` logical operator.
|
6
|
+
#
|
7
|
+
# It evaluates an array of subconditions and returns true
|
8
|
+
# if *any one* of them matches.
|
9
|
+
#
|
10
|
+
# Each subcondition is handled by a HashConditionMatcher with conversion disabled,
|
11
|
+
# since the parent matcher already manages data conversion.
|
12
|
+
#
|
13
|
+
# This matcher inherits submatcher dispatch and evaluation logic
|
14
|
+
# from AbstractMultiMatcher.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# matcher = OrMatcher.build([
|
18
|
+
# { age: { :$lt => 18 } },
|
19
|
+
# { admin: true }
|
20
|
+
# ])
|
21
|
+
# matcher.match?(record) #=> true if either condition matches
|
22
|
+
#
|
23
|
+
# @see AbstractMultiMatcher
|
24
|
+
class OrMatcher < AbstractMultiMatcher
|
25
|
+
enable_unwrap!
|
26
|
+
# Constructs a HashConditionMatcher for each subcondition.
|
27
|
+
# Conversion is disabled to avoid double-processing.
|
28
|
+
|
29
|
+
# @see HashConditionMatcher
|
30
|
+
# @param condition [Object] a subcondition to be wrapped
|
31
|
+
# @return [HashConditionMatcher] a matcher for this condition
|
32
|
+
def build_sub_matcher(condition)
|
33
|
+
HashConditionMatcher.build(condition)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Uses `:any?` to return true if any submatcher passes.
|
37
|
+
#
|
38
|
+
# @return [Symbol] the combining operator
|
39
|
+
def operator
|
40
|
+
:any?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Ensures the condition is an array of hashes.
|
44
|
+
#
|
45
|
+
# @raise [Mongory::TypeError] if not valid
|
46
|
+
# @return [void]
|
47
|
+
def check_validity!
|
48
|
+
raise TypeError, '$or needs an array' unless @condition.is_a?(Array)
|
49
|
+
|
50
|
+
@condition.each do |sub_condition|
|
51
|
+
raise TypeError, '$or needs an array of hash' unless sub_condition.is_a?(Hash)
|
52
|
+
end
|
53
|
+
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
register(:or, '$or', OrMatcher)
|
59
|
+
end
|
60
|
+
end
|