mongory 0.4.0 → 0.6.1

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/README.md +37 -7
  4. data/examples/benchmark-rails.rb +52 -0
  5. data/examples/benchmark.rb +79 -18
  6. data/lib/generators/mongory/matcher/matcher_generator.rb +1 -1
  7. data/lib/mongory/converters/abstract_converter.rb +22 -32
  8. data/lib/mongory/converters/condition_converter.rb +9 -19
  9. data/lib/mongory/converters/converted.rb +81 -0
  10. data/lib/mongory/converters/data_converter.rb +18 -7
  11. data/lib/mongory/converters/key_converter.rb +43 -19
  12. data/lib/mongory/converters/value_converter.rb +24 -19
  13. data/lib/mongory/converters.rb +1 -0
  14. data/lib/mongory/matchers/abstract_matcher.rb +94 -32
  15. data/lib/mongory/matchers/abstract_multi_matcher.rb +16 -45
  16. data/lib/mongory/matchers/and_matcher.rb +38 -10
  17. data/lib/mongory/matchers/array_record_matcher.rb +54 -28
  18. data/lib/mongory/matchers/elem_match_matcher.rb +13 -9
  19. data/lib/mongory/matchers/eq_matcher.rb +12 -7
  20. data/lib/mongory/matchers/every_matcher.rb +20 -9
  21. data/lib/mongory/matchers/exists_matcher.rb +15 -14
  22. data/lib/mongory/matchers/field_matcher.rb +58 -38
  23. data/lib/mongory/matchers/gt_matcher.rb +15 -7
  24. data/lib/mongory/matchers/gte_matcher.rb +15 -7
  25. data/lib/mongory/matchers/hash_condition_matcher.rb +54 -26
  26. data/lib/mongory/matchers/in_matcher.rb +20 -13
  27. data/lib/mongory/matchers/literal_matcher.rb +42 -48
  28. data/lib/mongory/matchers/lt_matcher.rb +15 -7
  29. data/lib/mongory/matchers/lte_matcher.rb +15 -7
  30. data/lib/mongory/matchers/ne_matcher.rb +12 -7
  31. data/lib/mongory/matchers/nin_matcher.rb +20 -12
  32. data/lib/mongory/matchers/not_matcher.rb +9 -5
  33. data/lib/mongory/matchers/or_matcher.rb +42 -13
  34. data/lib/mongory/matchers/present_matcher.rb +14 -15
  35. data/lib/mongory/matchers/regex_matcher.rb +37 -22
  36. data/lib/mongory/matchers/size_matcher.rb +50 -0
  37. data/lib/mongory/matchers.rb +1 -1
  38. data/lib/mongory/query_builder.rb +89 -27
  39. data/lib/mongory/query_matcher.rb +40 -13
  40. data/lib/mongory/query_operator.rb +1 -1
  41. data/lib/mongory/utils/context.rb +41 -0
  42. data/lib/mongory/utils/debugger.rb +6 -4
  43. data/lib/mongory/utils.rb +1 -0
  44. data/lib/mongory/version.rb +1 -1
  45. data/lib/mongory.rb +3 -3
  46. data/mongory.gemspec +3 -3
  47. metadata +11 -9
  48. data/lib/mongory/matchers/README.md +0 -57
  49. data/lib/mongory/matchers/abstract_operator_matcher.rb +0 -46
@@ -2,55 +2,49 @@
2
2
 
3
3
  module Mongory
4
4
  module Matchers
5
- # LiteralMatcher is responsible for handling raw literal values in query conditions.
5
+ # LiteralMatcher handles direct value comparison with special array handling.
6
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.
7
+ # This matcher is used when a condition is a literal value (not an operator).
8
+ # It handles both direct equality comparison and array-record scenarios.
9
9
  #
