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,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mongory initializer
4
+ #
5
+ # Mongory.configure do ... will lock all configuration after execution.
6
+ # After the block ends, further modification is forbidden for safety.
7
+ # It is recommended to register all data/key/value converters within this block.
8
+ #
9
+ # === Converter Notes ===
10
+ #
11
+ # Data converter:
12
+ # - Responsible for normalizing data records (e.g., model instances) into Hashes with string keys.
13
+ # - Expected output should be either a Hash with string keys or a primitive type (Array, String, Number, etc.).
14
+ # - Registered method or block should take no arguments.
15
+ #
16
+ # Key converter:
17
+ # - Responsible for transforming condition keys (e.g., :age.gt) into query fragments.
18
+ # - Output must be a string-keyed Hash.
19
+ # - Registered method or block should accept one parameter (the condition value), and return a key-value pair.
20
+ #
21
+ # Value converter:
22
+ # - Responsible for transforming condition values before matching.
23
+ # - Registered method or block should take no arguments.
24
+ #
25
+ # All converters may support recursive conversion if needed.
26
+
27
+ Mongory.configure do |mc|
28
+ # Here to let you use query snippet like `:age.gt => 18`
29
+ # Or just disable it, and use `"age.$gt" => 18` instead.
30
+ # NOTE: This method will NOT override existing Symbol methods like :gt or :in.
31
+ # It only defines missing ones, and is safe to use with Mongoid or other libraries.
32
+ mc.enable_symbol_snippets!
33
+
34
+ # Here to let you use `some_collection.mongory.where(...)` to build mongory query filter.
35
+ # Or disable it, and use `Mongory.build_query(some_collection).where(...)` instead.
36
+ mc.register(Array)
37
+ <% if @use_ar -%>
38
+ mc.register(ActiveRecord::Relation)
39
+ <% end -%>
40
+ <% if @use_mongoid -%>
41
+ mc.register(Mongoid::Criteria)
42
+ <% end -%>
43
+ <% if @use_sequel -%>
44
+ mc.register(Sequel::Dataset)
45
+ <% end -%>
46
+
47
+ mc.data_converter.configure do |dc|
48
+ # Here to let you customize how to normalizing your ORM object or custom class into comparable format.
49
+ # Example:
50
+ # dc.register(ActiveRecord::Base, :attributes)
51
+ # NOTE: If your model overrides `attributes`, or includes virtual fields,
52
+ # consider defining a custom method to ensure consistency.
53
+ <% if @use_ar -%>
54
+ dc.register(ActiveRecord::Base, :attributes)
55
+ <% end -%>
56
+ <% if @use_mongoid -%>
57
+ dc.register(Mongoid::Document, :as_document)
58
+ dc.register(BSON::ObjectId, :to_s)
59
+ <% end -%>
60
+ <% if @use_sequel -%>
61
+ dc.register(Sequel::Model) { values.transform_keys(&:to_s) }
62
+ <% end -%>
63
+ end
64
+
65
+ mc.condition_converter.configure do |cc|
66
+ cc.key_converter.configure do |kc|
67
+ # You may register condition key converters here.
68
+ # Example:
69
+ # kc.register(MyKeyObject, :trans_to_string_key_pair)
70
+ # kc.register(MyEnumKey, ->(val) { { "my_enum" => val.to_s } })
71
+ end
72
+
73
+ cc.value_converter.configure do |vc|
74
+ # You may register condition value converters here.
75
+ # Example:
76
+ # vc.register(MyCollectionType) { map { |v| vc.convert(v) } }
77
+ # vc.register(MyWrapperType) { unwrap_and_return_value }
78
+ <% if @use_mongoid -%>
79
+ vc.register(BSON::ObjectId, :to_s)
80
+ <% end -%>
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require_relative '../install/install_generator'
5
+
6
+ module Mongory
7
+ module Generators
8
+ # Generates a new Mongory matcher.
9
+ #
10
+ # @example
11
+ # rails g mongory:matcher class_in
12
+ # # Creates:
13
+ # # lib/mongory/matchers/class_in_matcher.rb
14
+ # # spec/mongory/matchers/class_in_matcher_spec.rb
15
+ # # config/initializers/mongory.rb (if not exists)
16
+ #
17
+ # @see Mongory::Matchers::AbstractOperatorMatcher
18
+ class MatcherGenerator < Rails::Generators::NamedBase
19
+ source_root File.expand_path('templates', __dir__)
20
+
21
+ # Generates the matcher files and updates the initializer.
22
+ #
23
+ # @return [void]
24
+ def create_matcher
25
+ @matcher_name = "#{class_name}Matcher"
26
+ @operator_name = name.underscore
27
+ @mongo_operator = "$#{name.camelize(:lower)}"
28
+
29
+ template 'matcher.rb.erb', "lib/mongory/matchers/#{file_name}_matcher.rb"
30
+ template 'matcher_spec.rb.erb', "spec/mongory/matchers/#{file_name}_matcher_spec.rb"
31
+ end
32
+
33
+ # Updates or creates the Mongory initializer.
34
+ #
35
+ # @return [void]
36
+ def update_initializer
37
+ initializer_path = 'config/initializers/mongory.rb'
38
+ inject_line = "require \"#\{Rails.root\}/lib/mongory/matchers/#{file_name}_matcher\""
39
+ front_line = '# frozen_string_literal: true'
40
+
41
+ Mongory::Generators::InstallGenerator.start unless File.exist?(initializer_path)
42
+ content = File.read(initializer_path)
43
+ return if content.include?(inject_line)
44
+
45
+ required_file_lines = content.scan(/.*require\s+["'].*_matcher["'].*/)
46
+ if required_file_lines.empty?
47
+ inject_line = "\n#{inject_line}"
48
+ else
49
+ front_line = required_file_lines.last
50
+ end
51
+
52
+ inject_into_file initializer_path, "#{inject_line}\n", after: "#{front_line}\n"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ # <%= @matcher_name %> implements the Mongo-style `<%= @mongo_operator %>` operator.
4
+ #
5
+ # Instance Variables:
6
+ # - @condition: The raw condition value passed to this matcher during initialization.
7
+ # This value is used for matching logic and should be validated in check_validity!.
8
+ #
9
+ # Condition Examples:
10
+ # # When query is: { field: { "<%= @mongo_operator %>" => your_condition } }
11
+ # # @condition will be: your_condition
12
+ #
13
+ # # When query is: { field: { "<%= @mongo_operator %>" => { key: value } } }
14
+ # # @condition will be: { key: value }
15
+ #
16
+ # # When query is: { field: { "<%= @mongo_operator %>" => [value1, value2] } }
17
+ # # @condition will be: [value1, value2]
18
+ #
19
+ # Matcher Tree Integration:
20
+ # When this matcher is created as part of a matcher tree:
21
+ # 1. It receives its @condition during initialization
22
+ # 2. The @condition is validated via check_validity!
23
+ # 3. The match method is called with normalized values
24
+ # 4. The matcher can use normalize(record) to handle KEY_NOT_FOUND values
25
+ #
26
+ # This matcher typically serves as a leaf node in the matcher tree,
27
+ # responsible for evaluating a single condition against a value.
28
+ # It may be combined with other matchers through logical operators
29
+ # like $and, $or, or $not to form complex conditions.
30
+ #
31
+ # Usage Examples:
32
+ # # Basic usage
33
+ # matcher = <%= @matcher_name %>.build(condition)
34
+ # matcher.match?(value) #=> true/false
35
+ #
36
+ # # Usage in queries:
37
+ # # 1. Field-specific condition (MongoDB style)
38
+ # records.mongory.where(field: { "<%= @mongo_operator %>" => your_condition })
39
+ #
40
+ # # 2. Field-specific condition (Ruby DSL style)
41
+ # records.mongory.where(:field.<%= @operator_name %> => your_condition)
42
+ #
43
+ # # 3. Global condition (applies to all fields)
44
+ # records.mongory.where("<%= @mongo_operator %>" => your_condition)
45
+ #
46
+ # Implementation Notes:
47
+ # 1. The match method should implement the specific operator logic
48
+ # 2. Use normalize(subject) to handle KEY_NOT_FOUND values consistently
49
+ # 3. The condition is available via @condition
50
+ # 4. check_validity! should validate the format of @condition
51
+ #
52
+ # Interface Design:
53
+ # This matcher provides two levels of matching interface:
54
+ #
55
+ # 1. Public Interface (match?):
56
+ # - Provides error handling
57
+ # - Ensures safe matching process
58
+ # - Suitable for external calls
59
+ # - Can be tracked by Mongory.debugger
60
+ # - Used for internal calls and debugging
61
+ #
62
+ # 2. Internal Interface (match):
63
+ # - Implements the actual matching logic
64
+ #
65
+ # Debugging Support:
66
+ # The match method can be tracked by Mongory.debugger to:
67
+ # - Visualize the matching process
68
+ # - Diagnose matching issues
69
+ # - Provide detailed debugging information
70
+ #
71
+ # @see Mongory::Matchers::AbstractMatcher
72
+ class <%= @matcher_name %> < Mongory::Matchers::AbstractMatcher
73
+ # Matches the subject against the condition.
74
+ # This is the internal interface that implements the actual matching logic.
75
+ # It can be tracked by Mongory.debugger for debugging purposes.
76
+ #
77
+ # @param subject [Object] the value to be tested
78
+ # @return [Boolean] whether the value matches
79
+ def match(subject)
80
+ # Implement your matching logic here
81
+ end
82
+
83
+ # Validates the condition value.
84
+ #
85
+ # @raise [TypeError] if condition is invalid
86
+ # @return [void]
87
+ def check_validity!
88
+ # Implement your validation logic here
89
+ end
90
+ end
91
+
92
+ Mongory::Matchers.register(:<%= @operator_name %>, '<%= @mongo_operator %>', <%= @matcher_name %>)
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe <%= @matcher_name %> do
6
+ describe '#match?' do
7
+ it 'matches the condition' do
8
+ # Implement your test here
9
+ end
10
+ end
11
+
12
+ describe '#check_validity!' do
13
+ it 'validates the condition' do
14
+ # Implement your test here
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Converters
5
+ # AbstractConverter provides a flexible DSL-style mechanism
6
+ # for dynamically converting objects based on their class.
7
+ #
8
+ # It allows you to register conversion rules for specific classes,
9
+ # with optional fallback behavior.
10
+ #
11
+ # @example Basic usage
12
+ # converter = AbstractConverter.instance
13
+ # converter.register(String) { |v| v.upcase }
14
+ # converter.convert("hello") #=> "HELLO"
15
+ #
16
+ class AbstractConverter < Utils::SingletonBuilder
17
+ include Singleton
18
+
19
+ # @private
20
+ # A registry entry storing a conversion rule.
21
+ #
22
+ # @!attribute klass
23
+ # @return [Class] the class this rule applies to
24
+ # @!attribute exec
25
+ # @return [Proc] the block used to convert the object
26
+ Registry = Struct.new(:klass, :exec)
27
+
28
+ # @private
29
+ # A sentinel value used to indicate absence of a secondary argument.
30
+ NOTHING = Utils::SingletonBuilder.new('NOTHING')
31
+
32
+ # Initializes the builder with a label and optional configuration block.
33
+ def initialize
34
+ super(self.class.to_s)
35
+ @registries = []
36
+ @fallback = Proc.new { |*| self }
37
+ execute_once_only!(:default_registrations)
38
+ end
39
+
40
+ # Applies the registered conversion to the given target object.
41
+ #
42
+ # @param target [Object] the object to convert
43
+ # @param other [Object] optional secondary value
44
+ # @return [Object] converted result
45
+ def convert(target, other = NOTHING)
46
+ @registries.each do |registry|
47
+ next unless target.is_a?(registry.klass)
48
+
49
+ return exec_convert(target, other, &registry.exec)
50
+ end
51
+
52
+ exec_convert(target, other, &@fallback)
53
+ end
54
+
55
+ # Internal dispatch logic to apply a matching converter.
56
+ #
57
+ # @param target [Object] the object to match
58
+ # @param other [Object] optional extra data
59
+ # @yield fallback block if no converter is found
60
+ # @return [Object]
61
+ def exec_convert(target, other, &block)
62
+ if other == NOTHING
63
+ target.instance_exec(&block)
64
+ else
65
+ target.instance_exec(other, &block)
66
+ end
67
+ end
68
+
69
+ # Opens a configuration block to register more converters.
70
+ #
71
+ # @yield DSL block to configure more rules
72
+ # @return [void]
73
+ def configure
74
+ yield self
75
+ freeze
76
+ end
77
+
78
+ # Freezes all internal registries.
79
+ #
80
+ # @return [void]
81
+ def freeze
82
+ super
83
+ @registries.freeze
84
+ end
85
+
86
+ # Registers a conversion rule for a given class.
87
+ #
88
+ # @param klass [Class, Module] the target class
89
+ # @param converter [Symbol, nil] method name to call as a conversion
90
+ # @yield [*args] block that performs the conversion
91
+ # @return [void]
92
+ # @raise [RuntimeError] if input is invalid
93
+ def register(klass, converter = nil, &block)
94
+ raise 'converter or block is required.' if [converter, block].compact.empty?
95
+ raise 'A class or module is reuqired.' unless klass.is_a?(Module)
96
+
97
+ if converter.is_a?(Symbol)
98
+ register(klass) { |*args, &bl| send(converter, *args, &bl) }
99
+ elsif block.is_a?(Proc)
100
+ @registries.unshift(Registry.new(klass, block))
101
+ else
102
+ raise 'Support Symbol and block only.'
103
+ end
104
+ end
105
+
106
+ # Executes the given method only once by undefining it after execution.
107
+ #
108
+ # @param method_sym [Symbol] method name to execute once
109
+ # @return [void]
110
+ def execute_once_only!(method_sym)
111
+ send(method_sym)
112
+ singleton_class.undef_method(method_sym)
113
+ end
114
+
115
+ # Defines default class-to-converter registrations.
116
+ # Should be overridden by subclasses.
117
+ #
118
+ # @return [void]
119
+ def default_registrations; end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Converters
5
+ # ConditionConverter transforms a flat condition hash into nested hash form.
6
+ # This is used internally by ValueConverter to normalize condition structures
7
+ # like { "foo.bar" => 1, "foo.baz" => 2 } into nested Mongo-style conditions.
8
+ # Used by QueryMatcher to normalize condition input for internal matching.
9
+ #
10
+ # Combines key transformation (via KeyConverter) and
11
+ # value normalization (via ValueConverter), and merges overlapping keys.
12
+ #
13
+ # @example Convert condition hash
14
+ # ConditionConverter.instance.convert({ "a.b" => 1, "a.c" => 2 })
15
+ # # => { "a" => { "b" => 1, "c" => 2 } }
16
+ #
17
+ class ConditionConverter < AbstractConverter
18
+ # Converts a flat condition hash into a nested structure.
19
+ #
20
+ # @param condition [Hash]
21
+ # @return [Hash] the transformed nested condition
22
+ def convert(condition)
23
+ result = {}
24
+ condition.each_pair do |k, v|
25
+ converted_value = value_converter.convert(v)
26
+ converted_pair = key_converter.convert(k, converted_value)
27
+ result.merge!(converted_pair, &deep_merge_block)
28
+ end
29
+ result
30
+ end
31
+
32
+ # Provides a block that merges values for overlapping keys in a deep way.
33
+ #
34
+ # @return [Proc]
35
+ def deep_merge_block
36
+ @deep_merge_block ||= Proc.new do |_, a, b|
37
+ if a.is_a?(Hash) && b.is_a?(Hash)
38
+ a.merge(b, &deep_merge_block)
39
+ else
40
+ b
41
+ end
42
+ end
43
+ end
44
+
45
+ # @note Singleton instance, not configurable after initialization
46
+ # Returns the key converter used to transform condition keys.
47
+ #
48
+ # @return [AbstractConverter]
49
+ def key_converter
50
+ KeyConverter.instance
51
+ end
52
+
53
+ # @note Singleton instance, not configurable after initialization
54
+ # Returns the value converter used to transform condition values.
55
+ #
56
+ # @return [AbstractConverter]
57
+ def value_converter
58
+ ValueConverter.instance
59
+ end
60
+
61
+ # Freezes internal converters to prevent further modification.
62
+ #
63
+ # @return [void]
64
+ def freeze
65
+ deep_merge_block
66
+ super
67
+ key_converter.freeze
68
+ value_converter.freeze
69
+ end
70
+
71
+ undef_method :register, :exec_convert
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Converters
5
+ # DataConverter handles automatic transformation of raw query values.
6
+ # This class inherits from AbstractConverter and provides predefined conversions for
7
+ # common primitive types like Symbol, Date, Time, etc.
8
+ # - Symbols and Date objects are converted to string
9
+ # - Time and DateTime objects are ISO8601-encoded
10
+ # - Strings and Integers are passed through as-is
11
+ #
12
+ # @example Convert a symbol
13
+ # DataConverter.instance.convert(:status) #=> "status"
14
+ #
15
+ class DataConverter < AbstractConverter
16
+ def default_registrations
17
+ register(Symbol, :to_s)
18
+ register(Date, :to_s)
19
+ register(Time, :iso8601)
20
+ register(DateTime, :iso8601)
21
+ register(String, :itself)
22
+ register(Integer, :itself)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Converters
5
+ # KeyConverter handles transformation of field keys in query conditions.
6
+ # It normalizes symbol keys into string paths, splits dotted keys,
7
+ # and delegates to the appropriate converter logic.
8
+ #
9
+ # This class inherits from AbstractConverter and registers rules for
10
+ # strings, symbols, and includes a fallback handler.
11
+ # Used by ConditionConverter to build query structures from flat input.
12
+ #
13
+ # - `"a.b.c" => v` becomes `{ "a" => { "b" => { "c" => v } } }`
14
+ # - Symbols are stringified and delegated to String logic
15
+ # - QueryOperator dispatches to internal DSL hook
16
+ #
17
+ # @example Convert a dotted string key
18
+ # KeyConverter.instance.convert("user.name") #=> { "user" => { "name" => value } }
19
+ #
20
+ class KeyConverter < AbstractConverter
21
+ # fallback if key type is unknown — returns { self => value }
22
+ def initialize
23
+ super
24
+ @fallback = ->(x) { { self => x } }
25
+ end
26
+
27
+ def default_registrations
28
+ convert_string_key = method(:convert_string_key)
29
+ register(String) do |value|
30
+ convert_string_key.call(self, value)
31
+ end
32
+
33
+ # - `:"a.b.c" => v` becomes `{ "a" => { "b" => { "c" => v } } }`
34
+ register(Symbol) do |other|
35
+ convert_string_key.call(to_s, other)
36
+ end
37
+
38
+ # - `:"a.b.c".present => true` becomes `{ "a" => { "b" => { "c" => { "$present" => true } } } }`
39
+ register(QueryOperator, :__expr_part__)
40
+ end
41
+
42
+ # Converts a dotted string key into nested hash form.
43
+ #
44
+ # @param key [String] the dotted key string, e.g. "a.b.c"
45
+ # @param value [Object] the value to assign at the deepest level
46
+ # @return [Hash] nested hash structure
47
+ def convert_string_key(key, value)
48
+ ret = {}
49
+ *sub_keys, last_key = key.split(/(?<!\\)\./)
50
+ last_hash = sub_keys.reduce(ret) do |res, sub_key|
51
+ next_res = res[normalize_key(sub_key)] = {}
52
+ next_res
53
+ end
54
+ last_hash[normalize_key(last_key)] = value
55
+ ret
56
+ end
57
+
58
+ def normalize_key(key)
59
+ key.gsub(/\\\./, '.')
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongory
4
+ module Converters
5
+ # ValueConverter transforms query values into a standardized form.
6
+ # It handles arrays, hashes, regex, and basic types, and delegates
7
+ # fallback logic to DataConverter.
8
+ # Used by ConditionConverter to prepare values in nested queries.
9
+ #
10
+ # - Arrays are recursively converted
11
+ # - Hashes are interpreted as nested conditions
12
+ # - Regex becomes a Mongo-style `$regex` hash
13
+ # - Strings and Integers are passed through
14
+ # - Everything else falls back to DataConverter
15
+ #
16
+ # @example Convert a regex
17
+ # ValueConverter.instance.convert(/foo/) #=> { "$regex" => "foo" }
18
+ #
19
+ class ValueConverter < AbstractConverter
20
+ # Sets a fallback using DataConverter for unsupported types.
21
+ #
22
+ # @return [void]
23
+ def initialize
24
+ super
25
+ @fallback = Proc.new do
26
+ Mongory.data_converter.convert(self)
27
+ end
28
+ end
29
+
30
+ def default_registrations
31
+ v_convert = method(:convert)
32
+ register(Array) do
33
+ map { |x| v_convert.call(x) }
34
+ end
35
+
36
+ # - Hashes are interpreted as nested condition trees
37
+ # using ConditionConverter
38
+ register(Hash) do
39
+ Mongory.condition_converter.convert(self)
40
+ end
41
+
42
+ register(String, :itself)
43
+ register(Integer, :itself)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'converters/abstract_converter'
4
+ require_relative 'converters/condition_converter'
5
+ require_relative 'converters/data_converter'
6
+ require_relative 'converters/key_converter'
7
+ require_relative 'converters/value_converter'
@@ -0,0 +1,57 @@
1
+ # Matcher classes inheritance diagram
2
+
3
+ ```mermaid
4
+ ---
5
+ config:
6
+ theme: neutral
7
+ look: classic
8
+ ---
9
+
10
+ graph TD
11
+ %% style blocks
12
+ classDef abstract fill:#ddd,stroke:#333,stroke-width:1px,color:#000;
13
+ classDef main fill:#cce5ff,stroke:#339,stroke-width:1px;
14
+ classDef multi fill:#d4edda,stroke:#282,stroke-width:1px;
15
+ classDef operator fill:#fff3cd,stroke:#aa8800,stroke-width:1px;
16
+ classDef leaf fill:#f8f9fa,stroke:#999,stroke-width:1px;
17
+
18
+ %% Abstract base classes
19
+ A[AbstractMatcher]
20
+ C[AbstractMultiMatcher]
21
+ D[AbstractOperatorMatcher]
22
+
23
+ subgraph MultipleConditions
24
+ F[HashConditionMatcher]
25
+ I[AndMatcher]
26
+ J[OrMatcher]
27
+ W[ArrayRecordMatcher]
28
+ end
29
+
30
+ subgraph SimpleCompare
31
+ E[EqMatcher]
32
+ K[RegexMatcher]
33
+ L[PresentMatcher]
34
+ M[ExistsMatcher]
35
+ O[NeMatcher]
36
+ Q[GtMatcher]
37
+ R[GteMatcher]
38
+ S[LtMatcher]
39
+ T[LteMatcher]
40
+ end
41
+
42
+ A --> B[LiteralMatcher]
43
+ A --> U[InMatcher]
44
+ A --> V[NinMatcher]
45
+ A --> C --> MultipleConditions
46
+ A --> D --> SimpleCompare
47
+ B --> G[FieldMatcher]
48
+ B --> H[ElemMatchMatcher]
49
+ B --> N[NotMatcher]
50
+
51
+ %% Apply classes
52
+ class A,C,D abstract;
53
+ class B main;
54
+ class F,I,J,W multi;
55
+ class E,K,L,M,O,Q,R,S,T operator;
56
+ class G,U,V,H,N leaf;
57
+ ```