mongory 0.7.3-x64-mingw-ucrt

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 (115) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +88 -0
  4. data/.yardopts +7 -0
  5. data/CHANGELOG.md +364 -0
  6. data/CODE_OF_CONDUCT.md +84 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +488 -0
  9. data/Rakefile +107 -0
  10. data/SUBMODULE_INTEGRATION.md +325 -0
  11. data/docs/advanced_usage.md +40 -0
  12. data/docs/clang_bridge.md +69 -0
  13. data/docs/field_names.md +30 -0
  14. data/docs/migration.md +30 -0
  15. data/docs/performance.md +61 -0
  16. data/examples/README.md +41 -0
  17. data/examples/benchmark-rails.rb +52 -0
  18. data/examples/benchmark.rb +184 -0
  19. data/ext/mongory_ext/extconf.rb +91 -0
  20. data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/array.h +122 -0
  21. data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/config.h +161 -0
  22. data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/error.h +79 -0
  23. data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/memory_pool.h +95 -0
  24. data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/table.h +127 -0
  25. data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/value.h +175 -0
  26. data/ext/mongory_ext/mongory-core/include/mongory-core/matchers/matcher.h +76 -0
  27. data/ext/mongory_ext/mongory-core/include/mongory-core.h +12 -0
  28. data/ext/mongory_ext/mongory-core/src/foundations/array.c +287 -0
  29. data/ext/mongory_ext/mongory-core/src/foundations/array_private.h +19 -0
  30. data/ext/mongory_ext/mongory-core/src/foundations/config.c +270 -0
  31. data/ext/mongory_ext/mongory-core/src/foundations/config_private.h +48 -0
  32. data/ext/mongory_ext/mongory-core/src/foundations/error.c +38 -0
  33. data/ext/mongory_ext/mongory-core/src/foundations/memory_pool.c +298 -0
  34. data/ext/mongory_ext/mongory-core/src/foundations/string_buffer.c +65 -0
  35. data/ext/mongory_ext/mongory-core/src/foundations/string_buffer.h +49 -0
  36. data/ext/mongory_ext/mongory-core/src/foundations/table.c +498 -0
  37. data/ext/mongory_ext/mongory-core/src/foundations/utils.c +210 -0
  38. data/ext/mongory_ext/mongory-core/src/foundations/utils.h +70 -0
  39. data/ext/mongory_ext/mongory-core/src/foundations/value.c +500 -0
  40. data/ext/mongory_ext/mongory-core/src/matchers/array_record_matcher.c +164 -0
  41. data/ext/mongory_ext/mongory-core/src/matchers/array_record_matcher.h +47 -0
  42. data/ext/mongory_ext/mongory-core/src/matchers/base_matcher.c +122 -0
  43. data/ext/mongory_ext/mongory-core/src/matchers/base_matcher.h +100 -0
  44. data/ext/mongory_ext/mongory-core/src/matchers/compare_matcher.c +217 -0
  45. data/ext/mongory_ext/mongory-core/src/matchers/compare_matcher.h +83 -0
  46. data/ext/mongory_ext/mongory-core/src/matchers/composite_matcher.c +573 -0
  47. data/ext/mongory_ext/mongory-core/src/matchers/composite_matcher.h +125 -0
  48. data/ext/mongory_ext/mongory-core/src/matchers/existance_matcher.c +147 -0
  49. data/ext/mongory_ext/mongory-core/src/matchers/existance_matcher.h +48 -0
  50. data/ext/mongory_ext/mongory-core/src/matchers/external_matcher.c +124 -0
  51. data/ext/mongory_ext/mongory-core/src/matchers/external_matcher.h +46 -0
  52. data/ext/mongory_ext/mongory-core/src/matchers/inclusion_matcher.c +126 -0
  53. data/ext/mongory_ext/mongory-core/src/matchers/inclusion_matcher.h +46 -0
  54. data/ext/mongory_ext/mongory-core/src/matchers/literal_matcher.c +314 -0
  55. data/ext/mongory_ext/mongory-core/src/matchers/literal_matcher.h +97 -0
  56. data/ext/mongory_ext/mongory-core/src/matchers/matcher.c +252 -0
  57. data/ext/mongory_ext/mongory-core/src/matchers/matcher_explainable.c +79 -0
  58. data/ext/mongory_ext/mongory-core/src/matchers/matcher_explainable.h +23 -0
  59. data/ext/mongory_ext/mongory-core/src/matchers/matcher_traversable.c +60 -0
  60. data/ext/mongory_ext/mongory-core/src/matchers/matcher_traversable.h +23 -0
  61. data/ext/mongory_ext/mongory_ext.c +683 -0
  62. data/lib/generators/mongory/install/install_generator.rb +42 -0
  63. data/lib/generators/mongory/install/templates/initializer.rb.erb +83 -0
  64. data/lib/generators/mongory/matcher/matcher_generator.rb +56 -0
  65. data/lib/generators/mongory/matcher/templates/matcher.rb.erb +92 -0
  66. data/lib/generators/mongory/matcher/templates/matcher_spec.rb.erb +17 -0
  67. data/lib/mongory/c_query_builder.rb +44 -0
  68. data/lib/mongory/converters/abstract_converter.rb +111 -0
  69. data/lib/mongory/converters/condition_converter.rb +64 -0
  70. data/lib/mongory/converters/converted.rb +81 -0
  71. data/lib/mongory/converters/data_converter.rb +37 -0
  72. data/lib/mongory/converters/key_converter.rb +87 -0
  73. data/lib/mongory/converters/value_converter.rb +52 -0
  74. data/lib/mongory/converters.rb +8 -0
  75. data/lib/mongory/matchers/abstract_matcher.rb +219 -0
  76. data/lib/mongory/matchers/abstract_multi_matcher.rb +124 -0
  77. data/lib/mongory/matchers/and_matcher.rb +72 -0
  78. data/lib/mongory/matchers/array_record_matcher.rb +93 -0
  79. data/lib/mongory/matchers/elem_match_matcher.rb +55 -0
  80. data/lib/mongory/matchers/eq_matcher.rb +46 -0
  81. data/lib/mongory/matchers/every_matcher.rb +56 -0
  82. data/lib/mongory/matchers/exists_matcher.rb +53 -0
  83. data/lib/mongory/matchers/field_matcher.rb +147 -0
  84. data/lib/mongory/matchers/gt_matcher.rb +41 -0
  85. data/lib/mongory/matchers/gte_matcher.rb +41 -0
  86. data/lib/mongory/matchers/hash_condition_matcher.rb +62 -0
  87. data/lib/mongory/matchers/in_matcher.rb +68 -0
  88. data/lib/mongory/matchers/literal_matcher.rb +121 -0
  89. data/lib/mongory/matchers/lt_matcher.rb +41 -0
  90. data/lib/mongory/matchers/lte_matcher.rb +41 -0
  91. data/lib/mongory/matchers/ne_matcher.rb +38 -0
  92. data/lib/mongory/matchers/nin_matcher.rb +68 -0
  93. data/lib/mongory/matchers/not_matcher.rb +40 -0
  94. data/lib/mongory/matchers/or_matcher.rb +68 -0
  95. data/lib/mongory/matchers/present_matcher.rb +55 -0
  96. data/lib/mongory/matchers/regex_matcher.rb +80 -0
  97. data/lib/mongory/matchers/size_matcher.rb +54 -0
  98. data/lib/mongory/matchers.rb +176 -0
  99. data/lib/mongory/mongoid.rb +19 -0
  100. data/lib/mongory/query_builder.rb +257 -0
  101. data/lib/mongory/query_matcher.rb +93 -0
  102. data/lib/mongory/query_operator.rb +28 -0
  103. data/lib/mongory/rails.rb +15 -0
  104. data/lib/mongory/utils/context.rb +48 -0
  105. data/lib/mongory/utils/debugger.rb +125 -0
  106. data/lib/mongory/utils/rails_patch.rb +22 -0
  107. data/lib/mongory/utils/singleton_builder.rb +31 -0
  108. data/lib/mongory/utils.rb +76 -0
  109. data/lib/mongory/version.rb +5 -0
  110. data/lib/mongory.rb +123 -0
  111. data/lib/mongory_ext.so +0 -0
  112. data/mongory.gemspec +62 -0
  113. data/scripts/build_with_core.sh +292 -0
  114. data/sig/mongory.rbs +4 -0
  115. metadata +159 -0
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Converters
5
+ # KeyConverter handles transformation of field keys in query conditions.
6
+ # It normalizes symbol keys into string paths, splits dotted keys,
7
+ # and delegates to the appropriate converter logic.
8
+ #
9
+ # This class inherits from AbstractConverter and provides specialized
10
+ # handling for different key types:
11
+ # - String keys with dots are split into nested paths
12
+ # - Symbol keys are converted to strings
13
+ # - QueryOperator instances are handled via their DSL hooks
14
+ # - Other types fall back to the parent converter
15
+ #
16
+ # Used by ConditionConverter to build query structures from flat input.
17
+ #
18
+ # - `"a.b.c" => v` becomes `{ "a" => { "b" => { "c" => v } } }`
19
+ # - Symbols are stringified and delegated to String logic
20
+ # - QueryOperator dispatches to internal DSL hook
21
+ #
22
+ # @example Convert a dotted string key
23
+ # KeyConverter.instance.convert("user.name", "John")
24
+ # # => { "user" => { "name" => "John" } }
25
+ #
26
+ # @example Convert a symbol key
27
+ # KeyConverter.instance.convert(:status, "active")
28
+ # # => { "status" => "active" }
29
+ #
30
+ # @see AbstractConverter
31
+ class KeyConverter < AbstractConverter
32
+ alias_method :super_convert, :convert
33
+
34
+ # Converts a key into its normalized form based on its type.
35
+ # Handles strings, symbols, and QueryOperator instances.
36
+ # Falls back to parent converter for other types.
37
+ #
38
+ # @param target [Object] the key to convert
39
+ # @param other [Object] the value associated with the key
40
+ # @return [Hash] the converted key-value pair
41
+ def convert(target, other)
42
+ case target
43
+ when String
44
+ convert_string_key(target, other)
45
+ when Symbol
46
+ convert_string_key(target.to_s, other)
47
+ when QueryOperator
48
+ # Handle special case for QueryOperator
49
+ convert_string_key(*target.__expr_part__(other).first)
50
+ else
51
+ super_convert(target, other)
52
+ end
53
+ end
54
+
55
+ def fallback(target, other)
56
+ { target => other }
57
+ end
58
+
59
+ # Converts a dotted string key into nested hash form.
60
+ # Splits the key on dots and builds a nested structure.
61
+ # Handles escaped dots in the key.
62
+ #
63
+ # @param key [String] the dotted key string, e.g. "a.b.c"
64
+ # @param value [Object] the value to assign at the deepest level
65
+ # @return [Hash] nested hash structure
66
+ def convert_string_key(key, value)
67
+ ret = {}
68
+ *sub_keys, last_key = key.split(/(?<!\\)\./)
69
+ last_hash = sub_keys.reduce(ret) do |res, sub_key|
70
+ next_res = res[normalize_key(sub_key)] = {}
71
+ next_res
72
+ end
73
+ last_hash[normalize_key(last_key)] = value
74
+ ret
75
+ end
76
+
77
+ # Normalizes a key by unescaping escaped dots.
78
+ # This allows for literal dots in field names.
79
+ #
80
+ # @param key [String] the key to normalize
81
+ # @return [String] the normalized key
82
+ def normalize_key(key)
83
+ key.gsub(/\\\./, '.')
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Converters
5
+ # ValueConverter transforms query values into a standardized form.
6
+ # It handles arrays, hashes, regex, and basic types, and delegates
7
+ # fallback logic to DataConverter.
8
+ # Used by ConditionConverter to prepare values in nested queries.
9
+ #
10
+ # - Arrays are recursively converted
11
+ # - Hashes are interpreted as nested conditions
12
+ # - Regex becomes a Mongo-style `$regex` hash
13
+ # - Strings and Integers are passed through
14
+ # - Everything else falls back to DataConverter
15
+ #
16
+ # @example Convert a regex
17
+ # ValueConverter.instance.convert(/foo/) #=> { "$regex" => "foo" }
18
+ #
19
+ class ValueConverter < AbstractConverter
20
+ alias_method :super_convert, :convert
21
+
22
+ # Converts a value into its standardized form based on its type.
23
+ # Handles arrays, hashes, regex, and basic types.
24
+ #
25
+ # @param target [Object] the value to convert
26
+ # @return [Object] the converted value
27
+ def convert(target)
28
+ case target
29
+ when String, Integer, Regexp, Converted
30
+ target
31
+ when Array
32
+ Converted::Array.new(target.map { |x| convert(x) })
33
+ when Hash
34
+ condition_converter.convert(target)
35
+ else
36
+ super_convert(target)
37
+ end
38
+ end
39
+
40
+ def fallback(target, *)
41
+ Mongory.data_converter.convert(target)
42
+ end
43
+
44
+ # Returns the condition converter instance.
45
+ #
46
+ # @return [ConditionConverter] the condition converter instance
47
+ def condition_converter
48
+ @condition_converter ||= Mongory.condition_converter
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'converters/abstract_converter'
4
+ require_relative 'converters/condition_converter'
5
+ require_relative 'converters/data_converter'
6
+ require_relative 'converters/key_converter'
7
+ require_relative 'converters/value_converter'
8
+ require_relative 'converters/converted'
@@ -0,0 +1,219 @@
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
+ # Each matcher is responsible for evaluating a specific type of condition against
11
+ # a record value. The base class provides infrastructure for:
12
+ # - Condition validation
13
+ # - Value conversion
14
+ # - Debug output
15
+ # - Proc caching
16
+ #
17
+ # @abstract Subclasses must implement {#match} to define their matching logic
18
+ #
19
+ # @example Basic matcher implementation
20
+ # class MyMatcher < AbstractMatcher
21
+ # def match(record)
22
+ # record == @condition
23
+ # end
24
+ # end
25
+ #
26
+ # @example Using a matcher
27
+ # matcher = MyMatcher.build(42)
28
+ # matcher.match?(42) #=> true
29
+ # matcher.match?(43) #=> false
30
+ #
31
+ # @see Mongory::Matchers for the full list of available matchers
32
+ class AbstractMatcher
33
+ include Utils
34
+
35
+ singleton_class.alias_method :build, :new
36
+
37
+ # Sentinel value used to represent missing keys when traversing nested hashes.
38
+ # This is used instead of nil to distinguish between missing keys and nil values.
39
+ #
40
+ # @api private
41
+ KEY_NOT_FOUND = SingletonBuilder.new('KEY_NOT_FOUND')
42
+
43
+ # Defines a lazily-evaluated matcher accessor with instance-level caching.
44
+ # This is used to create cached accessors for submatcher instances.
45
+ #
46
+ # @param name [Symbol] the name of the matcher (e.g., :collection)
47
+ # @yield the block that constructs the matcher instance
48
+ # @return [void]
49
+ # @example
50
+ # define_matcher(:array_matcher) do
51
+ # ArrayMatcher.build(@condition)
52
+ # end
53
+ def self.define_matcher(name, &block)
54
+ define_instance_cache_method(:"#{name}_matcher", &block)
55
+ end
56
+
57
+ # @return [Object] the raw condition this matcher was initialized with
58
+ attr_reader :condition
59
+
60
+ # @return [Context] the query context containing configuration and current record
61
+ attr_reader :context
62
+
63
+ # Returns a unique key representing this matcher instance.
64
+ # Used for deduplication in multi-matchers.
65
+ #
66
+ # @return [String] a unique key for this matcher instance
67
+ # @see AbstractMultiMatcher#matchers
68
+ def uniq_key
69
+ "#{self.class}:condition:#{@condition.class}:#{@condition}"
70
+ end
71
+
72
+ # Initializes the matcher with a raw condition.
73
+ #
74
+ # @param condition [Object] the condition to match against
75
+ # @param context [Context] the query context containing configuration
76
+ def initialize(condition, context: Context.new)
77
+ @condition = condition
78
+ @context = context
79
+
80
+ check_validity!
81
+ end
82
+
83
+ # Matches the given record against the condition.
84
+ # This method handles error cases and uses the cached proc for performance.
85
+ #
86
+ # @param record [Object] the input record
87
+ # @return [Boolean] whether the record matches the condition
88
+ def match?(record)
89
+ to_proc.call(record)
90
+ rescue StandardError
91
+ false
92
+ end
93
+
94
+ # Converts the matcher into a Proc that can be used for matching.
95
+ # The Proc is cached for better performance.
96
+ #
97
+ # @return [Proc] a Proc that can be used to match records
98
+ def cached_proc
99
+ @cached_proc ||= raw_proc
100
+ end
101
+
102
+ alias_method :to_proc, :cached_proc
103
+
104
+ # Creates a debug-enabled version of the matcher proc.
105
+ # This version includes tracing and error handling.
106
+ #
107
+ # @return [Proc] a debug-enabled version of the matcher proc
108
+ def debug_proc
109
+ return @debug_proc if defined?(@debug_proc)
110
+
111
+ raw_proc = raw_proc()
112
+ @debug_proc = Proc.new do |record|
113
+ result = nil
114
+
115
+ Debugger.instance.with_indent do
116
+ result = begin
117
+ raw_proc.call(record)
118
+ rescue StandardError => e
119
+ e
120
+ end
121
+
122
+ debug_display(record, result)
123
+ end
124
+
125
+ result.is_a?(Exception) ? false : result
126
+ end
127
+ end
128
+
129
+ # Creates a raw Proc from the match method.
130
+ # This is used internally by to_proc and can be overridden by subclasses
131
+ # to provide custom matching behavior.
132
+ #
133
+ # @return [Proc] the raw Proc implementation of the match method
134
+ def raw_proc
135
+ method(:match).to_proc
136
+ end
137
+
138
+ # Performs the actual match logic.
139
+ # Subclasses must override this method.
140
+ #
141
+ # @abstract
142
+ # @param record [Object] the input record to test
143
+ # @return [Boolean] whether the record matches the condition
144
+ def match(record); end
145
+
146
+ # Validates the condition (no-op by default).
147
+ # Override in subclasses to raise error if invalid.
148
+ #
149
+ # @abstract
150
+ # @raise [TypeError] if the condition is invalid
151
+ # @return [void]
152
+ def check_validity!; end
153
+
154
+ # Recursively prints the matcher structure into a formatted tree.
155
+ # Supports indentation and branching layout using prefix symbols.
156
+ #
157
+ # @param prefix [String] tree prefix (indentation + lines)
158
+ # @param is_last [Boolean] whether this node is the last among siblings
159
+ # @return [void]
160
+ def render_tree(prefix = '', is_last: true)
161
+ puts "#{prefix}#{is_last ? '└─ ' : '├─ '}#{tree_title}\n"
162
+ end
163
+
164
+ def priority
165
+ 20
166
+ end
167
+
168
+ private
169
+
170
+ # Returns a single-line string representing this matcher in the tree output.
171
+ # Format: `<MatcherType>: <condition.inspect>`
172
+ #
173
+ # @return [String] a formatted title for tree display
174
+ def tree_title
175
+ matcher_name = self.class.name.split('::').last.sub('Matcher', '')
176
+ "#{matcher_name}: #{@condition.inspect}"
177
+ end
178
+
179
+ # Normalizes the record before matching.
180
+ #
181
+ # If the record is the KEY_NOT_FOUND sentinel (representing a missing field),
182
+ # it is converted to `nil` so matchers can interpret it consistently.
183
+ # Other values are returned as-is.
184
+ #
185
+ # @param record [Object] the record value to normalize
186
+ # @return [Object, nil] the normalized record
187
+ # @see Mongory::KEY_NOT_FOUND
188
+ def normalize(record)
189
+ record == KEY_NOT_FOUND ? nil : record
190
+ end
191
+
192
+ # Formats a debug string for match output.
193
+ # Uses ANSI escape codes to highlight matched vs. mismatched records.
194
+ #
195
+ # @param record [Object] the record being tested
196
+ # @param result [Boolean, Exception] whether the match succeeded or an error occurred
197
+ # @return [String] the formatted output string
198
+ def debug_display(record, result)
199
+ "#{self.class.name.split('::').last} #{colored_result(result)}, " \
200
+ "condition: #{@condition.inspect}, " \
201
+ "record: #{record.inspect}"
202
+ end
203
+
204
+ # Formats the match result with ANSI color codes for terminal output.
205
+ #
206
+ # @param result [Boolean, Exception] the match result or error
207
+ # @return [String] the colored result string
208
+ def colored_result(result)
209
+ if result.is_a?(Exception)
210
+ "\e[45;97m#{result}\e[0m"
211
+ elsif result
212
+ "\e[30;42mMatched\e[0m"
213
+ else
214
+ "\e[30;41mDismatch\e[0m"
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,124 @@
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
+ # A Proc that always returns true, used as a default for empty AND conditions
20
+ # @return [Proc] A proc that always returns true
21
+ TRUE_PROC = Proc.new { |_| true }
22
+
23
+ # A Proc that always returns false, used as a default for empty OR conditions
24
+ # @return [Proc] A proc that always returns false
25
+ FALSE_PROC = Proc.new { |_| false }
26
+
27
+ # Enables auto-unwrap logic.
28
+ # When used, `.build` may unwrap to first matcher if only one is present.
29
+ #
30
+ # @private
31
+ # @return [void]
32
+ def self.enable_unwrap!
33
+ @enable_unwrap = true
34
+ singleton_class.alias_method :build, :build_or_unwrap
35
+ end
36
+
37
+ private_class_method :enable_unwrap!
38
+
39
+ # Builds a matcher and conditionally unwraps it.
40
+ # If unwrapping is enabled and there is only one submatcher,
41
+ # returns that submatcher instead of the multi-matcher wrapper.
42
+ #
43
+ # @param args [Array] arguments passed to the constructor
44
+ # @param context [Context] the query context
45
+ # @return [AbstractMatcher] the constructed matcher or its unwrapped submatcher
46
+ def self.build_or_unwrap(*args, context: Context.new)
47
+ matcher = new(*args, context: context)
48
+ return matcher unless @enable_unwrap
49
+
50
+ matcher = matcher.matchers.first if matcher.matchers.count == 1
51
+ matcher
52
+ end
53
+
54
+ # Recursively checks all submatchers for validity.
55
+ # Raises an error if any submatcher is invalid.
56
+ #
57
+ # @raise [Mongory::TypeError] if any submatcher is invalid
58
+ # @return [void]
59
+ def check_validity!
60
+ matchers.each(&:check_validity!)
61
+ end
62
+
63
+ # Overrides base render_tree to recursively print all submatchers.
64
+ # Each child matcher will be displayed under this multi-matcher node.
65
+ #
66
+ # @param prefix [String] current line prefix for tree alignment
67
+ # @param is_last [Boolean] whether this node is the last sibling
68
+ # @return [void]
69
+ def render_tree(prefix = '', is_last: true)
70
+ super
71
+
72
+ new_prefix = "#{prefix}#{is_last ? ' ' : '│ '}"
73
+ last_index = matchers.count - 1
74
+ matchers.each_with_index do |matcher, index|
75
+ matcher.render_tree(new_prefix, is_last: index == last_index)
76
+ end
77
+ end
78
+
79
+ def priority
80
+ 1 + matchers.sum(&:priority)
81
+ end
82
+
83
+ private
84
+
85
+ # Recursively combines multiple matcher procs with AND logic.
86
+ # This method optimizes the combination of multiple matchers by building
87
+ # a balanced tree of AND operations.
88
+ #
89
+ # @param left [Proc] The left matcher proc to combine
90
+ # @param rest [Array<Proc>] The remaining matcher procs to combine
91
+ # @return [Proc] A new proc that combines all matchers with AND logic
92
+ # @example
93
+ # combine_procs_with_and(proc1, proc2, proc3)
94
+ # #=> proc { |record| proc1.call(record) && proc2.call(record) && proc3.call(record) }
95
+ def combine_procs_with_and(left = TRUE_PROC, *rest)
96
+ return left if rest.empty?
97
+
98
+ right = combine_procs_with_and(*rest)
99
+ Proc.new do |record|
100
+ left.call(record) && right.call(record)
101
+ end
102
+ end
103
+
104
+ # Recursively combines multiple matcher procs with OR logic.
105
+ # This method optimizes the combination of multiple matchers by building
106
+ # a balanced tree of OR operations.
107
+ #
108
+ # @param left [Proc] The left matcher proc to combine
109
+ # @param rest [Array<Proc>] The remaining matcher procs to combine
110
+ # @return [Proc] A new proc that combines all matchers with OR logic
111
+ # @example
112
+ # combine_procs_with_or(proc1, proc2, proc3)
113
+ # #=> proc { |record| proc1.call(record) || proc2.call(record) || proc3.call(record) }
114
+ def combine_procs_with_or(left = FALSE_PROC, *rest)
115
+ return left if rest.empty?
116
+
117
+ right = combine_procs_with_or(*rest)
118
+ Proc.new do |record|
119
+ left.call(record) || right.call(record)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,72 @@
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
+ # For empty conditions, it returns true (using TRUE_PROC), following MongoDB's behavior.
9
+ #
10
+ # Unlike other matchers, AndMatcher flattens the underlying matcher tree by
11
+ # delegating each subcondition to a `HashConditionMatcher`, and further extracting
12
+ # all inner matchers. Duplicate matchers are deduplicated by `uniq_key`.
13
+ #
14
+ # This allows the matcher trace (`.explain`) to render as a flat list of independent conditions.
15
+ #
16
+ # @example Basic usage
17
+ # matcher = AndMatcher.build([
18
+ # { age: { :$gte => 18 } },
19
+ # { name: /foo/ }
20
+ # ])
21
+ # matcher.match?(record) #=> true if both match
22
+ #
23
+ # @example Empty conditions
24
+ # matcher = AndMatcher.build([])
25
+ # matcher.match?(record) #=> true (uses TRUE_PROC)
26
+ #
27
+ # @see AbstractMultiMatcher
28
+ class AndMatcher < AbstractMultiMatcher
29
+ # Constructs a HashConditionMatcher for each subcondition.
30
+ # Conversion is disabled to avoid double-processing.
31
+ enable_unwrap!
32
+
33
+ # Creates a raw Proc that performs the AND operation.
34
+ # The Proc combines all subcondition Procs and returns true only if all match.
35
+ # For empty conditions, returns TRUE_PROC.
36
+ #
37
+ # @return [Proc] a Proc that performs the AND operation
38
+ def raw_proc
39
+ combine_procs_with_and(*matchers.map(&:to_proc))
40
+ end
41
+
42
+ # Returns the flattened list of all matchers from each subcondition.
43
+ #
44
+ # Each condition is passed to a HashConditionMatcher, then recursively flattened.
45
+ # All matchers are then deduplicated using `uniq_key`.
46
+ #
47
+ # @return [Array<AbstractMatcher>] A flattened, deduplicated list of matchers
48
+ # @see AbstractMatcher#uniq_key
49
+ define_instance_cache_method(:matchers) do
50
+ @condition.flat_map do |condition|
51
+ HashConditionMatcher.new(condition, context: @context).matchers
52
+ end.uniq(&:uniq_key).sort_by(&:priority)
53
+ end
54
+
55
+ # Ensures the condition is an array of hashes.
56
+ #
57
+ # @raise [Mongory::TypeError] if not valid
58
+ # @return [void]
59
+ def check_validity!
60
+ raise TypeError, '$and needs an array' unless @condition.is_a?(Array)
61
+
62
+ @condition.each do |sub_condition|
63
+ raise TypeError, '$and needs an array of hash' unless sub_condition.is_a?(Hash)
64
+ end
65
+
66
+ super
67
+ end
68
+ end
69
+
70
+ register(:and, '$and', AndMatcher)
71
+ end
72
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # ArrayRecordMatcher handles matching against array-type records.
6
+ #
7
+ # This matcher is used when a field value is an array and needs to be matched
8
+ # against a condition. It supports both exact array matching and element-wise
9
+ # comparison through `$elemMatch`.
10
+ #
11
+ # For empty conditions, it returns false (using FALSE_PROC).
12
+ #
13
+ # @example Match exact array
14
+ # matcher = ArrayRecordMatcher.build([1, 2, 3])
15
+ # matcher.match?([1, 2, 3]) #=> true
16
+ # matcher.match?([1, 2]) #=> false
17
+ #
18
+ # @example Match with hash condition
19
+ # matcher = ArrayRecordMatcher.build({ '$gt' => 5 })
20
+ # matcher.match?([3, 6, 9]) #=> true (6 and 9 match)
21
+ # matcher.match?([1, 2, 3]) #=> false
22
+ #
23
+ # @example Empty conditions
24
+ # matcher = ArrayRecordMatcher.build([])
25
+ # matcher.match?(record) #=> false (uses FALSE_PROC)
26
+ #
27
+ # @see AbstractMultiMatcher
28
+ class ArrayRecordMatcher < AbstractMultiMatcher
29
+ enable_unwrap!
30
+
31
+ # Creates a raw Proc that performs the array matching operation.
32
+ # The Proc checks if any element in the array matches the condition.
33
+ # For empty conditions, returns FALSE_PROC.
34
+ #
35
+ # @return [Proc] a Proc that performs the array matching operation
36
+ def raw_proc
37
+ combine_procs_with_or(*matchers.map(&:to_proc))
38
+ end
39
+
40
+ # Builds an array of matchers to evaluate the given condition against an array record.
41
+ #
42
+ # This method returns multiple matchers that will be evaluated using `:any?` logic:
43
+ # - An equality matcher for exact array match
44
+ # - A hash condition matcher if the condition is a hash
45
+ # - An `$elemMatch` matcher for element-wise comparison
46
+ #
47
+ # @return [Array<AbstractMatcher>] An array of matcher instances
48
+ define_instance_cache_method(:matchers) do
49
+ result = []
50
+ result << EqMatcher.build(@condition, context: @context) if @condition.is_a?(Array)
51
+ result << case @condition
52
+ when Hash
53
+ HashConditionMatcher.build(parsed_condition, context: @context)
54
+ when Regexp
55
+ ElemMatchMatcher.build({ '$regex' => @condition }, context: @context)
56
+ else
57
+ ElemMatchMatcher.build({ '$eq' => @condition }, context: @context)
58
+ end
59
+ result.sort_by(&:priority)
60
+ end
61
+
62
+ private
63
+
64
+ # Parses the original condition hash into a normalized structure suitable for HashConditionMatcher.
65
+ #
66
+ # This method classifies keys in the condition hash as:
67
+ # - Numeric (integers or numeric strings): treated as index-based field matchers
68
+ # - Operator keys (e.g., `$size`, `$type`): retained at the top level
69
+ # - All other keys: grouped under a `$elemMatch` clause for element-wise comparison
70
+ #
71
+ # @return [Hash] A normalized condition hash, potentially containing `$elemMatch`
72
+ def parsed_condition
73
+ h_parsed = {}
74
+ h_elem_match = {}
75
+ @condition.each_pair do |key, value|
76
+ case key
77
+ when Integer, /^-?\d+$/
78
+ h_parsed[key.to_i] = value
79
+ when '$elemMatch'
80
+ h_elem_match.merge!(value)
81
+ when *Matchers.operators
82
+ h_parsed[key] = value
83
+ else
84
+ h_elem_match[key] = value
85
+ end
86
+ end
87
+
88
+ h_parsed['$elemMatch'] = h_elem_match if is_present?(h_elem_match)
89
+ h_parsed
90
+ end
91
+ end
92
+ end
93
+ end