10
- # It is used when the query condition is a direct literal and not an operator or nested query.
10
+ # For array records:
11
+ # - Uses ArrayRecordMatcher to check if any element matches
12
+ # For non-array records:
13
+ # - Uses appropriate matcher based on condition type (Hash, Regexp, nil, etc.)
11
14
  #
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)
15
+ # @example Basic equality matching
16
+ # matcher = LiteralMatcher.build(42)
17
+ # matcher.match?(42) #=> true
18
+ # matcher.match?([42, 43]) #=> true (array contains 42)
19
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.
20
+ # @example Regexp matching
21
+ # matcher = LiteralMatcher.build(/foo/)
22
+ # matcher.match?("foo") #=> true
23
+ # matcher.match?(["foobar"]) #=> true
22
24
  #
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 (==)
25
+ # @example Hash condition matching
26
+ # matcher = LiteralMatcher.build({ '$gt' => 10 })
27
+ # matcher.match?(15) #=> true
28
+ # matcher.match?([5, 15]) #=> true
33
29
  #
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
30
+ # @see AbstractMatcher
31
+ # @see ArrayRecordMatcher
43
32
  class LiteralMatcher < AbstractMatcher
44
- # Matches the given record against the condition.
33
+ # Creates a raw Proc that performs the literal matching operation.
34
+ # The Proc handles both array and non-array records appropriately.
45
35
  #
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)
36
+ # @return [Proc] a Proc that performs the literal matching operation
37
+ def raw_proc
38
+ array_record_proc = nil
39
+ dispatched_proc = dispatched_matcher.to_proc
40
+
41
+ Proc.new do |record|
42
+ if record.is_a?(Array)
43
+ array_record_proc ||= array_record_matcher.to_proc
44
+ array_record_proc.call(record)
45
+ else
46
+ dispatched_proc.call(record)
47
+ end
54
48
  end
55
49
  end
56
50
 
@@ -77,16 +71,16 @@ module Mongory
77
71
  define_matcher(:dispatched) do
78
72
  case @condition
79
73
  when Hash
80
- HashConditionMatcher.build(@condition)
74
+ HashConditionMatcher.build(@condition, context: @context)
81
75
  when Regexp
82
- RegexMatcher.build(@condition)
76
+ RegexMatcher.build(@condition, context: @context)
83
77
  when nil
84
78
  OrMatcher.build([
85
79
  { '$exists' => false },
86
80
  { '$eq' => nil }
87
- ])
81
+ ], context: @context)
88
82
  else
89
- EqMatcher.build(@condition)
83
+ EqMatcher.build(@condition, context: @context)
90
84
  end
91
85
  end
92
86
 
@@ -96,7 +90,7 @@ module Mongory
96
90
  # @return [ArrayRecordMatcher] the matcher used to match array-type records
97
91
  # @!method array_record_matcher
98
92
  define_matcher(:array_record) do
99
- ArrayRecordMatcher.build(@condition)
93
+ ArrayRecordMatcher.build(@condition, context: @context)
100
94
  end
101
95
 
102
96
  # Validates the nested condition matcher, if applicable.
@@ -109,8 +103,8 @@ module Mongory
109
103
  # Outputs the matcher tree by selecting either collection or condition matcher.
110
104
  # Delegates `render_tree` to whichever submatcher was active.
111
105
  #
112
- # @param prefix [String]
113
- # @param is_last [Boolean]
106
+ # @param prefix [String] the prefix string for tree rendering
107
+ # @param is_last [Boolean] whether this is the last node in the tree
114
108
  # @return [void]
115
109
  def render_tree(prefix = '', is_last: true)
116
110
  super
@@ -6,7 +6,7 @@ module Mongory
6
6
  #
7
7
  # It returns true if the record is strictly less than the condition value.
8
8
  #
9
- # This matcher inherits from AbstractOperatorMatcher and uses the `<` operator.
9
+ # This matcher inherits from AbstractMatcher and uses the `<` operator.
10
10
  #
11
11
  # @example
12
12
  # matcher = LtMatcher.build(10)
@@ -14,13 +14,21 @@ module Mongory
14
14
  # matcher.match?(10) #=> false
15
15
  # matcher.match?(11) #=> false
16
16
  #
17
- # @see AbstractOperatorMatcher
18
- class LtMatcher < AbstractOperatorMatcher
19
- # Returns the Ruby `<` operator symbol for comparison.
17
+ # @see AbstractMatcher
18
+ class LtMatcher < AbstractMatcher
19
+ # Creates a raw Proc that performs the less-than comparison.
20
+ # The Proc uses the `<` operator to compare values.
20
21
  #
