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,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # PresentMatcher implements the `$present` operator.
6
+ #
7
+ # It returns true if the record value is considered "present"
8
+ # (i.e., not nil, not empty, not KEY_NOT_FOUND), and matches
9
+ # the expected boolean condition.
10
+ #
11
+ # This is similar to `$exists`, but evaluates truthiness
12
+ # of the value instead of mere existence.
13
+ #
14
+ # @example
15
+ # matcher = PresentMatcher.build(true)
16
+ # matcher.match?('hello') #=> true
17
+ # matcher.match?(nil) #=> false
18
+ # matcher.match?([]) #=> false
19
+ #
20
+ # matcher = PresentMatcher.build(false)
21
+ # matcher.match?(nil) #=> true
22
+ #
23
+ # @see AbstractOperatorMatcher
24
+ class PresentMatcher < AbstractOperatorMatcher
25
+ # Transforms the record into a boolean presence flag
26
+ # before applying comparison.
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
33
+
34
+ # Uses Ruby `==` to compare the presence result to the expected boolean.
35
+ #
36
+ # @return [Symbol] the equality operator
37
+ def operator
38
+ :==
39
+ end
40
+
41
+ # Ensures that the condition value is a boolean.
42
+ #
43
+ # @raise [TypeError] if condition is not true or false
44
+ # @return [void]
45
+ def check_validity!
46
+ raise TypeError, '$present needs a boolean' unless BOOLEAN_VALUES.include?(@condition)
47
+ end
48
+ end
49
+
50
+ register(:present, '$present', PresentMatcher)
51
+ end
52
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # RegexMatcher implements the `$regex` operator and also handles raw Regexp values.
6
+ #
7
+ # This matcher checks whether a string record matches a regular expression.
8
+ # It supports both:
9
+ # - Explicit queries using `:field.regex => /pattern/i`
10
+ # - Implicit literal Regexp values like `{ field: /pattern/i }`
11
+ #
12
+ # If a string is provided instead of a Regexp, it will be converted via `Regexp.new(...)`.
13
+ # This ensures consistent behavior for queries like `:field.regex => "foo"` and `:field.regex => /foo/`.
14
+ #
15
+ # @example
16
+ # matcher = RegexMatcher.build('^foo')
17
+ # matcher.match?('foobar') #=> true
18
+ # matcher.match?('barfoo') #=> false
19
+ #
20
+ # @example Match with explicit regex
21
+ # RegexMatcher.build(/admin/i).match?("ADMIN") # => true
22
+ #
23
+ # @example Match via LiteralMatcher fallback
24
+ # LiteralMatcher.new(/admin/i).match("ADMIN") # => true
25
+ #
26
+ # @see LiteralMatcher
27
+ # @see Mongory::Matchers::AbstractOperatorMatcher
28
+ class RegexMatcher < AbstractOperatorMatcher
29
+ # Uses `:match?` as the operator to invoke on the record string.
30
+ #
31
+ # @return [Symbol] the match? method symbol
32
+ def operator
33
+ :match?
34
+ end
35
+
36
+ # Ensures the record is a string before applying regex.
37
+ # If not, coerces to empty string to ensure match fails safely.
38
+ #
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)
43
+
44
+ record
45
+ end
46
+
47
+ # Ensures the condition is a Regexp (strings are converted during initialization).
48
+ #
49
+ # @raise [TypeError] if condition is not a string
50
+ # @return [void]
51
+ def check_validity!
52
+ return if @condition.is_a?(Regexp)
53
+ return if @condition.is_a?(String)
54
+
55
+ raise TypeError, '$regex needs a Regexp or string'
56
+ end
57
+ end
58
+
59
+ register(:regex, '$regex', RegexMatcher)
60
+ end
61
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ # Provides matcher registration and operator-to-class lookup for query evaluation.
5
+ #
6
+ # This module is responsible for:
7
+ # - Mapping Mongo-style operators like "$gt" to matcher classes
8
+ # - Dynamically extending Symbol with query operator snippets (e.g., :age.gt)
9
+ # - Safely isolating symbol extension behind an explicit opt-in flag
10
+ #
11
+ # Matchers are registered using `Matchers.registry(method_sym, operator, klass)`
12
+ # and can be looked up via `Matchers.lookup(operator)`.
13
+ #
14
+ # Symbol snippets are only enabled if `Matchers.enable_symbol_snippets!` is called,
15
+ # preventing namespace pollution unless explicitly requested.
16
+ module Matchers
17
+ @operator_mapping = {}
18
+ @registries = []
19
+
20
+ # Registers a matcher class for a given operator and method symbol.
21
+ #
22
+ # @param method_sym [Symbol] the method name to be added to Symbol (e.g., :gt)
23
+ # @param operator [String] the Mongo-style operator (e.g., "$gt")
24
+ # @param klass [Class] the matcher class to associate with the operator
25
+ # @return [void]
26
+ # @raise [ArgumentError] if validations fail
27
+ def self.register(method_sym, operator, klass)
28
+ Validator.validate_method(method_sym)
29
+ Validator.validate_operator(operator)
30
+ Validator.validate_class(klass)
31
+
32
+ @operator_mapping[operator] = klass
33
+ registry = Registry.new(method_sym, operator)
34
+ @registries << registry
35
+ return unless @enable_symbol_snippets
36
+
37
+ registry.apply!
38
+ end
39
+
40
+ # Enables dynamic symbol snippet generation for registered operators.
41
+ # This defines methods like `:age.gt => QueryOperator.new(...)`.
42
+ #
43
+ # @return [void]
44
+ def self.enable_symbol_snippets!
45
+ @enable_symbol_snippets = true
46
+ @registries.each(&:apply!)
47
+ end
48
+
49
+ # Retrieves the matcher class associated with a Mongo-style operator.
50
+ #
51
+ # @param operator [String]
52
+ # @return [Class, nil] the registered matcher class or nil if not found
53
+ def self.lookup(operator)
54
+ @operator_mapping[operator]
55
+ end
56
+
57
+ # Returns all registered operator keys.
58
+ #
59
+ # @return [Array<String>]
60
+ def self.operators
61
+ @operator_mapping.keys
62
+ end
63
+
64
+ def self.freeze
65
+ super
66
+ @operator_mapping.freeze
67
+ @registries.freeze
68
+ end
69
+
70
+ # @private
71
+ #
72
+ # Internal helper module used by `Matchers.registry` to validate matcher registration parameters.
73
+ #
74
+ # This includes:
75
+ # - Ensuring operators are valid Mongo-style strings (e.g., "$gt")
76
+ # - Verifying matcher class inheritance
77
+ # - Enforcing naming rules for symbol snippets (e.g., :gt, :not_match)
78
+ #
79
+ # These validations protect against incorrect matcher setup and prevent unsafe symbol definitions.
80
+ #
81
+ # @see Matchers.registry
82
+ module Validator
83
+ # Validates the given operator string.
84
+ # Ensures it matches the Mongo-style format like "$gt".
85
+ # Warns on duplicate registration.
86
+ #
87
+ # @param operator [String]
88
+ # @return [void]
89
+ # @raise [Mongory::TypeError] if operator format is invalid
90
+ def self.validate_operator(operator)
91
+ if Matchers.lookup(operator)
92
+ warn "Duplicate operator registration: #{operator} (#{Matchers.lookup(operator)} vs #{klass})"
93
+ end
94
+
95
+ return if operator.is_a?(String) && operator.match?(/^\$[a-z]+([A-Z][a-z]+)*$/)
96
+
97
+ raise Mongory::TypeError, "Operator must match /^\$[a-z]+([A-Z][a-z]*)*$/, but got #{operator.inspect}"
98
+ end
99
+
100
+ # Validates the matcher class to ensure it is a subclass of AbstractMatcher.
101
+ #
102
+ # @param klass [Class]
103
+ # @return [void]
104
+ # @raise [Mongory::TypeError] if class is not valid
105
+ def self.validate_class(klass)
106
+ return if klass.is_a?(Class) && klass < AbstractMatcher
107
+
108
+ raise Mongory::TypeError, "Matcher class must be a subclass of AbstractMatcher, but got #{klass}"
109
+ end
110
+
111
+ # Validates the method symbol to ensure it is a valid lowercase underscore symbol (e.g., :gt, :not_match).
112
+ #
113
+ # @param method_sym [Symbol]
114
+ # @return [void]
115
+ # @raise [Mongory::TypeError] if symbol format is invalid
116
+ def self.validate_method(method_sym)
117
+ return if method_sym.is_a?(Symbol) && method_sym.match?(/^([a-z]+_)*[a-z]+$/)
118
+
119
+ raise Mongory::TypeError, "Method symbol must match /^([a-z]+_)*[a-z]+$/, but got #{method_sym.inspect}"
120
+ end
121
+ end
122
+
123
+ # @private
124
+ #
125
+ # Internal helper representing a registration of an operator and its associated symbol snippet method.
126
+ # Used to delay method definition on Symbol until explicitly enabled.
127
+ #
128
+ # Each instance holds:
129
+ # - the method symbol (e.g., `:gt`)
130
+ # - the corresponding Mongo-style operator (e.g., `"$gt"`)
131
+ #
132
+ # These instances are collected and replayed upon calling `Matchers.enable_symbol_snippets!`.
133
+ #
134
+ # @!attribute method_sym
135
+ # @return [Symbol] the symbol method name (e.g., :in, :gt, :exists)
136
+ # @!attribute operator
137
+ # @return [String] the Mongo-style operator this snippet maps to (e.g., "$in")
138
+ Registry = Struct.new(:method_sym, :operator) do
139
+ # Defines a method on Symbol to support operator snippet expansion.
140
+ #
141
+ # @return [void]
142
+ def apply!
143
+ return if Symbol.method_defined?(method_sym)
144
+
145
+ operator = operator()
146
+ Symbol.define_method(method_sym) do
147
+ Mongory::QueryOperator.new(to_s, operator)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ require_relative 'matchers/abstract_matcher'
155
+ require_relative 'matchers/abstract_multi_matcher'
156
+ require_relative 'matchers/abstract_operator_matcher'
157
+ require_relative 'matchers/literal_matcher'
158
+ require_relative 'matchers/hash_condition_matcher'
159
+ require_relative 'matchers/and_matcher'
160
+ require_relative 'matchers/array_record_matcher'
161
+ require_relative 'matchers/elem_match_matcher'
162
+ require_relative 'matchers/every_matcher'
163
+ require_relative 'matchers/eq_matcher'
164
+ require_relative 'matchers/exists_matcher'
165
+ require_relative 'matchers/gt_matcher'
166
+ require_relative 'matchers/gte_matcher'
167
+ require_relative 'matchers/in_matcher'
168
+ require_relative 'matchers/field_matcher'
169
+ require_relative 'matchers/lt_matcher'
170
+ require_relative 'matchers/lte_matcher'
171
+ require_relative 'matchers/ne_matcher'
172
+ require_relative 'matchers/nin_matcher'
173
+ require_relative 'matchers/not_matcher'
174
+ require_relative 'matchers/or_matcher'
175
+ require_relative 'matchers/present_matcher'
176
+ require_relative 'matchers/regex_matcher'
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ # Only loaded when Mongoid is present
5
+ module MongoidPatch
6
+ # Regist Mongoid operator key object into KeyConverter
7
+ # @see Converters::KeyConverter
8
+ # @return [void]
9
+ def self.patch!
10
+ kc = Mongory::Converters::KeyConverter.instance
11
+ # It's Mongoid built-in key operator that born from `:key.gt`
12
+ kc.register(::Mongoid::Criteria::Queryable::Key) do |v|
13
+ kc.convert(@name.to_s, @operator => v)
14
+ end
15
+ end
16
+
17
+ patch!
18
+ end
19
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils'
4
+
5
+ module Mongory
6
+ # QueryBuilder provides a Mongo-like in-memory query interface.
7
+ #
8
+ # It supports condition chaining (`where`, `or`, `not`),
9
+ # limiting, and plucking fields.
10
+ #
11
+ # Internally it compiles all conditions and invokes `QueryMatcher`.
12
+ #
13
+ # @example
14
+ # records.mongory
15
+ # .where(:age.gte => 18)
16
+ # .or({ :name => /J/ }, { :name.eq => 'Bob' })
17
+ # .limit(2)
18
+ # .to_a
19
+ class QueryBuilder
20
+ include ::Enumerable
21
+ include Utils
22
+
23
+ # Initializes a new query builder with the given record set.
24
+ #
25
+ # @param records [Enumerable] the collection to query against
26
+ def initialize(records)
27
+ @records = records
28
+ set_matcher
29
+ end
30
+
31
+ # Iterates through all records that match the current matcher.
32
+ #
33
+ # @yieldparam record [Object]
34
+ # @return [Enumerator]
35
+ def each
36
+ return to_enum(:each) unless block_given?
37
+
38
+ @matcher.prepare_query
39
+ @records.each do |record|
40
+ yield record if @matcher.match?(record)
41
+ end
42
+ end
43
+
44
+ # Adds a condition to filter records using the given condition.
45
+ #
46
+ # @param condition [Hash]
47
+ # @return [QueryBuilder] a new builder instance
48
+ def where(condition)
49
+ self.and(condition)
50
+ end
51
+
52
+ # Adds a negated condition to the current query.
53
+ #
54
+ # @param condition [Hash]
55
+ # @return [QueryBuilder]
56
+ def not(condition)
57
+ self.and('$not' => condition)
58
+ end
59
+
60
+ # Adds one or more conditions combined with `$and`.
61
+ #
62
+ # @param conditions [Array<Hash>]
63
+ # @return [QueryBuilder]
64
+ def and(*conditions)
65
+ dup_instance_exec do
66
+ add_conditions('$and', conditions)
67
+ end
68
+ end
69
+
70
+ # Adds one or more conditions combined with `$or`.
71
+ #
72
+ # @param conditions [Array<Hash>]
73
+ # @return [QueryBuilder]
74
+ def or(*conditions)
75
+ operator = '$or'
76
+ dup_instance_exec do
77
+ if @matcher.condition.each_key.all? { |k| k == operator }
78
+ add_conditions(operator, conditions)
79
+ else
80
+ set_matcher(operator => [@matcher.condition.dup, *conditions])
81
+ end
82
+ end
83
+ end
84
+
85
+ # Adds a `$or` query combined inside an `$and` block.
86
+ # This is a semantic alias for `.and('$or' => [...])`
87
+ #
88
+ # @param conditions [Array<Hash>]
89
+ # @return [QueryBuilder]
90
+ def any_of(*conditions)
91
+ self.and('$or' => conditions)
92
+ end
93
+
94
+ def in(condition)
95
+ self.and(wrap_values_with_key(condition, '$in'))
96
+ end
97
+
98
+ def nin(condition)
99
+ self.and(wrap_values_with_key(condition, '$nin'))
100
+ end
101
+
102
+ # Limits the number of records returned by the query.
103
+ #
104
+ # @param count [Integer]
105
+ # @return [QueryBuilder]
106
+ def limit(count)
107
+ dup_instance_exec do
108
+ @records = take(count)
109
+ end
110
+ end
111
+
112
+ # Extracts selected fields from matching records.
113
+ #
114
+ # @param field [Symbol, String]
115
+ # @param fields [Array<Symbol, String>]
116
+ # @return [Array<Object>, Array<Array<Object>>]
117
+ def pluck(field, *fields)
118
+ if fields.empty?
119
+ map { |record| record[field] }
120
+ else
121
+ fields.unshift(field)
122
+ map { |record| fields.map { |key| record[key] } }
123
+ end
124
+ end
125
+
126
+ # Returns the raw parsed condition for this query.
127
+ #
128
+ # @return [Hash] the raw compiled condition
129
+ def condition
130
+ @matcher.condition
131
+ end
132
+
133
+ alias_method :selector, :condition
134
+
135
+ # Prints the internal matcher tree structure for the current query.
136
+ # Will output a human-readable visual tree of matchers.
137
+ # This is useful for debugging and visualizing complex conditions.
138
+ #
139
+ # @return [void]
140
+ def explain
141
+ @matcher.match?(@records.first)
142
+ @matcher.render_tree
143
+ nil
144
+ end
145
+
146
+ private
147
+
148
+ # @private
149
+ # Duplicates the query and executes the block in context.
150
+ #
151
+ # @yieldparam dup [QueryBuilder]
152
+ # @return [QueryBuilder]
153
+ def dup_instance_exec(&block)
154
+ dup.tap do |obj|
155
+ obj.instance_exec(&block)
156
+ end
157
+ end
158
+
159
+ # @private
160
+ # Builds the internal matcher tree from a condition hash.
161
+ # Used to eagerly parse conditions to improve inspect/debug visibility.
162
+ #
163
+ # @param condition [Hash]
164
+ # @return [void]
165
+ def set_matcher(condition = {})
166
+ @matcher = QueryMatcher.new(condition)
167
+ end
168
+
169
+ # @private
170
+ # Merges additional conditions into the matcher.
171
+ #
172
+ # @param key [String, Symbol]
173
+ # @param conditions [Array<Hash>]
174
+ def add_conditions(key, conditions)
175
+ condition_dup = @matcher.condition.dup
176
+ condition_dup[key] ||= []
177
+ condition_dup[key] += conditions
178
+ set_matcher(condition_dup)
179
+ end
180
+
181
+ def wrap_values_with_key(condition, key)
182
+ condition.transform_values do |sub_condition|
183
+ { key => sub_condition }
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils'
4
+
5
+ module Mongory
6
+ # The top-level matcher for compiled query conditions.
7
+ #
8
+ # Delegates to {Matchers::LiteralMatcher} after transforming input
9
+ # via {Converters::ConditionConverter}.
10
+ #
11
+ # Typically used internally by `QueryBuilder`.
12
+ #
13
+ # Conversion via Mongory.data_converter is applied to the record
14
+ #
15
+ # @example
16
+ # matcher = QueryMatcher.build({ :age.gte => 18 })
17
+ # matcher.match?(record)
18
+ #
19
+ # @see Matchers::LiteralMatcher
20
+ # @see Converters::ConditionConverter
21
+ class QueryMatcher < Matchers::LiteralMatcher
22
+ # @param condition [Hash<Symbol, Object>] a query condition using operator-style symbol keys,
23
+ # e.g. { :age.gt => 18 }, which will be parsed by `Mongory.condition_converter`.
24
+ def initialize(condition)
25
+ super(Mongory.condition_converter.convert(condition))
26
+ end
27
+
28
+ # Matches the given record against the condition.
29
+ #
30
+ # @param record [Object] the raw input record (e.g., Hash or model object) to be matched.
31
+ # It will be converted internally using `Mongory.data_converter`.
32
+ # @return [Boolean] whether the record satisfies the condition
33
+ def match(record)
34
+ super(Mongory.data_converter.convert(record))
35
+ end
36
+
37
+ # Renders the full matcher tree for the current query.
38
+ #
39
+ # This method is intended to be the public entry point for rendering
40
+ # the matcher tree. It does not accept any arguments and internally
41
+ # handles rendering via the configured pretty-print logic.
42
+ #
43
+ # Subclasses or internal matchers should implement their own
44
+ # `#render_tree(prefix, is_last:)` for internal recursion.
45
+ #
46
+ # @return [void]
47
+ # @see Matchers::LiteralMatcher#render_tree
48
+ def render_tree
49
+ super
50
+ end
51
+
52
+ # Prepares the query for execution by ensuring all necessary matchers are initialized.
53
+ # This is called before query execution to avoid premature matcher tree construction.
54
+ #
55
+ # @return [void]
56
+ alias_method :prepare_query, :check_validity!
57
+
58
+ # Overrides the parent class's check_validity! to prevent premature matcher tree construction.
59
+ # This matcher does not require validation, so this is a no-op.
60
+ #
61
+ # @return [void]
62
+ def check_validity!
63
+ # No-op for this matcher
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ # Wrapper for symbol-based operator expressions.
5
+ #
6
+ # Used to support DSL like `:age.gt => 18` where `gt` maps to `$gt`.
7
+ # Converts into: `{ "age" => { "$gt" => 18 } }`
8
+ class QueryOperator
9
+ # Initializes a new query operator wrapper.
10
+ #
11
+ # @param name [String] the original field name
12
+ # @param operator [String] the Mongo-style operator (e.g., '$gt')
13
+ def initialize(name, operator)
14
+ @name = name
15
+ @operator = operator
16
+ end
17
+
18
+ # Converts the operator and value into a condition hash.
19
+ #
20
+ # Typically called by the key converter.
21
+ #
22
+ # @param other [Object] the value to match against
23
+ # @return [Hash] converted query condition
24
+ def __expr_part__(other, *)
25
+ Converters::KeyConverter.instance.convert(@name, @operator => other)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+ require_relative 'utils/rails_patch'
5
+
6
+ module Mongory
7
+ # @see Utils::RailsPatch
8
+ class Railtie < Rails::Railtie
9
+ initializer 'mongory.patch_utils' do
10
+ [Mongory::Utils, *Mongory::Utils.included_classes].each do |klass|
11
+ klass.prepend(Mongory::Utils::RailsPatch)
12
+ end
13
+ end
14
+ end
15
+ end