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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +84 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +246 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +517 -0
- data/Rakefile +12 -0
- data/examples/README.md +41 -0
- data/examples/benchmark.rb +44 -0
- data/lib/generators/mongory/install/install_generator.rb +42 -0
- data/lib/generators/mongory/install/templates/initializer.rb.erb +83 -0
- data/lib/generators/mongory/matcher/matcher_generator.rb +56 -0
- data/lib/generators/mongory/matcher/templates/matcher.rb.erb +92 -0
- data/lib/generators/mongory/matcher/templates/matcher_spec.rb.erb +17 -0
- data/lib/mongory/converters/abstract_converter.rb +122 -0
- data/lib/mongory/converters/condition_converter.rb +74 -0
- data/lib/mongory/converters/data_converter.rb +26 -0
- data/lib/mongory/converters/key_converter.rb +63 -0
- data/lib/mongory/converters/value_converter.rb +47 -0
- data/lib/mongory/converters.rb +7 -0
- data/lib/mongory/matchers/README.md +57 -0
- data/lib/mongory/matchers/abstract_matcher.rb +153 -0
- data/lib/mongory/matchers/abstract_multi_matcher.rb +109 -0
- data/lib/mongory/matchers/abstract_operator_matcher.rb +46 -0
- data/lib/mongory/matchers/and_matcher.rb +65 -0
- data/lib/mongory/matchers/array_record_matcher.rb +88 -0
- data/lib/mongory/matchers/elem_match_matcher.rb +47 -0
- data/lib/mongory/matchers/eq_matcher.rb +37 -0
- data/lib/mongory/matchers/every_matcher.rb +41 -0
- data/lib/mongory/matchers/exists_matcher.rb +48 -0
- data/lib/mongory/matchers/field_matcher.rb +123 -0
- data/lib/mongory/matchers/gt_matcher.rb +29 -0
- data/lib/mongory/matchers/gte_matcher.rb +29 -0
- data/lib/mongory/matchers/hash_condition_matcher.rb +55 -0
- data/lib/mongory/matchers/in_matcher.rb +52 -0
- data/lib/mongory/matchers/literal_matcher.rb +123 -0
- data/lib/mongory/matchers/lt_matcher.rb +29 -0
- data/lib/mongory/matchers/lte_matcher.rb +29 -0
- data/lib/mongory/matchers/ne_matcher.rb +29 -0
- data/lib/mongory/matchers/nin_matcher.rb +51 -0
- data/lib/mongory/matchers/not_matcher.rb +32 -0
- data/lib/mongory/matchers/or_matcher.rb +60 -0
- data/lib/mongory/matchers/present_matcher.rb +52 -0
- data/lib/mongory/matchers/regex_matcher.rb +61 -0
- data/lib/mongory/matchers.rb +176 -0
- data/lib/mongory/mongoid.rb +19 -0
- data/lib/mongory/query_builder.rb +187 -0
- data/lib/mongory/query_matcher.rb +66 -0
- data/lib/mongory/query_operator.rb +28 -0
- data/lib/mongory/rails.rb +15 -0
- data/lib/mongory/utils/debugger.rb +123 -0
- data/lib/mongory/utils/rails_patch.rb +22 -0
- data/lib/mongory/utils/singleton_builder.rb +31 -0
- data/lib/mongory/utils.rb +75 -0
- data/lib/mongory/version.rb +5 -0
- data/lib/mongory-rb.rb +3 -0
- data/lib/mongory.rb +116 -0
- data/mongory.gemspec +40 -0
- data/sig/mongory.rbs +4 -0
- 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, ®istry.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
|
+
```
|