21
- # @return [Symbol] the less-than operator
22
- def operator
23
- :<
22
+ # @return [Proc] A proc that performs less-than comparison with error handling
23
+ # @note The proc includes error handling for invalid comparisons
24
+ def raw_proc
25
+ condition = @condition
26
+
27
+ Proc.new do |record|
28
+ record < condition
29
+ rescue StandardError
30
+ false
31
+ end
24
32
  end
25
33
  end
26
34
 
@@ -6,7 +6,7 @@ module Mongory
6
6
  #
7
7
  # It returns true if the record is less than or equal to the condition value.
8
8
  #
9
- # This matcher inherits from AbstractOperatorMatcher and uses the `<=` operator.
9
+ # This matcher inherits from AbstractMatcher and uses the `<=` operator.
10
10
  #
11
11
  # @example
12
12
  # matcher = LteMatcher.build(10)
@@ -14,13 +14,21 @@ module Mongory
14
14
  # matcher.match?(10) #=> true
15
15
  # matcher.match?(11) #=> false
16
16
  #
17
- # @see AbstractOperatorMatcher
18
- class LteMatcher < AbstractOperatorMatcher
19
- # Returns the Ruby `<=` operator symbol for comparison.
17
+ # @see AbstractMatcher
18
+ class LteMatcher < AbstractMatcher
19
+ # Creates a raw Proc that performs the less-than-or-equal comparison.
20
+ # The Proc uses the `<=` operator to compare values.
20
21
  #
21
- # @return [Symbol] the less-than-or-equal operator
22
- def operator
23
- :<=
22
+ # @return [Proc] A proc that performs less-than-or-equal comparison with error handling
23
+ # @note The proc includes error handling for invalid comparisons
24
+ def raw_proc
25
+ condition = @condition
26
+
27
+ Proc.new do |record|
28
+ record <= condition
29
+ rescue StandardError
30
+ false
31
+ end
24
32
  end
25
33
  end
26
34
 
@@ -6,7 +6,7 @@ module Mongory
6
6
  #
7
7
  # It returns true if the record is *not equal* to the condition.
8
8
  #
9
- # This matcher inherits its logic from AbstractOperatorMatcher
9
+ # This matcher inherits its logic from AbstractMatcher
10
10
  # and uses Ruby's `!=` operator for comparison.
11
11
  #
12
12
  # @example
@@ -14,13 +14,18 @@ module Mongory
14
14
  # matcher.match?(41) #=> true
15
15
  # matcher.match?(42) #=> false
16
16
  #
17
- # @see AbstractOperatorMatcher
18
- class NeMatcher < AbstractOperatorMatcher
19
- # Returns the Ruby `!=` operator symbol for comparison.
17
+ # @see AbstractMatcher
18
+ class NeMatcher < AbstractMatcher
19
+ # Creates a raw Proc that performs the not-equal comparison.
20
+ # The Proc uses the `!=` operator to compare values.
20
21
  #
21
- # @return [Symbol] the not-equal operator
22
- def operator
23
- :!=
22
+ # @return [Proc] A proc that performs not-equal comparison
23
+ def raw_proc
24
+ condition = @condition
25
+
26
+ Proc.new do |record|
27
+ record != condition
28
+ end
24
29
  end
25
30
  end
26
31
 
@@ -24,25 +24,33 @@ module Mongory
24
24
  #
25
25
  # @see AbstractMatcher
26
26
  class NinMatcher < AbstractMatcher
27
- # Matches true if the record has no elements in common with the condition array.
27
+ # Creates a raw Proc that performs the not-in matching operation.
28
+ # The Proc checks if the record has no elements in common with the condition array.
28
29
  #
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)
30
+ # @return [Proc] A proc that performs not-in matching
31
+ def raw_proc
32
+ condition = @condition
33
+
34
+ Proc.new do |record|
35
+ if record.is_a?(Array)
36
+ return true if condition.is_a?(Range)
37
+
38
+ is_blank?(condition & record)
39
+ else
40
+ !condition.include?(record)
41
+ end
37
42
  end
