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.

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,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # LtMatcher implements the `$lt` (less than) operator.
6
+ #
7
+ # It returns true if the record is strictly less than the condition value.
8
+ #
9
+ # This matcher inherits from AbstractMatcher and uses the `<` operator.
10
+ #
11
+ # @example
12
+ # matcher = LtMatcher.build(10)
13
+ # matcher.match?(9) #=> true
14
+ # matcher.match?(10) #=> false
15
+ # matcher.match?(11) #=> false
16
+ #
17
+ # @see AbstractMatcher
18
+ class LtMatcher < AbstractMatcher
19
+ # Creates a raw Proc that performs the less-than comparison.
20
+ # The Proc uses the `<` operator to compare values.
21
+ #
22
+ # @return [Proc] A proc that performs less-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(:lt, '$lt', LtMatcher)
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # LteMatcher implements the `$lte` (less than or equal to) operator.
6
+ #
7
+ # It returns true if the record is less than or equal to the condition value.
8
+ #
9
+ # This matcher inherits from AbstractMatcher and uses the `<=` operator.
10
+ #
11
+ # @example
12
+ # matcher = LteMatcher.build(10)
13
+ # matcher.match?(9) #=> true
14
+ # matcher.match?(10) #=> true
15
+ # matcher.match?(11) #=> false
16
+ #
17
+ # @see AbstractMatcher
18
+ class LteMatcher < AbstractMatcher
19
+ # Creates a raw Proc that performs the less-than-or-equal comparison.
20
+ # The Proc uses the `<=` operator to compare values.
21
+ #
22
+ # @return [Proc] A proc that performs less-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(:lte, '$lte', LteMatcher)
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # NeMatcher implements the `$ne` (not equal) operator.
6
+ #
7
+ # It returns true if the record is *not equal* to the condition.
8
+ #
9
+ # This matcher inherits its logic from AbstractMatcher
10
+ # and uses Ruby's `!=` operator for comparison.
11
+ #
12
+ # @example
13
+ # matcher = NeMatcher.build(42)
14
+ # matcher.match?(41) #=> true
15
+ # matcher.match?(42) #=> false
16
+ #
17
+ # @see AbstractMatcher
18
+ class NeMatcher < AbstractMatcher
19
+ # Creates a raw Proc that performs the not-equal comparison.
20
+ # The Proc uses the `!=` operator to compare values.
21
+ #
22
+ # @return [Proc] A proc that performs not-equal comparison
23
+ def raw_proc
24
+ condition = @condition
25
+
26
+ Proc.new do |record|
27
+ record != condition
28
+ end
29
+ end
30
+
31
+ def priority
32
+ 1
33
+ end
34
+ end
35
+
36
+ register(:ne, '$ne', NeMatcher)
37
+ end
38
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # NinMatcher implements the `$nin` (not in) operator.
6
+ #
7
+ # It succeeds only if the record does not match any value in the condition array.
8
+ # If the record is an array, it fails if any element overlaps with the condition.
9
+ # If the record is a single value (including `nil`), it fails if it is included in the condition.
10
+ #
11
+ # @example Match single value
12
+ # matcher = NinMatcher.build([1, 2, 3])
13
+ # matcher.match?(4) #=> true
14
+ # matcher.match?(2) #=> false
15
+ #
16
+ # @example Match nil
17
+ # matcher = NinMatcher.build([nil])
18
+ # matcher.match?(nil) #=> false
19
+ #
20
+ # @example Match with array
21
+ # matcher = NinMatcher.build([2, 4])
22
+ # matcher.match?([1, 3, 5]) #=> true
23
+ # matcher.match?([4, 5]) #=> false
24
+ #
25
+ # @see AbstractMatcher
26
+ class NinMatcher < AbstractMatcher
27
+ def self.build(condition, *args)
28
+ return super unless condition.is_a?(Range)
29
+
30
+ end_op = condition.exclude_end? ? '$gte' : '$gt'
31
+ head, tail = [condition.first, condition.last].sort
32
+ OrMatcher.build([{ '$lt' => head }, { end_op => tail }], *args)
33
+ end
34
+
35
+ # Creates a raw Proc that performs the not-in matching operation.
36
+ # The Proc checks if the record has no elements in common with the condition array.
37
+ #
38
+ # @return [Proc] A proc that performs not-in matching
39
+ def raw_proc
40
+ condition = Set.new(@condition)
41
+
42
+ Proc.new do |record|
43
+ if record.is_a?(Array)
44
+ is_blank?(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 a valid array or range.
56
+ #
57
+ # @raise [TypeError] if the 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, '$nin needs an array or range'
63
+ end
64
+ end
65
+
66
+ register(:nin, '$nin', NinMatcher)
67
+ end
68
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # NotMatcher implements the `$not` logical operator.
6
+ #
7
+ # It returns true if the wrapped matcher fails, effectively inverting the result.
8
+ #
9
+ # It delegates to LiteralMatcher and simply negates the outcome.
10
+ #
11
+ # This allows constructs like:
12
+ # { age: { :$not => { :$gte => 30 } } }
13
+ #
14
+ # @example
15
+ # matcher = NotMatcher.build({ :$gte => 10 })
16
+ # matcher.match?(5) #=> true
17
+ # matcher.match?(15) #=> false
18
+ #
19
+ # @see LiteralMatcher
20
+ class NotMatcher < LiteralMatcher
21
+ # Creates a raw Proc that performs the not-matching operation.
22
+ # The Proc inverts the result of the wrapped matcher.
23
+ #
24
+ # @return [Proc] A proc that performs not-matching
25
+ def raw_proc
26
+ super_proc = super
27
+
28
+ Proc.new do |record|
29
+ !super_proc.call(record)
30
+ end
31
+ end
32
+
33
+ def priority
34
+ 1 + super
35
+ end
36
+ end
37
+
38
+ register(:not, '$not', NotMatcher)
39
+ end
40
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # OrMatcher implements the `$or` logical operator.
6
+ #
7
+ # It evaluates an array of subconditions and returns true
8
+ # if *any one* of them matches. For empty conditions, it returns false
9
+ # (using FALSE_PROC).
10
+ #
11
+ # Each subcondition is handled by a HashConditionMatcher with conversion disabled,
12
+ # since the parent matcher already manages data conversion.
13
+ #
14
+ # This matcher inherits submatcher dispatch and evaluation logic
15
+ # from AbstractMultiMatcher.
16
+ #
17
+ # @example
18
+ # matcher = OrMatcher.build([
19
+ # { age: { :$lt => 18 } },
20
+ # { admin: true }
21
+ # ])
22
+ # matcher.match?(record) #=> true if either condition matches
23
+ #
24
+ # @example Empty conditions
25
+ # matcher = OrMatcher.build([])
26
+ # matcher.match?(record) #=> false (uses FALSE_PROC)
27
+ #
28
+ # @see AbstractMultiMatcher
29
+ class OrMatcher < AbstractMultiMatcher
30
+ enable_unwrap!
31
+
32
+ # Creates a raw Proc that performs the or-matching operation.
33
+ # The Proc combines all submatcher Procs and returns true if any match.
34
+ # For empty conditions, returns FALSE_PROC.
35
+ #
36
+ # @return [Proc] a Proc that performs the or-matching operation
37
+ def raw_proc
38
+ combine_procs_with_or(*matchers.map(&:to_proc))
39
+ end
40
+
41
+ # Builds an array of matchers from the subconditions.
42
+ # Each subcondition is wrapped in a HashConditionMatcher.
43
+ #
44
+ # @return [Array<AbstractMatcher>] array of submatchers
45
+ define_instance_cache_method(:matchers) do
46
+ @condition.map do |condition|
47
+ HashConditionMatcher.build(condition, context: @context)
48
+ end.sort_by(&:priority)
49
+ end
50
+
51
+ # Ensures the condition is an array of hashes.
52
+ #
53
+ # @raise [Mongory::TypeError] if not valid
54
+ # @return [void]
55
+ def check_validity!
56
+ raise TypeError, '$or needs an array' unless @condition.is_a?(Array)
57
+
58
+ @condition.each do |sub_condition|
59
+ raise TypeError, '$or needs an array of hash' unless sub_condition.is_a?(Hash)
60
+ end
61
+
62
+ super
63
+ end
64
+ end
65
+
66
+ register(:or, '$or', OrMatcher)
67
+ end
68
+ end
@@ -0,0 +1,55 @@
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 AbstractMatcher
24
+ class PresentMatcher < AbstractMatcher
25
+ # Creates a raw Proc that performs the presence check.
26
+ # The Proc checks if the record's presence matches the condition.
27
+ #
28
+ # @return [Proc] A proc that performs presence check
29
+ def raw_proc
30
+ condition = @condition
31
+
32
+ Proc.new do |record|
33
+ record = nil if record == KEY_NOT_FOUND
34
+ is_present?(record) == condition
35
+ end
36
+ end
37
+
38
+ def priority
39
+ 2
40
+ end
41
+
42
+ # Ensures that the condition value is a boolean.
43
+ #
44
+ # @raise [TypeError] if condition is not true or false
45
+ # @return [void]
46
+ def check_validity!
47
+ return if [true, false].include?(@condition)
48
+
49
+ raise TypeError, '$present needs a boolean'
50
+ end
51
+ end
52
+
53
+ register(:present, '$present', PresentMatcher)
54
+ end
55
+ end
@@ -0,0 +1,80 @@
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 Basic regex matching
16
+ # matcher = RegexMatcher.build(/^foo/)
17
+ # matcher.match?('foobar') #=> true
18
+ # matcher.match?('barfoo') #=> false
19
+ #
20
+ # @example Case-insensitive matching
21
+ # matcher = RegexMatcher.build(/admin/i)
22
+ # matcher.match?("ADMIN") #=> true
23
+ #
24
+ # @example String pattern
25
+ # matcher = RegexMatcher.build("^foo")
26
+ # matcher.match?("foobar") #=> true
27
+ #
28
+ # @example Non-string input
29
+ # matcher = RegexMatcher.build(/\d+/)
30
+ # matcher.match?(123) #=> false (not a string)
31
+ #
32
+ # @see AbstractMatcher
33
+ class RegexMatcher < AbstractMatcher
34
+ # Initializes the matcher with a regex pattern.
35
+ # Converts string patterns to Regexp objects.
36
+ #
37
+ # @param condition [String, Regexp] the regex pattern to match against
38
+ # @param context [Context] the query context
39
+ # @raise [TypeError] if condition is not a string or Regexp
40
+ def initialize(condition, context: Context.new)
41
+ super
42
+ @condition = Regexp.new(condition) if condition.is_a?(String)
43
+ end
44
+
45
+ # Creates a raw Proc that performs the regex matching operation.
46
+ # The Proc checks if the record is a string that matches the pattern.
47
+ # Returns false for non-string inputs or if the match fails.
48
+ #
49
+ # @return [Proc] a Proc that performs regex matching
50
+ def raw_proc
51
+ condition = @condition
52
+
53
+ Proc.new do |record|
54
+ next false unless record.is_a?(String)
55
+
56
+ record.match?(condition)
57
+ rescue StandardError
58
+ false
59
+ end
60
+ end
61
+
62
+ def priority
63
+ @condition.source.start_with?('^') ? 8 : 20
64
+ end
65
+
66
+ # Ensures the condition is a valid regex pattern (Regexp or String).
67
+ #
68
+ # @raise [TypeError] if condition is not a string or Regexp
69
+ # @return [void]
70
+ def check_validity!
71
+ return if @condition.is_a?(Regexp)
72
+ return if @condition.is_a?(String)
73
+
74
+ raise TypeError, '$regex needs a Regexp or string'
75
+ end
76
+ end
77
+
78
+ register(:regex, '$regex', RegexMatcher)
79
+ end
80
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Matchers
5
+ # Matcher for the `$size` operator.
6
+ #
7
+ # This matcher expects the input to be an array, and delegates the comparison
8
+ # to a literal matcher using the array's size as the value.
9
+ #
10
+ # For example, the condition `{ tags: { '$size' => 3 } }` will match any
11
+ # document where `tags` is an array of length 3.
12
+ #
13
+ # ### Supported compound usages:
14
+ #
15
+ # ```ruby
16
+ # Mongory.where(tags: { '$size' => 3 }) # exactly 3 elements
17
+ # Mongory.where(tags: { '$size' => { '$gt' => 1 } }) # more than 1
18
+ # Mongory.where(comments: { '$size' => { '$gt' => 1, '$lte' => 5 } }) # more than 1, up to 5 elements
19
+ # Mongory.where(tags: { '$size' => { '$in' => [1, 2, 3] } }) # 1, 2, or 3 elements
20
+ # ```
21
+ #
22
+ # @see LiteralMatcher
23
+ #
24
+ # @note Ruby's Symbol class already defines a `#size` method,
25
+ # that will return the size of the symbol object.
26
+ # So, this is the only operator that cannot be used with
27
+ # the symbol snippet syntax (e.g. `:tags.size`).
28
+ #
29
+ # Use string key syntax instead: `:"tags.$size" => ...`
30
+ class SizeMatcher < LiteralMatcher
31
+ # Creates a raw Proc that performs the size matching operation.
32
+ #
33
+ # The returned Proc checks if the input is an Array. If so, it calculates
34
+ # the array's size and passes it to the wrapped literal matcher Proc.
35
+ #
36
+ # @return [Proc] A proc that performs size-based matching
37
+ def raw_proc
38
+ super_proc = super
39
+
40
+ Proc.new do |record|
41
+ next false unless record.is_a?(Array)
42
+
43
+ super_proc.call(record.size)
44
+ end
45
+ end
46
+
47
+ def priority
48
+ 2 + super
49
+ end
50
+ end
51
+
52
+ register(:size, '$size', SizeMatcher)
53
+ end
54
+ 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/literal_matcher'
157
+ require_relative 'matchers/hash_condition_matcher'
158
+ require_relative 'matchers/and_matcher'
159
+ require_relative 'matchers/array_record_matcher'
160
+ require_relative 'matchers/elem_match_matcher'
161
+ require_relative 'matchers/every_matcher'
162
+ require_relative 'matchers/eq_matcher'
163
+ require_relative 'matchers/exists_matcher'
164
+ require_relative 'matchers/gt_matcher'
165
+ require_relative 'matchers/gte_matcher'
166
+ require_relative 'matchers/in_matcher'
167
+ require_relative 'matchers/field_matcher'
168
+ require_relative 'matchers/lt_matcher'
169
+ require_relative 'matchers/lte_matcher'
170
+ require_relative 'matchers/ne_matcher'
171
+ require_relative 'matchers/nin_matcher'
172
+ require_relative 'matchers/not_matcher'
173
+ require_relative 'matchers/or_matcher'
174
+ require_relative 'matchers/present_matcher'
175
+ require_relative 'matchers/regex_matcher'
176
+ require_relative 'matchers/size_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