mongory 0.7.3-x64-mingw32

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,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # ElemMatchMatcher implements the logic for Mongo-style `$elemMatch`.
6
+ #
7
+ # It is used to determine if *any* element in an array matches the given condition.
8
+ #
9
+ # This matcher delegates element-wise comparison to HashConditionMatcher,
10
+ # allowing nested conditions to be applied recursively.
11
+ #
12
+ # Typically used internally by ArrayRecordMatcher when dealing with
13
+ # non-indexed hash-style subconditions.
14
+ #
15
+ # @example
16
+ # matcher = ElemMatchMatcher.build({ status: 'active' })
17
+ # matcher.match?([{ status: 'inactive' }, { status: 'active' }]) #=> true
18
+ #
19
+ # @see HashConditionMatcher
20
+ class ElemMatchMatcher < HashConditionMatcher
21
+ # Creates a raw Proc that performs the element matching operation.
22
+ # The Proc checks if any element in the array matches the condition.
23
+ #
24
+ # @return [Proc] a Proc that performs the element matching operation
25
+ def raw_proc
26
+ super_proc = super
27
+ need_convert = @context.need_convert
28
+ data_converter = Mongory.data_converter
29
+
30
+ Proc.new do |collection|
31
+ collection.any? do |record|
32
+ record = data_converter.convert(record) if need_convert
33
+ super_proc.call(record)
34
+ end
35
+ end
36
+ end
37
+
38
+ def priority
39
+ 3 + super
40
+ end
41
+
42
+ # Ensures the condition is a Hash.
43
+ #
44
+ # @raise [Mongory::TypeError] if the condition is not a Hash
45
+ # @return [void]
46
+ def check_validity!
47
+ raise TypeError, '$elemMatch needs a Hash.' unless @condition.is_a?(Hash)
48
+
49
+ super
50
+ end
51
+ end
52
+
53
+ register(:elem_match, '$elemMatch', ElemMatchMatcher)
54
+ end
55
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # EqMatcher matches values using the equality operator `==`.
6
+ #
7
+ # It inherits from AbstractMatcher and defines its operator as `:==`.
8
+ #
9
+ # Used for conditions like:
10
+ # - { age: { '$eq' => 30 } }
11
+ # - { name: "Alice" } (implicit fallback)
12
+ #
13
+ # This matcher supports any Ruby object that implements `#==`.
14
+ #
15
+ # @example
16
+ # matcher = EqMatcher.build(42)
17
+ # matcher.match?(42) #=> true
18
+ # matcher.match?("42") #=> false
19
+ #
20
+ # @note This matcher is also used as the fallback for non-operator literal values,
21
+ # such as `{ name: "Alice" }`, when no other specialized matcher is applicable.
22
+ #
23
+ # @note Equality behavior depends on how `==` is implemented for the given objects.
24
+ #
25
+ # @see AbstractMatcher
26
+ class EqMatcher < AbstractMatcher
27
+ # Creates a raw Proc that performs the equality check.
28
+ # The Proc uses the `==` operator to compare values.
29
+ #
30
+ # @return [Proc] a Proc that performs the equality check
31
+ def raw_proc
32
+ condition = @condition
33
+
34
+ Proc.new do |record|
35
+ record == condition
36
+ end
37
+ end
38
+
39
+ def priority
40
+ 1
41
+ end
42
+ end
43
+
44
+ register(:eq, '$eq', EqMatcher)
45
+ end
46
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # EveryMatcher implements the logic for Mongo-style `$every` which is not really support in MongoDB.
6
+ #
7
+ # It is used to determine if *all* element in an array matches the given condition.
8
+ #
9
+ # This matcher delegates element-wise comparison to HashConditionMatcher,
10
+ # allowing nested conditions to be applied recursively.
11
+ #
12
+ # @example
13
+ # matcher = EveryMatcher.build({ status: 'active' })
14
+ # matcher.match?([{ status: 'inactive' }, { status: 'active' }]) #=> false
15
+ #
16
+ # @see HashConditionMatcher
17
+ class EveryMatcher < HashConditionMatcher
18
+ # Creates a raw Proc that performs the element matching operation.
19
+ # The Proc checks if all elements in the array match the condition.
20
+ #
21
+ # @return [Proc] A proc that performs element matching with context awareness
22
+ # @note The proc includes error handling and context-based record conversion
23
+ def raw_proc
24
+ super_proc = super
25
+ need_convert = @context.need_convert
26
+ data_converter = Mongory.data_converter
27
+
28
+ Proc.new do |collection|
29
+ next false unless collection.is_a?(Array)
30
+ next false if collection.empty?
31
+
32
+ collection.all? do |record|
33
+ record = data_converter.convert(record) if need_convert
34
+ super_proc.call(record)
35
+ end
36
+ end
37
+ end
38
+
39
+ def priority
40
+ 3 + super
41
+ end
42
+
43
+ # Ensures the condition is a Hash.
44
+ #
45
+ # @raise [Mongory::TypeError] if the condition is not a Hash
46
+ # @return [void]
47
+ def check_validity!
48
+ raise TypeError, '$every needs a Hash.' unless @condition.is_a?(Hash)
49
+
50
+ super
51
+ end
52
+ end
53
+
54
+ register(:every, '$every', EveryMatcher)
55
+ end
56
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # ExistsMatcher implements the `$exists` operator, which checks whether a key exists.
6
+ #
7
+ # It transforms the presence (or absence) of a field into a boolean value,
8
+ # then compares it to the condition using the `==` operator.
9
+ #
10
+ # This matcher ensures the condition is strictly a boolean (`true` or `false`).
11
+ #
12
+ # @example
13
+ # matcher = ExistsMatcher.build(true)
14
+ # matcher.match?(42) #=> true
15
+ # matcher.match?(KEY_NOT_FOUND) #=> false
16
+ #
17
+ # matcher = ExistsMatcher.build(false)
18
+ # matcher.match?(KEY_NOT_FOUND) #=> true
19
+ #
20
+ # @see AbstractMatcher
21
+ class ExistsMatcher < AbstractMatcher
22
+ # Creates a raw Proc that performs the existence check.
23
+ # The Proc checks if the record exists and compares it to the condition.
24
+ #
25
+ # @return [Proc] A proc that performs existence check with error handling
26
+ def raw_proc
27
+ condition = @condition
28
+
29
+ Proc.new do |record|
30
+ # Check if the record is nil or KEY_NOT_FOUND
31
+ # and compare it to the condition.
32
+ (record != KEY_NOT_FOUND) == condition
33
+ end
34
+ end
35
+
36
+ def priority
37
+ 2
38
+ end
39
+
40
+ # Ensures that the condition value is a valid boolean.
41
+ #
42
+ # @raise [TypeError] if condition is not true or false
43
+ # @return [void]
44
+ def check_validity!
45
+ return if [true, false].include?(@condition)
46
+
47
+ raise TypeError, "$exists needs a boolean, but got #{@condition.inspect}"
48
+ end
49
+ end
50
+
51
+ register(:exists, '$exists', ExistsMatcher)
52
+ end
53
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # FieldMatcher handles field-level matching by extracting and comparing field values.
6
+ #
7
+ # This matcher is responsible for:
8
+ # 1. Extracting field values from records using dot notation
9
+ # 2. Converting extracted values if needed
10
+ # 3. Delegating the actual comparison to a submatcher
11
+ #
12
+ # It supports:
13
+ # - Hash records with string/symbol keys
14
+ # - Array records with numeric indices
15
+ # - Objects that respond to `[]`
16
+ #
17
+ # @example Basic field matching
18
+ # matcher = FieldMatcher.build('age', 30)
19
+ # matcher.match?({ 'age' => 30 }) #=> true
20
+ # matcher.match?({ age: 30 }) #=> true
21
+ #
22
+ # @see LiteralMatcher
23
+ class FieldMatcher < LiteralMatcher
24
+ # A list of classes that should never be used for value digging.
25
+ # These typically respond to `#[]` but are semantically invalid for this context.
26
+ CLASSES_NOT_ALLOW_TO_DIG = [
27
+ ::String,
28
+ ::Integer,
29
+ ::Proc,
30
+ ::Method,
31
+ ::MatchData,
32
+ ::Thread,
33
+ ::Symbol
34
+ ].freeze
35
+
36
+ # Initializes a new field matcher.
37
+ #
38
+ # @param field [String, Symbol] the field to match against
39
+ # @param condition [Object] the condition to match with
40
+ # @param context [Context] the query context
41
+ def initialize(field, condition, context: Context.new)
42
+ @field = field
43
+ super(condition, context: context)
44
+ end
45
+
46
+ # Creates a raw Proc that performs the field matching operation.
47
+ # The Proc extracts the field value and delegates to the submatcher.
48
+ #
49
+ # This method first ensures the record is structurally eligible for field extraction—
50
+ # it must be a Hash, Array, or respond to `[]`. If the structure does not allow for
51
+ # field access (e.g., nil, primitive values, or unsupported types), the match returns false.
52
+ #
53
+ # The field value is then extracted using the following rules:
54
+ # - If the record is a Hash, it attempts to fetch using the field key,
55
+ # falling back to symbolized key if needed.
56
+ # - If the record is an Array, it fetches by index.
57
+ # - If the record does not support `[]` or is disallowed for dig operations,
58
+ # the match returns false immediately.
59
+ #
60
+ # Once the value is extracted, it is passed through the data converter
61
+ # and matched against the condition via the superclass.
62
+ #
63
+ # @example Matching a Hash with a nil field value
64
+ # matcher = Mongory::QueryMatcher.new(a: nil)
65
+ # matcher.match?({ a: nil }) # => true
66
+ #
67
+ # @example Record is nil (structure not diggable)
68
+ # matcher = Mongory::QueryMatcher.new(a: nil)
69
+ # matcher.match?(nil) # => false
70
+ #
71
+ # @example Matching against an Array by index
72
+ # matcher = Mongory::QueryMatcher.new(0 => /abc/)
73
+ # matcher.match?(['abcdef']) # => true
74
+ #
75
+ # @example Hash with symbol key, matcher uses string key
76
+ # matcher = Mongory::QueryMatcher.new('a' => 123)
77
+ # matcher.match?({ a: 123 }) # => true
78
+
79
+ # Creates a raw Proc that performs the field-based matching operation.
80
+ # The Proc extracts the field value and delegates matching to the superclass.
81
+ #
82
+ # @return [Proc] A proc that performs field-based matching with context awareness
83
+ # @note The proc handles field extraction and delegates matching to the superclass
84
+ def raw_proc
85
+ super_proc = super
86
+ field = @field
87
+ need_convert = @context.need_convert
88
+ data_converter = Mongory.data_converter
89
+
90
+ Proc.new do |record|
91
+ sub_record =
92
+ case record
93
+ when Hash
94
+ record.fetch(field) do
95
+ record.fetch(field.to_sym, KEY_NOT_FOUND)
96
+ end
97
+ when Array
98
+ record.fetch(field, KEY_NOT_FOUND)
99
+ when KEY_NOT_FOUND, *CLASSES_NOT_ALLOW_TO_DIG
100
+ next false
101
+ else
102
+ next false unless record.respond_to?(:[])
103
+
104
+ record[field]
105
+ end
106
+
107
+ sub_record = data_converter.convert(sub_record) if need_convert
108
+ super_proc.call(sub_record)
109
+ end
110
+ end
111
+
112
+ def priority
113
+ 1 + super
114
+ end
115
+
116
+ # Returns a unique key for this matcher, including the field name.
117
+ # Used for deduplication in multi-matchers.
118
+ #
119
+ # @return [String] a unique key for this matcher
120
+ # @see AbstractMultiMatcher#matchers
121
+ def uniq_key
122
+ super + "field:#{@field}"
123
+ end
124
+
125
+ private
126
+
127
+ # Returns a single-line summary of the field matcher including the field and condition.
128
+ #
129
+ # @return [String] a formatted title for tree display
130
+ def tree_title
131
+ "Field: #{@field.inspect} to match: #{@condition.inspect}"
132
+ end
133
+
134
+ # Custom display logic for debugging, including colored field highlighting.
135
+ #
136
+ # @param record [Object] the input record
137
+ # @param result [Boolean] match result
138
+ # @return [String] formatted debug string with highlighted field
139
+ def debug_display(record, result)
140
+ "#{self.class.name.split('::').last} #{colored_result(result)}, " \
141
+ "condition: #{@condition.inspect}, " \
142
+ "\e[30;47mfield: #{@field.inspect}\e[0m, " \
143
+ "record: #{record.inspect.gsub(@field.inspect, "\e[30;47m#{@field.inspect}\e[0m")}"
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # GtMatcher implements the `$gt` (greater than) operator.
6
+ #
7
+ # It returns true if the record is strictly greater than the condition.
8
+ #
9
+ # Inherits core logic from AbstractMatcher, including
10
+ # error handling and optional preprocessing.
11
+ #
12
+ # @example
13
+ # matcher = GtMatcher.build(10)
14
+ # matcher.match?(15) #=> true
15
+ # matcher.match?(10) #=> false
16
+ #
17
+ # @see AbstractMatcher
18
+ class GtMatcher < AbstractMatcher
19
+ # Creates a raw Proc that performs the greater-than comparison.
20
+ # The Proc uses the `>` operator to compare values.
21
+ #
22
+ # @return [Proc] A proc that performs greater-than comparison with error handling
23
+ # @note The proc includes error handling for invalid comparisons
24
+ def raw_proc
25
+ condition = @condition
26
+
27
+ Proc.new do |record|
28
+ record > condition
29
+ rescue StandardError
30
+ false
31
+ end
32
+ end
33
+
34
+ def priority
35
+ 3
36
+ end
37
+ end
38
+
39
+ register(:gt, '$gt', GtMatcher)
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # GteMatcher implements the `$gte` (greater than or equal) operator.
6
+ #
7
+ # It returns true if the record is greater than or equal to the condition value.
8
+ #
9
+ # Inherits comparison logic and error safety from AbstractMatcher.
10
+ #
11
+ # @example
12
+ # matcher = GteMatcher.build(10)
13
+ # matcher.match?(10) #=> true
14
+ # matcher.match?(11) #=> true
15
+ # matcher.match?(9) #=> false
16
+ #
17
+ # @see AbstractMatcher
18
+ class GteMatcher < AbstractMatcher
19
+ # Creates a raw Proc that performs the greater-than-or-equal comparison.
20
+ # The Proc uses the `>=` operator to compare values.
21
+ #
22
+ # @return [Proc] A proc that performs greater-than-or-equal comparison with error handling
23
+ # @note The proc includes error handling for invalid comparisons
24
+ def raw_proc
25
+ condition = @condition
26
+
27
+ Proc.new do |record|
28
+ record >= condition
29
+ rescue StandardError
30
+ false
31
+ end
32
+ end
33
+
34
+ def priority
35
+ 3
36
+ end
37
+ end
38
+
39
+ register(:gte, '$gte', GteMatcher)
40
+ end
41
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # HashConditionMatcher is responsible for handling field-level query conditions.
6
+ #
7
+ # It receives a Hash of key-value pairs and delegates each one to an appropriate matcher
8
+ # based on whether the key is a recognized operator or a data field path.
9
+ #
10
+ # Each subcondition is matched independently using the `:all?` strategy, meaning
11
+ # all subconditions must match for the entire HashConditionMatcher to succeed.
12
+ # For empty conditions, it returns true (using TRUE_PROC).
13
+ #
14
+ # This matcher plays a central role in dispatching symbolic query conditions
15
+ # to the appropriate field or operator matcher.
16
+ #
17
+ # @example Basic field matching
18
+ # matcher = HashConditionMatcher.build({ age: { :$gt => 30 }, active: true })
19
+ # matcher.match?(record) #=> true only if all subconditions match
20
+ #
21
+ # @example Empty conditions
22
+ # matcher = HashConditionMatcher.build({})
23
+ # matcher.match?(record) #=> true (uses TRUE_PROC)
24
+ #
25
+ # @see AbstractMultiMatcher
26
+ class HashConditionMatcher < AbstractMultiMatcher
27
+ enable_unwrap!
28
+
29
+ # Creates a raw Proc that performs the hash condition matching operation.
30
+ # The Proc combines all submatcher Procs and returns true only if all match.
31
+ # For empty conditions, returns TRUE_PROC.
32
+ #
33
+ # @return [Proc] a Proc that performs the hash condition matching operation
34
+ def raw_proc
35
+ combine_procs_with_and(*matchers.map(&:to_proc))
36
+ end
37
+
38
+ # Returns the list of matchers for each key-value pair in the condition.
39
+ #
40
+ # For each pair:
41
+ # - If the key is a registered operator, uses the corresponding matcher
42
+ # - Otherwise, wraps the value in a FieldMatcher for field path matching
43
+ #
44
+ # @return [Array<AbstractMatcher>] List of matchers for each condition
45
+ define_instance_cache_method(:matchers) do
46
+ @condition.map do |key, value|
47
+ if (matcher_class = Matchers.lookup(key))
48
+ matcher_class.build(value, context: @context)
49
+ else
50
+ FieldMatcher.build(key, value, context: @context)
51
+ end
52
+ end.sort_by(&:priority)
53
+ end
54
+
55
+ def check_validity!
56
+ return super if @condition.is_a?(Hash)
57
+
58
+ raise TypeError, 'condition needs a Hash.'
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # InMatcher implements the `$in` operator.
6
+ #
7
+ # It checks whether the record matches any value in the condition array.
8
+ # If the record is an array, it succeeds if any item overlaps with the condition.
9
+ # If the record is a single value (including `nil`), it matches if it is included in the condition.
10
+ #
11
+ # @example Match single value
12
+ # matcher = InMatcher.build([1, 2, 3])
13
+ # matcher.match?(2) #=> true
14
+ # matcher.match?(5) #=> false
15
+ #
16
+ # @example Match nil
17
+ # matcher = InMatcher.build([nil])
18
+ # matcher.match?(nil) #=> true
19
+ #
20
+ # @example Match with array
21
+ # matcher = InMatcher.build([2, 4])
22
+ # matcher.match?([1, 2, 3]) #=> true
23
+ # matcher.match?([5, 6]) #=> false
24
+ #
25
+ # @see AbstractMatcher
26
+ class InMatcher < AbstractMatcher
27
+ def self.build(condition, *args)
28
+ return super unless condition.is_a?(Range)
29
+
30
+ end_op = condition.exclude_end? ? '$lt' : '$lte'
31
+ head, tail = [condition.first, condition.last].sort
32
+ AndMatcher.build([{ '$gte' => head }, { end_op => tail }], *args)
33
+ end
34
+
35
+ # Creates a raw Proc that performs the in-matching operation.
36
+ # The Proc checks if any element of the record is in the condition array.
37
+ #
38
+ # @return [Proc] a Proc that performs the in-matching operation
39
+ def raw_proc
40
+ condition = Set.new(@condition)
41
+
42
+ Proc.new do |record|
43
+ if record.is_a?(Array)
44
+ is_present?(condition & record)
45
+ else
46
+ condition.include?(record)
47
+ end
48
+ end
49
+ end
50
+
51
+ def priority
52
+ 1 + Math.log(@condition.size + 1, 1.5)
53
+ end
54
+
55
+ # Ensures the condition is an array or range.
56
+ #
57
+ # @raise [TypeError] if condition is not an array nor a range
58
+ # @return [void]
59
+ def check_validity!
60
+ return if @condition.is_a?(Array)
61
+
62
+ raise TypeError, '$in needs an array or range'
63
+ end
64
+ end
65
+
66
+ register(:in, '$in', InMatcher)
67
+ end
68
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # LiteralMatcher handles direct value comparison with special array handling.
6
+ #
7
+ # This matcher is used when a condition is a literal value (not an operator).
8
+ # It handles both direct equality comparison and array-record scenarios.
9
+ #
10
+ # For array records:
11
+ # - Uses ArrayRecordMatcher to check if any element matches
12
+ # For non-array records:
13
+ # - Uses appropriate matcher based on condition type (Hash, Regexp, nil, etc.)
14
+ #
15
+ # @example Basic equality matching
16
+ # matcher = LiteralMatcher.build(42)
17
+ # matcher.match?(42) #=> true
18
+ # matcher.match?([42, 43]) #=> true (array contains 42)
19
+ #
20
+ # @example Regexp matching
21
+ # matcher = LiteralMatcher.build(/foo/)
22
+ # matcher.match?("foo") #=> true
23
+ # matcher.match?(["foobar"]) #=> true
24
+ #
25
+ # @example Hash condition matching
26
+ # matcher = LiteralMatcher.build({ '$gt' => 10 })
27
+ # matcher.match?(15) #=> true
28
+ # matcher.match?([5, 15]) #=> true
29
+ #
30
+ # @see AbstractMatcher
31
+ # @see ArrayRecordMatcher
32
+ class LiteralMatcher < AbstractMatcher
33
+ # Creates a raw Proc that performs the literal matching operation.
34
+ # The Proc handles both array and non-array records appropriately.
35
+ #
36
+ # @return [Proc] a Proc that performs the literal matching operation
37
+ def raw_proc
38
+ array_record_proc = nil
39
+ dispatched_proc = dispatched_matcher.to_proc
40
+
41
+ Proc.new do |record|
42
+ if record.is_a?(Array)
43
+ array_record_proc ||= array_record_matcher.to_proc
44
+ array_record_proc.call(record)
45
+ else
46
+ dispatched_proc.call(record)
47
+ end
48
+ end
49
+ end
50
+
51
+ def priority
52
+ 1 + dispatched_matcher.priority
53
+ end
54
+
55
+ # Selects and returns the appropriate matcher instance for a given literal condition.
56
+ #
57
+ # This method analyzes the type of the raw condition (e.g., Hash, Regexp, nil)
58
+ # and returns a dedicated matcher instance accordingly:
59
+ #
60
+ # - Hash → dispatches to `HashConditionMatcher`
61
+ # - Regexp → dispatches to `RegexMatcher`
62
+ # - nil → dispatches to an `OrMatcher` that emulates MongoDB's `{ field: nil }` behavior
63
+ #
64
+ # For all other literal types, this method returns `EqMatcher`, and fallback equality matching will be used.
65
+ #
66
+ # This matcher is cached after the first invocation using `define_instance_cache_method`
67
+ # to avoid unnecessary re-instantiation.
68
+ #
69
+ # @see Mongory::Matchers::HashConditionMatcher
70
+ # @see Mongory::Matchers::RegexMatcher
71
+ # @see Mongory::Matchers::OrMatcher
72
+ # @see Mongory::Matchers::EqMatcher
73
+ # @return [AbstractMatcher] the matcher used for non-array literal values
74
+ # @!method dispatched_matcher
75
+ define_matcher(:dispatched) do
76
+ case @condition
77
+ when Hash
78
+ HashConditionMatcher.build(@condition, context: @context)
79
+ when Regexp
80
+ RegexMatcher.build(@condition, context: @context)
81
+ when nil
82
+ OrMatcher.build([
83
+ { '$exists' => false },
84
+ { '$eq' => nil }
85
+ ], context: @context)
86
+ else
87
+ EqMatcher.build(@condition, context: @context)
88
+ end
89
+ end
90
+
91
+ # Lazily defines the collection matcher for array records.
92
+ #
93
+ # @see ArrayRecordMatcher
94
+ # @return [ArrayRecordMatcher] the matcher used to match array-type records
95
+ # @!method array_record_matcher
96
+ define_matcher(:array_record) do
97
+ ArrayRecordMatcher.build(@condition, context: @context)
98
+ end
99
+
100
+ # Validates the nested condition matcher, if applicable.
101
+ #
102
+ # @return [void]
103
+ def check_validity!
104
+ dispatched_matcher.check_validity!
105
+ end
106
+
107
+ # Outputs the matcher tree by selecting either collection or condition matcher.
108
+ # Delegates `render_tree` to whichever submatcher was active.
109
+ #
110
+ # @param prefix [String] the prefix string for tree rendering
111
+ # @param is_last [Boolean] whether this is the last node in the tree
112
+ # @return [void]
113
+ def render_tree(prefix = '', is_last: true)
114
+ super
115
+
116
+ target_matcher = @array_record_matcher || dispatched_matcher
117
+ target_matcher.render_tree("#{prefix}#{is_last ? ' ' : '│ '}", is_last: true)
118
+ end
119
+ end
120
+ end
121
+ end