38
43
  end
39
44
 
40
- # Ensures the condition is a valid array.
45
+ # Ensures the condition is a valid array or range.
41
46
  #
42
- # @raise [TypeError] if the condition is not an array
47
+ # @raise [TypeError] if the condition is not an array nor a range
43
48
  # @return [void]
44
49
  def check_validity!
45
- raise TypeError, '$nin needs an array' unless @condition.is_a?(Array)
50
+ return if @condition.is_a?(Array)
51
+ return if @condition.is_a?(Range)
52
+
53
+ raise TypeError, '$nin needs an array'
46
54
  end
47
55
  end
48
56
 
@@ -18,12 +18,16 @@ module Mongory
18
18
  #
19
19
  # @see LiteralMatcher
20
20
  class NotMatcher < LiteralMatcher
21
- # Inverts the result of LiteralMatcher#match.
21
+ # Creates a raw Proc that performs the not-matching operation.
22
+ # The Proc inverts the result of the wrapped matcher.
22
23
  #
23
- # @param record [Object] the value to test
24
- # @return [Boolean] whether the negated condition is satisfied
25
- def match(record)
26
- !super(record)
24
+ # @return [Proc] A proc that performs not-matching
25
+ def raw_proc
26
+ super_proc = super
27
+
28
+ Proc.new do |record|
29
+ !super_proc.call(record)
30
+ end
27
31
  end
28
32
  end
29
33
 
@@ -5,7 +5,8 @@ module Mongory
5
5
  # OrMatcher implements the `$or` logical operator.
6
6
  #
7
7
  # It evaluates an array of subconditions and returns true
8
- # if *any one* of them matches.
8
+ # if *any one* of them matches. For empty conditions, it returns false
9
+ # (using FALSE_PROC).
9
10
  #
10
11
  # Each subcondition is handled by a HashConditionMatcher with conversion disabled,
11
12
  # since the parent matcher already manages data conversion.
@@ -20,24 +21,52 @@ module Mongory
20
21
  # ])
21
22
  # matcher.match?(record) #=> true if either condition matches
22
23
  #
24
+ # @example Empty conditions
25
+ # matcher = OrMatcher.build([])
26
+ # matcher.match?(record) #=> false (uses FALSE_PROC)
27
+ #
23
28
  # @see AbstractMultiMatcher
24
29
  class OrMatcher < AbstractMultiMatcher
25
30
  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)
31
+
32
+ # Creates a raw Proc that performs the or-matching operation.
33
+ # The Proc combines all submatcher Procs and returns true if any match.
34
+ # For empty conditions, returns FALSE_PROC.
35
+ #
36
+ # @return [Proc] a Proc that performs the or-matching operation
37
+ def raw_proc
38
+ return FALSE_PROC if matchers.empty?
39
+
40
+ combine_procs(*matchers.map(&:to_proc))
41
+ end
42
+
43
+ # Recursively combines multiple matcher procs with OR logic.
44
+ # This method optimizes the combination of multiple matchers by building
45
+ # a balanced tree of OR operations.
46
+ #
47
+ # @param left [Proc] The left matcher proc to combine
48
+ # @param rest [Array<Proc>] The remaining matcher procs to combine
49
+ # @return [Proc] A new proc that combines all matchers with OR logic
50
+ # @example
51
+ # combine_procs(proc1, proc2, proc3)
52
+ # #=> proc { |record| proc1.call(record) || proc2.call(record) || proc3.call(record) }
53
+ def combine_procs(left, *rest)
54
+ return left if rest.empty?
55
+
56
+ right = combine_procs(*rest)
57
+ Proc.new do |record|
58
+ left.call(record) || right.call(record)
59
+ end
34
60
  end
35
61
 
36
- # Uses `:any?` to return true if any submatcher passes.
62
+ # Builds an array of matchers from the subconditions.
63
+ # Each subcondition is wrapped in a HashConditionMatcher.
37
64
  #
