mongory 0.7.3-aarch64-linux

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.

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,257 @@
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 Basic query
14
+ # records.mongory
15
+ # .where(:age.gte => 18)
16
+ # .or({ :name => /J/ }, { :name.eq => 'Bob' })
17
+ # .limit(2)
18
+ # .to_a
19
+ #
20
+ # @example Complex query
21
+ # records.mongory
22
+ # .where(:status => 'active')
23
+ # .not(:age.lt => 18)
24
+ # .any_of({ :role => 'admin' }, { :role => 'moderator' })
25
+ # .pluck(:name, :email)
26
+ class QueryBuilder
27
+ include ::Enumerable
28
+ include Utils
29
+
30
+ # Initializes a new query builder with the given record set.
31
+ #
32
+ # @param records [Enumerable] the collection to query against
33
+ def initialize(records, context: Utils::Context.new)
34
+ @records = records
35
+ @context = context
36
+ set_matcher
37
+ end
38
+
39
+ # Iterates through all records that match the current matcher.
40
+ # Uses the standard matcher implementation.
41
+ #
42
+ # @yieldparam record [Object] each matching record
43
+ # @return [Enumerator] if no block given
44
+ # @return [void] if block given
45
+ def each
46
+ return to_enum(:each) unless block_given?
47
+
48
+ @matcher.prepare_query
49
+ @records.each do |record|
50
+ @context.current_record = record
51
+ yield record if @matcher.match?(record)
52
+ end
53
+ end
54
+
55
+ # Iterates through all records that match the current matcher.
56
+ # Uses a compiled Proc for faster matching.
57
+ #
58
+ # @yieldparam record [Object] each matching record
59
+ # @return [Enumerator] if no block given
60
+ # @return [void] if block given
61
+ def fast
62
+ return to_enum(:fast) unless block_given?
63
+
64
+ @context.need_convert = false
65
+ @matcher.prepare_query
66
+ matcher_block = @matcher.to_proc
67
+ @records.each do |record|
68
+ yield record if matcher_block.call(record)
69
+ end
70
+ end
71
+
72
+ # Adds a condition to filter records using the given condition.
73
+ # This is an alias for `and`.
74
+ #
75
+ # @param condition [Hash] the condition to add
76
+ # @return [QueryBuilder] a new builder instance
77
+ def where(condition)
78
+ self.and(condition)
79
+ end
80
+
81
+ # Adds a negated condition to the current query.
82
+ # Wraps the condition in a `$not` operator.
83
+ #
84
+ # @param condition [Hash] the condition to negate
85
+ # @return [QueryBuilder] a new builder instance
86
+ def not(condition)
87
+ self.and('$not' => condition)
88
+ end
89
+
90
+ # Adds one or more conditions combined with `$and`.
91
+ # All conditions must match for a record to be included.
92
+ #
93
+ # @param conditions [Array<Hash>] the conditions to add
94
+ # @return [QueryBuilder] a new builder instance
95
+ def and(*conditions)
96
+ dup_instance_exec do
97
+ add_conditions('$and', conditions)
98
+ end
99
+ end
100
+
101
+ # Adds one or more conditions combined with `$or`.
102
+ # Any condition can match for a record to be included.
103
+ #
104
+ # @param conditions [Array<Hash>] the conditions to add
105
+ # @return [QueryBuilder] a new builder instance
106
+ def or(*conditions)
107
+ operator = '$or'
108
+ dup_instance_exec do
109
+ if @matcher.condition.each_key.all? { |k| k == operator }
110
+ add_conditions(operator, conditions)
111
+ else
112
+ set_matcher(operator => [@matcher.condition.dup, *conditions])
113
+ end
114
+ end
115
+ end
116
+
117
+ # Adds a `$or` query combined inside an `$and` block.
118
+ # This is a semantic alias for `.and('$or' => [...])`
119
+ #
120
+ # @param conditions [Array<Hash>] the conditions to add
121
+ # @return [QueryBuilder] a new builder instance
122
+ def any_of(*conditions)
123
+ self.and('$or' => conditions)
124
+ end
125
+
126
+ # Adds an `$in` condition to the query.
127
+ # Matches records where the field value is in the given array.
128
+ #
129
+ # @param condition [Hash] the field and values to match
130
+ # @return [QueryBuilder] a new builder instance
131
+ def in(condition)
132
+ self.and(wrap_values_with_key(condition, '$in'))
133
+ end
134
+
135
+ # Adds a `$nin` condition to the query.
136
+ # Matches records where the field value is not in the given array.
137
+ #
138
+ # @param condition [Hash] the field and values to exclude
139
+ # @return [QueryBuilder] a new builder instance
140
+ def nin(condition)
141
+ self.and(wrap_values_with_key(condition, '$nin'))
142
+ end
143
+
144
+ # Limits the number of records returned by the query.
145
+ #
146
+ # @param count [Integer] the maximum number of records to return
147
+ # @return [QueryBuilder] a new builder instance
148
+ def limit(count)
149
+ dup_instance_exec do
150
+ @records = take(count)
151
+ end
152
+ end
153
+
154
+ # Extracts selected fields from matching records.
155
+ #
156
+ # @param field [Symbol, String] the first field to extract
157
+ # @param fields [Array<Symbol, String>] additional fields to extract
158
+ # @return [Array<Object>] array of single field values if one field given
159
+ # @return [Array<Array<Object>>] array of field value arrays if multiple fields given
160
+ def pluck(field, *fields)
161
+ if fields.empty?
162
+ map { |record| record[field] }
163
+ else
164
+ fields.unshift(field)
165
+ map { |record| fields.map { |key| record[key] } }
166
+ end
167
+ end
168
+
169
+ # Returns the raw parsed condition for this query.
170
+ #
171
+ # @return [Hash] the raw compiled condition
172
+ def raw_condition
173
+ @matcher.condition
174
+ end
175
+
176
+ # Creates a new query builder with additional context configuration.
177
+ #
178
+ # @param addon_context [Hash] Additional context configuration to merge
179
+ # @return [QueryBuilder] A new query builder instance with merged context
180
+ # @note Creates a new query builder with the current matcher's condition and merged context
181
+ # @example
182
+ # query.with_context(need_convert: false) #=> Returns a new query builder with conversion disabled
183
+ def with_context(addon_context = {})
184
+ dup_instance_exec do
185
+ @context = @context.dup
186
+ @context.config.merge!(addon_context)
187
+ set_matcher(@matcher.condition)
188
+ end
189
+ end
190
+
191
+ # Prints the internal matcher tree structure for the current query.
192
+ # Will output a human-readable visual tree of matchers.
193
+ # This is useful for debugging and visualizing complex conditions.
194
+ #
195
+ # @return [void]
196
+ def explain
197
+ @matcher.match?(@records.first)
198
+ @matcher.render_tree
199
+ nil
200
+ end
201
+
202
+ def c
203
+ return self unless defined?(Mongory::CMatcher)
204
+
205
+ c_builder = CQueryBuilder.new(@records, context: @context)
206
+ c_builder.send(:set_matcher, @matcher.condition)
207
+ c_builder
208
+ end
209
+
210
+ private
211
+
212
+ # @private
213
+ # Duplicates the query and executes the block in context.
214
+ #
215
+ # @return [QueryBuilder] the modified duplicate
216
+ def dup_instance_exec(&block)
217
+ dup.tap do |obj|
218
+ obj.instance_exec(&block)
219
+ end
220
+ end
221
+
222
+ # @private
223
+ # Builds the internal matcher tree from a condition hash.
224
+ # Used to eagerly parse conditions to improve inspect/debug visibility.
225
+ #
226
+ # @param condition [Hash] the condition to build the matcher from
227
+ # @return [void]
228
+ def set_matcher(condition = {})
229
+ @matcher = QueryMatcher.new(condition, context: @context)
230
+ end
231
+
232
+ # @private
233
+ # Merges additional conditions into the matcher.
234
+ #
235
+ # @param key [String, Symbol] the operator key (e.g. '$and', '$or')
236
+ # @param conditions [Array<Hash>] the conditions to add
237
+ # @return [void]
238
+ def add_conditions(key, conditions)
239
+ condition_dup = {}.merge!(@matcher.condition)
240
+ condition_dup[key] ||= []
241
+ condition_dup[key] += conditions
242
+ set_matcher(condition_dup)
243
+ end
244
+
245
+ # @private
246
+ # Wraps values in a condition hash with a given operator key.
247
+ #
248
+ # @param condition [Hash] the condition to transform
249
+ # @param key [String] the operator key to wrap with
250
+ # @return [Hash] the transformed condition
251
+ def wrap_values_with_key(condition, key)
252
+ condition.transform_values do |sub_condition|
253
+ { key => sub_condition }
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,93 @@
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
+ # before matching to ensure consistent data types.
15
+ #
16
+ # @example Basic matching
17
+ # matcher = QueryMatcher.new({ :age.gte => 18 })
18
+ # matcher.match?(record) # => true/false
19
+ #
20
+ # @example Complex condition
21
+ # matcher = QueryMatcher.new({
22
+ # :age.gte => 18,
23
+ # :$or => [
24
+ # { :name => /J/ },
25
+ # { :name.eq => 'Bob' }
26
+ # ]
27
+ # })
28
+ #
29
+ # @see Matchers::LiteralMatcher
30
+ # @see Converters::ConditionConverter
31
+ class QueryMatcher < Matchers::HashConditionMatcher
32
+ # Initializes a new query matcher with the given condition.
33
+ # The condition is converted using Mongory.condition_converter
34
+ # before being passed to the parent matcher.
35
+ #
36
+ # @param condition [Hash<Symbol, Object>] a query condition using operator-style symbol keys,
37
+ # e.g. { :age.gt => 18 }, which will be parsed by `Mongory.condition_converter`.
38
+ # @param context [Context] The query context containing configuration and current record
39
+ # @option context [Hash] :config The query configuration
40
+ # @option context [Object] :current_record The current record being processed
41
+ # @option context [Boolean] :need_convert Whether the record needs to be converted
42
+ def initialize(condition, context: Utils::Context.new)
43
+ super(Mongory.condition_converter.convert(condition), context: context)
44
+ end
45
+
46
+ # Returns a Proc that can be used for fast matching.
47
+ # The Proc converts the record using Mongory.data_converter
48
+ # and delegates to the superclass's raw_proc.
49
+ #
50
+ # @return [Proc] A proc that performs query matching with context awareness
51
+ # @note The proc includes error handling and context-based record conversion
52
+ def raw_proc
53
+ super_proc = super
54
+ need_convert = @context.need_convert
55
+ data_converter = Mongory.data_converter
56
+
57
+ Proc.new do |record|
58
+ record = data_converter.convert(record) if need_convert
59
+ super_proc.call(record)
60
+ rescue StandardError
61
+ false
62
+ end
63
+ end
64
+
65
+ # Renders the full matcher tree for the current query.
66
+ # This method is intended to be the public entry point for rendering
67
+ # the matcher tree. It does not accept any arguments and internally
68
+ # handles rendering via the configured pretty-print logic.
69
+ #
70
+ # Subclasses or internal matchers should implement their own
71
+ # `#render_tree(prefix, is_last:)` for internal recursion.
72
+ #
73
+ # @return [void]
74
+ # @see Matchers::LiteralMatcher#render_tree
75
+ def render_tree
76
+ super
77
+ end
78
+
79
+ # Prepares the query for execution by ensuring all necessary matchers are initialized.
80
+ # This is called before query execution to avoid premature matcher tree construction.
81
+ #
82
+ # @return [void]
83
+ alias_method :prepare_query, :check_validity!
84
+
85
+ # Overrides the parent class's check_validity! to prevent premature matcher tree construction.
86
+ # This matcher does not require validation, so this is a no-op.
87
+ #
88
+ # @return [void]
89
+ def check_validity!
90
+ # No-op for this matcher
91
+ end
92
+ end
93
+ 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
+ { @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
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Utils
5
+ # Context is a utility class that provides a stable but mutatable
6
+ # shared context for the Mongory query builder. It holds the configuration
7
+ # and the current record being matcher tree processed.
8
+ #
9
+ # @example
10
+ # context = Mongory::Utils::Context.new(config)
11
+ # context.current_record = record
12
+ # context.config = new_config
13
+ #
14
+ # @attr [Config] config The configuration object for the context
15
+ # @attr [Record] current_record The current record being processed in the matcher tree
16
+ # @attr [Boolean] need_convert Whether the record needs to be converted before matching
17
+ class Context
18
+ attr_accessor :config, :current_record, :need_convert
19
+
20
+ # Initializes a new Context instance with the given configuration.
21
+ #
22
+ # @param config [Config] The configuration object for the context.
23
+ # @return [Context] A new Context instance.
24
+ def initialize(config = {})
25
+ @config = config
26
+ @current_record = nil
27
+ @need_convert = true
28
+ end
29
+
30
+ # Creates a duplicate of the context with its own configuration.
31
+ #
32
+ # @return [Context] A new context instance with duplicated configuration
33
+ # @note The new context shares the same configuration object but has its own state
34
+ def dup
35
+ new_context = super
36
+ new_context.config = @config.dup
37
+ new_context
38
+ end
39
+
40
+ def to_hash
41
+ {
42
+ config: @config,
43
+ need_convert: @need_convert
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Utils
5
+ # Debugger provides an internal tracing system for query matching.
6
+ # It tracks matcher evaluation in a tree structure and provides tree-like format output.
7
+ #
8
+ # It captures each matcher evaluation with indentation and visualizes the hierarchy,
9
+ # helping developers understand nested matcher flows (e.g. `$and`, `$or`, etc).
10
+ #
11
+ # Usage:
12
+ # Debugger.instance.with_indent { ... } # wraps around matcher logic
13
+ # Debugger.instance.display # prints trace tree after execution
14
+ #
15
+ # Note:
16
+ # The trace is recorded in post-order (leaf nodes enter first),
17
+ # and `reorder_traces_for_display` is used to transform it into a
18
+ # pre-order format suitable for display.
19
+ #
20
+ # This class is a singleton and should be accessed via `Debugger.instance`.
21
+ #
22
+ # @example Enable debugging
23
+ # Debugger.instance.enable
24
+ # Mongory::QueryBuilder.new(...).filter(...)
25
+ #
26
+ class Debugger < SingletonBuilder
27
+ include Singleton
28
+
29
+ def initialize
30
+ super(self.class.name)
31
+ @indent_level = -1
32
+ @trace_entries = []
33
+ end
34
+
35
+ # Enables debug mode by aliasing `to_proc` to `debug_proc`.
36
+ # @return [void]
37
+ # @note Changes the behavior of to_proc to use debug_proc instead of cached_proc
38
+ def enable
39
+ Matchers::AbstractMatcher.alias_method :to_proc, :debug_proc
40
+ end
41
+
42
+ # Disables debug mode by restoring `to_proc` to `cached_proc`.
43
+ # @return [void]
44
+ # @note Restores the original behavior of to_proc
45
+ def disable
46
+ Matchers::AbstractMatcher.alias_method :to_proc, :cached_proc
47
+ end
48
+
49
+ # Wraps a matcher evaluation block with indentation control.
50
+ #
51
+ # It increments the internal indent level before the block,
52
+ # and decrements it after. The yielded block's return value
53
+ # is used as trace content and pushed onto the trace_entries.
54
+ #
55
+ # @yieldreturn [String] the trace content to log
56
+ # @return [Object] the result of the block
57
+ def with_indent
58
+ @indent_level += 1
59
+ display_string = yield
60
+ @trace_entries << TraceEntry.new(display_string, @indent_level)
61
+ ensure
62
+ @indent_level -= 1
63
+ end
64
+
65
+ # Prints the visualized trace tree to STDOUT.
66
+ #
67
+ # This processes the internal trace_entries (post-order) into
68
+ # a structured pre-order list that represents nested matcher evaluation.
69
+ # @return [void]
70
+ def display
71
+ reorder_traces_for_display(@trace_entries).each do |trace|
72
+ puts trace.formatted
73
+ end
74
+
75
+ @trace_entries.clear
76
+ nil
77
+ end
78
+
79
+ # Recursively reorders trace lines by indentation level to produce a
80
+ # display-friendly structure where parents precede children.
81
+ #
82
+ # The original trace is built in post-order (leaf first), and this method
83
+ # transforms it into a pre-order structure suitable for tree display.
84
+ #
85
+ # @param traces [Array<TraceEntry>] the raw trace lines (leaf-first)
86
+ # @param level [Integer] current processing level
87
+ # @return [Array<TraceEntry>] reordered trace lines for display
88
+ def reorder_traces_for_display(traces, level = 0)
89
+ result = []
90
+ group = []
91
+ traces.each do |trace|
92
+ if trace.level == level
93
+ result << trace
94
+ result.concat reorder_traces_for_display(group, level + 1)
95
+ group = []
96
+ else
97
+ group << trace
98
+ end
99
+ end
100
+
101
+ result
102
+ end
103
+
104
+ def clear
105
+ # Clears the internal trace buffer.
106
+ # This is useful when re-running a query and avoiding stale trace output.
107
+ # @return [void]
108
+ @trace_entries.clear
109
+ end
110
+
111
+ # @private
112
+ # A trace entry representing a single matcher output at a given indentation level.
113
+ #
114
+ # @!attribute text
115
+ # @return [String] the string to be printed
116
+ # @!attribute level
117
+ # @return [Integer] the indentation level of this entry
118
+ TraceEntry = Struct.new(:text, :level) do
119
+ def formatted
120
+ "#{' ' * level}#{text}"
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Utils
5
+ # Only loaded when Rails is present
6
+ module RailsPatch
7
+ # Use Object#present? which defined in Rails.
8
+ # @param target[Object]
9
+ # @return [Boolean]
10
+ def is_present?(target)
11
+ target.present?
12
+ end
13
+
14
+ # Use Object#blank? which defined in Rails.
15
+ # @param target[Object]
16
+ # @return [Boolean]
17
+ def is_blank?(target)
18
+ target.blank?
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Utils
5
+ # A singleton placeholder object used to represent special sentinel values.
6
+ #
7
+ # Used in situations where `nil` is a valid value and cannot be used as a marker.
8
+ # Typically used for internal constants like `NOTHING` or `KEY_NOT_FOUND`.
9
+ #
10
+ # @example
11
+ # NOTHING = SingletonBuilder.new('NOTHING')
12
+ # value == NOTHING # => true if placeholder
13
+ class SingletonBuilder
14
+ # @param label [String] a human-readable label for the marker
15
+ def initialize(label, &block)
16
+ @label = label
17
+ instance_eval(&block) if block_given?
18
+ end
19
+
20
+ # @return [String] formatted label
21
+ def inspect
22
+ "#<#{@label}>"
23
+ end
24
+
25
+ # @return [String] formatted label
26
+ def to_s
27
+ "#<#{@label}>"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require_relative 'utils/singleton_builder'
5
+ require_relative 'utils/debugger'
6
+ require_relative 'utils/context'
7
+
8
+ module Mongory
9
+ # Utility helpers shared across Mongory internals.
10
+ #
11
+ # Includes blank checking, present checking,
12
+ # and class-level instance method caching.
13
+ module Utils
14
+ # When included, also extends the including class with ClassMethods.
15
+ # And record which class include this module.
16
+ #
17
+ # @param base [Class, Module]
18
+ def self.included(base)
19
+ base.extend(ClassMethods)
20
+ super
21
+ included_classes << base
22
+ end
23
+
24
+ # Where to record classes that include Utils.
25
+ #
26
+ # @return [Array]
27
+ def self.included_classes
28
+ @included_classes ||= []
29
+ end
30
+
31
+ # Checks if an object is "present".
32
+ # Inverse of {#is_blank?}.
33
+ #
34
+ # @param obj [Object]
35
+ # @return [Boolean]
36
+ def is_present?(obj)
37
+ !is_blank?(obj)
38
+ end
39
+
40
+ # Determines whether an object is considered "blank".
41
+ # Nil, false, empty string/array/hash are blank.
42
+ #
43
+ # @param obj [Object]
44
+ # @return [Boolean]
45
+ def is_blank?(obj)
46
+ case obj
47
+ when false, nil
48
+ true
49
+ when Hash, Array, String, Set
50
+ obj.empty?
51
+ else
52
+ false
53
+ end
54
+ end
55
+
56
+ # Class-level methods injected via Utils.
57
+ module ClassMethods
58
+ # Defines a lazily-evaluated, memoized instance method.
59
+ #
60
+ # @param name [Symbol] the method name
61
+ # @yield block to compute the value
62
+ # @return [void]
63
+ #
64
+ # @example
65
+ # define_instance_cache_method(:expensive_thing) { compute_something }
66
+ def define_instance_cache_method(name, &block)
67
+ instance_key = :"@#{name}"
68
+ define_method(name) do
69
+ return instance_variable_get(instance_key) if instance_variable_defined?(instance_key)
70
+
71
+ instance_variable_set(instance_key, instance_exec(&block))
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end