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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +84 -0
  4. data/.yardopts +7 -0
  5. data/CHANGELOG.md +246 -0
  6. data/CODE_OF_CONDUCT.md +84 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +517 -0
  9. data/Rakefile +12 -0
  10. data/examples/README.md +41 -0
  11. data/examples/benchmark.rb +44 -0
  12. data/lib/generators/mongory/install/install_generator.rb +42 -0
  13. data/lib/generators/mongory/install/templates/initializer.rb.erb +83 -0
  14. data/lib/generators/mongory/matcher/matcher_generator.rb +56 -0
  15. data/lib/generators/mongory/matcher/templates/matcher.rb.erb +92 -0
  16. data/lib/generators/mongory/matcher/templates/matcher_spec.rb.erb +17 -0
  17. data/lib/mongory/converters/abstract_converter.rb +122 -0
  18. data/lib/mongory/converters/condition_converter.rb +74 -0
  19. data/lib/mongory/converters/data_converter.rb +26 -0
  20. data/lib/mongory/converters/key_converter.rb +63 -0
  21. data/lib/mongory/converters/value_converter.rb +47 -0
  22. data/lib/mongory/converters.rb +7 -0
  23. data/lib/mongory/matchers/README.md +57 -0
  24. data/lib/mongory/matchers/abstract_matcher.rb +153 -0
  25. data/lib/mongory/matchers/abstract_multi_matcher.rb +109 -0
  26. data/lib/mongory/matchers/abstract_operator_matcher.rb +46 -0
  27. data/lib/mongory/matchers/and_matcher.rb +65 -0
  28. data/lib/mongory/matchers/array_record_matcher.rb +88 -0
  29. data/lib/mongory/matchers/elem_match_matcher.rb +47 -0
  30. data/lib/mongory/matchers/eq_matcher.rb +37 -0
  31. data/lib/mongory/matchers/every_matcher.rb +41 -0
  32. data/lib/mongory/matchers/exists_matcher.rb +48 -0
  33. data/lib/mongory/matchers/field_matcher.rb +123 -0
  34. data/lib/mongory/matchers/gt_matcher.rb +29 -0
  35. data/lib/mongory/matchers/gte_matcher.rb +29 -0
  36. data/lib/mongory/matchers/hash_condition_matcher.rb +55 -0
  37. data/lib/mongory/matchers/in_matcher.rb +52 -0
  38. data/lib/mongory/matchers/literal_matcher.rb +123 -0
  39. data/lib/mongory/matchers/lt_matcher.rb +29 -0
  40. data/lib/mongory/matchers/lte_matcher.rb +29 -0
  41. data/lib/mongory/matchers/ne_matcher.rb +29 -0
  42. data/lib/mongory/matchers/nin_matcher.rb +51 -0
  43. data/lib/mongory/matchers/not_matcher.rb +32 -0
  44. data/lib/mongory/matchers/or_matcher.rb +60 -0
  45. data/lib/mongory/matchers/present_matcher.rb +52 -0
  46. data/lib/mongory/matchers/regex_matcher.rb +61 -0
  47. data/lib/mongory/matchers.rb +176 -0
  48. data/lib/mongory/mongoid.rb +19 -0
  49. data/lib/mongory/query_builder.rb +187 -0
  50. data/lib/mongory/query_matcher.rb +66 -0
  51. data/lib/mongory/query_operator.rb +28 -0
  52. data/lib/mongory/rails.rb +15 -0
  53. data/lib/mongory/utils/debugger.rb +123 -0
  54. data/lib/mongory/utils/rails_patch.rb +22 -0
  55. data/lib/mongory/utils/singleton_builder.rb +31 -0
  56. data/lib/mongory/utils.rb +75 -0
  57. data/lib/mongory/version.rb +5 -0
  58. data/lib/mongory-rb.rb +3 -0
  59. data/lib/mongory.rb +116 -0
  60. data/mongory.gemspec +40 -0
  61. data/sig/mongory.rbs +4 -0
  62. 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