38
- # @return [Symbol] the combining operator
39
- def operator
40
- :any?
65
+ # @return [Array<AbstractMatcher>] array of submatchers
66
+ define_instance_cache_method(:matchers) do
67
+ @condition.map do |condition|
68
+ HashConditionMatcher.build(condition, context: @context)
69
+ end
41
70
  end
42
71
 
43
72
  # Ensures the condition is an array of hashes.
@@ -20,22 +20,19 @@ module Mongory
20
20
  # matcher = PresentMatcher.build(false)
21
21
  # matcher.match?(nil) #=> true
22
22
  #
23
- # @see AbstractOperatorMatcher
24
- class PresentMatcher < AbstractOperatorMatcher
25
- # Transforms the record into a boolean presence flag
26
- # before applying comparison.
23
+ # @see AbstractMatcher
24
+ class PresentMatcher < AbstractMatcher
25
+ # Creates a raw Proc that performs the presence check.
26
+ # The Proc checks if the record's presence matches the condition.
27
27
  #
28
- # @param record [Object] the original value
29
- # @return [Boolean] whether the value is present
30
- def preprocess(record)
31
- is_present?(super)
32
- end
28
+ # @return [Proc] A proc that performs presence check
29
+ def raw_proc
30
+ condition = @condition
33
31
 
34
- # Uses Ruby `==` to compare the presence result to the expected boolean.
35
- #
36
- # @return [Symbol] the equality operator
37
- def operator
38
- :==
32
+ Proc.new do |record|
33
+ record = nil if record == KEY_NOT_FOUND
34
+ is_present?(record) == condition
35
+ end
39
36
  end
40
37
 
41
38
  # Ensures that the condition value is a boolean.
@@ -43,7 +40,9 @@ module Mongory
43
40
  # @raise [TypeError] if condition is not true or false
44
41
  # @return [void]
45
42
  def check_validity!
46
- raise TypeError, '$present needs a boolean' unless BOOLEAN_VALUES.include?(@condition)
43
+ return if [true, false].include?(@condition)
44
+
45
+ raise TypeError, '$present needs a boolean'
47
46
  end
48
47
  end
49
48
 
@@ -12,41 +12,56 @@ module Mongory
12
12
  # If a string is provided instead of a Regexp, it will be converted via `Regexp.new(...)`.
13
13
  # This ensures consistent behavior for queries like `:field.regex => "foo"` and `:field.regex => /foo/`.
14
14
  #
15
- # @example
16
- # matcher = RegexMatcher.build('^foo')
15
+ # @example Basic regex matching
16
+ # matcher = RegexMatcher.build(/^foo/)
17
17
  # matcher.match?('foobar') #=> true
18
18
  # matcher.match?('barfoo') #=> false
19
19
  #
20
- # @example Match with explicit regex
21
- # RegexMatcher.build(/admin/i).match?("ADMIN") # => true
20
+ # @example Case-insensitive matching
21
+ # matcher = RegexMatcher.build(/admin/i)
22
+ # matcher.match?("ADMIN") #=> true
22
23
  #
23
- # @example Match via LiteralMatcher fallback
24
- # LiteralMatcher.new(/admin/i).match("ADMIN") # => true
24
+ # @example String pattern
25
+ # matcher = RegexMatcher.build("^foo")
26
+ # matcher.match?("foobar") #=> true
25
27
  #
26
- # @see LiteralMatcher
27
- # @see Mongory::Matchers::AbstractOperatorMatcher
28
- class RegexMatcher < AbstractOperatorMatcher
29
- # Uses `:match?` as the operator to invoke on the record string.
28
+ # @example Non-string input
29
+ # matcher = RegexMatcher.build(/\d+/)
30
+ # matcher.match?(123) #=> false (not a string)
31
+ #
32
+ # @see AbstractMatcher
33
+ class RegexMatcher < AbstractMatcher
34
+ # Initializes the matcher with a regex pattern.
35
+ # Converts string patterns to Regexp objects.
30
36
  #
31
- # @return [Symbol] the match? method symbol
32
- def operator
33
- :match?
37
+ # @param condition [String, Regexp] the regex pattern to match against
38
+ # @param context [Context] the query context
39
+ # @raise [TypeError] if condition is not a string or Regexp
40
+ def initialize(condition, context: Context.new)
41
+ super
42
+ @condition = Regexp.new(condition) if condition.is_a?(String)
34
43
  end
