mongory 0.7.3-aarch64-linux-musl
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.
Potentially problematic release.
This version of mongory might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +88 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +364 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +488 -0
- data/Rakefile +107 -0
- data/SUBMODULE_INTEGRATION.md +325 -0
- data/docs/advanced_usage.md +40 -0
- data/docs/clang_bridge.md +69 -0
- data/docs/field_names.md +30 -0
- data/docs/migration.md +30 -0
- data/docs/performance.md +61 -0
- data/examples/README.md +41 -0
- data/examples/benchmark-rails.rb +52 -0
- data/examples/benchmark.rb +184 -0
- data/ext/mongory_ext/extconf.rb +91 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/array.h +122 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/config.h +161 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/error.h +79 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/memory_pool.h +95 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/table.h +127 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/value.h +175 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/matchers/matcher.h +76 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core.h +12 -0
- data/ext/mongory_ext/mongory-core/src/foundations/array.c +287 -0
- data/ext/mongory_ext/mongory-core/src/foundations/array_private.h +19 -0
- data/ext/mongory_ext/mongory-core/src/foundations/config.c +270 -0
- data/ext/mongory_ext/mongory-core/src/foundations/config_private.h +48 -0
- data/ext/mongory_ext/mongory-core/src/foundations/error.c +38 -0
- data/ext/mongory_ext/mongory-core/src/foundations/memory_pool.c +298 -0
- data/ext/mongory_ext/mongory-core/src/foundations/string_buffer.c +65 -0
- data/ext/mongory_ext/mongory-core/src/foundations/string_buffer.h +49 -0
- data/ext/mongory_ext/mongory-core/src/foundations/table.c +498 -0
- data/ext/mongory_ext/mongory-core/src/foundations/utils.c +210 -0
- data/ext/mongory_ext/mongory-core/src/foundations/utils.h +70 -0
- data/ext/mongory_ext/mongory-core/src/foundations/value.c +500 -0
- data/ext/mongory_ext/mongory-core/src/matchers/array_record_matcher.c +164 -0
- data/ext/mongory_ext/mongory-core/src/matchers/array_record_matcher.h +47 -0
- data/ext/mongory_ext/mongory-core/src/matchers/base_matcher.c +122 -0
- data/ext/mongory_ext/mongory-core/src/matchers/base_matcher.h +100 -0
- data/ext/mongory_ext/mongory-core/src/matchers/compare_matcher.c +217 -0
- data/ext/mongory_ext/mongory-core/src/matchers/compare_matcher.h +83 -0
- data/ext/mongory_ext/mongory-core/src/matchers/composite_matcher.c +573 -0
- data/ext/mongory_ext/mongory-core/src/matchers/composite_matcher.h +125 -0
- data/ext/mongory_ext/mongory-core/src/matchers/existance_matcher.c +147 -0
- data/ext/mongory_ext/mongory-core/src/matchers/existance_matcher.h +48 -0
- data/ext/mongory_ext/mongory-core/src/matchers/external_matcher.c +124 -0
- data/ext/mongory_ext/mongory-core/src/matchers/external_matcher.h +46 -0
- data/ext/mongory_ext/mongory-core/src/matchers/inclusion_matcher.c +126 -0
- data/ext/mongory_ext/mongory-core/src/matchers/inclusion_matcher.h +46 -0
- data/ext/mongory_ext/mongory-core/src/matchers/literal_matcher.c +314 -0
- data/ext/mongory_ext/mongory-core/src/matchers/literal_matcher.h +97 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher.c +252 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher_explainable.c +79 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher_explainable.h +23 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher_traversable.c +60 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher_traversable.h +23 -0
- data/ext/mongory_ext/mongory_ext.c +683 -0
- data/lib/generators/mongory/install/install_generator.rb +42 -0
- data/lib/generators/mongory/install/templates/initializer.rb.erb +83 -0
- data/lib/generators/mongory/matcher/matcher_generator.rb +56 -0
- data/lib/generators/mongory/matcher/templates/matcher.rb.erb +92 -0
- data/lib/generators/mongory/matcher/templates/matcher_spec.rb.erb +17 -0
- data/lib/mongory/c_query_builder.rb +44 -0
- data/lib/mongory/converters/abstract_converter.rb +111 -0
- data/lib/mongory/converters/condition_converter.rb +64 -0
- data/lib/mongory/converters/converted.rb +81 -0
- data/lib/mongory/converters/data_converter.rb +37 -0
- data/lib/mongory/converters/key_converter.rb +87 -0
- data/lib/mongory/converters/value_converter.rb +52 -0
- data/lib/mongory/converters.rb +8 -0
- data/lib/mongory/matchers/abstract_matcher.rb +219 -0
- data/lib/mongory/matchers/abstract_multi_matcher.rb +124 -0
- data/lib/mongory/matchers/and_matcher.rb +72 -0
- data/lib/mongory/matchers/array_record_matcher.rb +93 -0
- data/lib/mongory/matchers/elem_match_matcher.rb +55 -0
- data/lib/mongory/matchers/eq_matcher.rb +46 -0
- data/lib/mongory/matchers/every_matcher.rb +56 -0
- data/lib/mongory/matchers/exists_matcher.rb +53 -0
- data/lib/mongory/matchers/field_matcher.rb +147 -0
- data/lib/mongory/matchers/gt_matcher.rb +41 -0
- data/lib/mongory/matchers/gte_matcher.rb +41 -0
- data/lib/mongory/matchers/hash_condition_matcher.rb +62 -0
- data/lib/mongory/matchers/in_matcher.rb +68 -0
- data/lib/mongory/matchers/literal_matcher.rb +121 -0
- data/lib/mongory/matchers/lt_matcher.rb +41 -0
- data/lib/mongory/matchers/lte_matcher.rb +41 -0
- data/lib/mongory/matchers/ne_matcher.rb +38 -0
- data/lib/mongory/matchers/nin_matcher.rb +68 -0
- data/lib/mongory/matchers/not_matcher.rb +40 -0
- data/lib/mongory/matchers/or_matcher.rb +68 -0
- data/lib/mongory/matchers/present_matcher.rb +55 -0
- data/lib/mongory/matchers/regex_matcher.rb +80 -0
- data/lib/mongory/matchers/size_matcher.rb +54 -0
- data/lib/mongory/matchers.rb +176 -0
- data/lib/mongory/mongoid.rb +19 -0
- data/lib/mongory/query_builder.rb +257 -0
- data/lib/mongory/query_matcher.rb +93 -0
- data/lib/mongory/query_operator.rb +28 -0
- data/lib/mongory/rails.rb +15 -0
- data/lib/mongory/utils/context.rb +48 -0
- data/lib/mongory/utils/debugger.rb +125 -0
- data/lib/mongory/utils/rails_patch.rb +22 -0
- data/lib/mongory/utils/singleton_builder.rb +31 -0
- data/lib/mongory/utils.rb +76 -0
- data/lib/mongory/version.rb +5 -0
- data/lib/mongory.rb +123 -0
- data/lib/mongory_ext.so +0 -0
- data/mongory.gemspec +62 -0
- data/scripts/build_with_core.sh +292 -0
- data/sig/mongory.rbs +4 -0
- 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
|