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,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