35
44
 
36
- # Ensures the record is a string before applying regex.
37
- # If not, coerces to empty string to ensure match fails safely.
45
+ # Creates a raw Proc that performs the regex matching operation.
46
+ # The Proc checks if the record is a string that matches the pattern.
47
+ # Returns false for non-string inputs or if the match fails.
38
48
  #
39
- # @param record [Object] the raw input
40
- # @return [String] a safe string to match against
41
- def preprocess(record)
42
- return '' unless record.is_a?(String)
49
+ # @return [Proc] a Proc that performs regex matching
50
+ def raw_proc
51
+ condition = @condition
52
+
53
+ Proc.new do |record|
54
+ next false unless record.is_a?(String)
43
55
 
44
- record
56
+ record.match?(condition)
57
+ rescue StandardError
58
+ false
59
+ end
45
60
  end
46
61
 
47
- # Ensures the condition is a Regexp (strings are converted during initialization).
62
+ # Ensures the condition is a valid regex pattern (Regexp or String).
48
63
  #
49
- # @raise [TypeError] if condition is not a string
64
+ # @raise [TypeError] if condition is not a string or Regexp
50
65
  # @return [void]
51
66
  def check_validity!
52
67
  return if @condition.is_a?(Regexp)
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # Matcher for the `$size` operator.
6
+ #
7
+ # This matcher expects the input to be an array, and delegates the comparison
8
+ # to a literal matcher using the array's size as the value.
9
+ #
10
+ # For example, the condition `{ tags: { '$size' => 3 } }` will match any
11
+ # document where `tags` is an array of length 3.
12
+ #
13
+ # ### Supported compound usages:
14
+ #
15
+ # ```ruby
16
+ # Mongory.where(tags: { '$size' => 3 }) # exactly 3 elements
17
+ # Mongory.where(tags: { '$size' => { '$gt' => 1 } }) # more than 1
18
+ # Mongory.where(comments: { '$size' => { '$gt' => 1, '$lte' => 5 } }) # more than 1, up to 5 elements
19
+ # Mongory.where(tags: { '$size' => { '$in' => [1, 2, 3] } }) # 1, 2, or 3 elements
20
+ # ```
21
+ #
22
+ # @see LiteralMatcher
23
+ #
24
+ # @note Ruby's Symbol class already defines a `#size` method,
25
+ # that will return the size of the symbol object.
26
+ # So, this is the only operator that cannot be used with
27
+ # the symbol snippet syntax (e.g. `:tags.size`).
28
+ #
29
+ # Use string key syntax instead: `:"tags.$size" => ...`
30
+ class SizeMatcher < LiteralMatcher
31
+ # Creates a raw Proc that performs the size matching operation.
32
+ #
33
+ # The returned Proc checks if the input is an Array. If so, it calculates
34
+ # the array's size and passes it to the wrapped literal matcher Proc.
35
+ #
36
+ # @return [Proc] A proc that performs size-based matching
37
+ def raw_proc
38
+ super_proc = super
39
+
40
+ Proc.new do |record|
41
+ next false unless record.is_a?(Array)
42
+
43
+ super_proc.call(record.size)
44
+ end
45
+ end
46
+ end
47
+
48
+ register(:size, '$size', SizeMatcher)
49
+ end
50
+ end
@@ -153,7 +153,6 @@ end
153
153
 
154
154
  require_relative 'matchers/abstract_matcher'
155
155
  require_relative 'matchers/abstract_multi_matcher'
156
- require_relative 'matchers/abstract_operator_matcher'
157
156
  require_relative 'matchers/literal_matcher'
158
157
  require_relative 'matchers/hash_condition_matcher'
159
158
  require_relative 'matchers/and_matcher'
@@ -174,3 +173,4 @@ require_relative 'matchers/not_matcher'
174
173
  require_relative 'matchers/or_matcher'
175
174
  require_relative 'matchers/present_matcher'
176
175
  require_relative 'matchers/regex_matcher'
176
+ require_relative 'matchers/size_matcher'