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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +88 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +364 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +488 -0
- data/Rakefile +107 -0
- data/SUBMODULE_INTEGRATION.md +325 -0
- data/docs/advanced_usage.md +40 -0
- data/docs/clang_bridge.md +69 -0
- data/docs/field_names.md +30 -0
- data/docs/migration.md +30 -0
- data/docs/performance.md +61 -0
- data/examples/README.md +41 -0
- data/examples/benchmark-rails.rb +52 -0
- data/examples/benchmark.rb +184 -0
- data/ext/mongory_ext/extconf.rb +91 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/array.h +122 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/config.h +161 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/error.h +79 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/memory_pool.h +95 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/table.h +127 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/value.h +175 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/matchers/matcher.h +76 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core.h +12 -0
- data/ext/mongory_ext/mongory-core/src/foundations/array.c +287 -0
- data/ext/mongory_ext/mongory-core/src/foundations/array_private.h +19 -0
- data/ext/mongory_ext/mongory-core/src/foundations/config.c +270 -0
- data/ext/mongory_ext/mongory-core/src/foundations/config_private.h +48 -0
- data/ext/mongory_ext/mongory-core/src/foundations/error.c +38 -0
- data/ext/mongory_ext/mongory-core/src/foundations/memory_pool.c +298 -0
- data/ext/mongory_ext/mongory-core/src/foundations/string_buffer.c +65 -0
- data/ext/mongory_ext/mongory-core/src/foundations/string_buffer.h +49 -0
- data/ext/mongory_ext/mongory-core/src/foundations/table.c +498 -0
- data/ext/mongory_ext/mongory-core/src/foundations/utils.c +210 -0
- data/ext/mongory_ext/mongory-core/src/foundations/utils.h +70 -0
- data/ext/mongory_ext/mongory-core/src/foundations/value.c +500 -0
- data/ext/mongory_ext/mongory-core/src/matchers/array_record_matcher.c +164 -0
- data/ext/mongory_ext/mongory-core/src/matchers/array_record_matcher.h +47 -0
- data/ext/mongory_ext/mongory-core/src/matchers/base_matcher.c +122 -0
- data/ext/mongory_ext/mongory-core/src/matchers/base_matcher.h +100 -0
- data/ext/mongory_ext/mongory-core/src/matchers/compare_matcher.c +217 -0
- data/ext/mongory_ext/mongory-core/src/matchers/compare_matcher.h +83 -0
- data/ext/mongory_ext/mongory-core/src/matchers/composite_matcher.c +573 -0
- data/ext/mongory_ext/mongory-core/src/matchers/composite_matcher.h +125 -0
- data/ext/mongory_ext/mongory-core/src/matchers/existance_matcher.c +147 -0
- data/ext/mongory_ext/mongory-core/src/matchers/existance_matcher.h +48 -0
- data/ext/mongory_ext/mongory-core/src/matchers/external_matcher.c +124 -0
- data/ext/mongory_ext/mongory-core/src/matchers/external_matcher.h +46 -0
- data/ext/mongory_ext/mongory-core/src/matchers/inclusion_matcher.c +126 -0
- data/ext/mongory_ext/mongory-core/src/matchers/inclusion_matcher.h +46 -0
- data/ext/mongory_ext/mongory-core/src/matchers/literal_matcher.c +314 -0
- data/ext/mongory_ext/mongory-core/src/matchers/literal_matcher.h +97 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher.c +252 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher_explainable.c +79 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher_explainable.h +23 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher_traversable.c +60 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher_traversable.h +23 -0
- data/ext/mongory_ext/mongory_ext.c +683 -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/c_query_builder.rb +44 -0
- data/lib/mongory/converters/abstract_converter.rb +111 -0
- data/lib/mongory/converters/condition_converter.rb +64 -0
- data/lib/mongory/converters/converted.rb +81 -0
- data/lib/mongory/converters/data_converter.rb +37 -0
- data/lib/mongory/converters/key_converter.rb +87 -0
- data/lib/mongory/converters/value_converter.rb +52 -0
- data/lib/mongory/converters.rb +8 -0
- data/lib/mongory/matchers/abstract_matcher.rb +219 -0
- data/lib/mongory/matchers/abstract_multi_matcher.rb +124 -0
- data/lib/mongory/matchers/and_matcher.rb +72 -0
- data/lib/mongory/matchers/array_record_matcher.rb +93 -0
- data/lib/mongory/matchers/elem_match_matcher.rb +55 -0
- data/lib/mongory/matchers/eq_matcher.rb +46 -0
- data/lib/mongory/matchers/every_matcher.rb +56 -0
- data/lib/mongory/matchers/exists_matcher.rb +53 -0
- data/lib/mongory/matchers/field_matcher.rb +147 -0
- data/lib/mongory/matchers/gt_matcher.rb +41 -0
- data/lib/mongory/matchers/gte_matcher.rb +41 -0
- data/lib/mongory/matchers/hash_condition_matcher.rb +62 -0
- data/lib/mongory/matchers/in_matcher.rb +68 -0
- data/lib/mongory/matchers/literal_matcher.rb +121 -0
- data/lib/mongory/matchers/lt_matcher.rb +41 -0
- data/lib/mongory/matchers/lte_matcher.rb +41 -0
- data/lib/mongory/matchers/ne_matcher.rb +38 -0
- data/lib/mongory/matchers/nin_matcher.rb +68 -0
- data/lib/mongory/matchers/not_matcher.rb +40 -0
- data/lib/mongory/matchers/or_matcher.rb +68 -0
- data/lib/mongory/matchers/present_matcher.rb +55 -0
- data/lib/mongory/matchers/regex_matcher.rb +80 -0
- data/lib/mongory/matchers/size_matcher.rb +54 -0
- data/lib/mongory/matchers.rb +176 -0
- data/lib/mongory/mongoid.rb +19 -0
- data/lib/mongory/query_builder.rb +257 -0
- data/lib/mongory/query_matcher.rb +93 -0
- data/lib/mongory/query_operator.rb +28 -0
- data/lib/mongory/rails.rb +15 -0
- data/lib/mongory/utils/context.rb +48 -0
- data/lib/mongory/utils/debugger.rb +125 -0
- data/lib/mongory/utils/rails_patch.rb +22 -0
- data/lib/mongory/utils/singleton_builder.rb +31 -0
- data/lib/mongory/utils.rb +76 -0
- data/lib/mongory/version.rb +5 -0
- data/lib/mongory.rb +123 -0
- data/lib/mongory_ext.so +0 -0
- data/mongory.gemspec +62 -0
- data/scripts/build_with_core.sh +292 -0
- data/sig/mongory.rbs +4 -0
- metadata +159 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
|
5
|
+
module Mongory
|
6
|
+
module Generators
|
7
|
+
# Generates a Mongory initializer file with suggested configuration
|
8
|
+
# based on detected ORMs (ActiveRecord, Mongoid, Sequel).
|
9
|
+
#
|
10
|
+
# This is intended to be used via:
|
11
|
+
# rails generate mongory:install
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# # Will generate config/initializers/mongory.rb with appropriate snippets
|
15
|
+
# rails g mongory:install
|
16
|
+
class InstallGenerator < Rails::Generators::Base
|
17
|
+
source_root File.expand_path('templates', __dir__)
|
18
|
+
|
19
|
+
# Generates the Mongory initializer under `config/initializers/mongory.rb`.
|
20
|
+
# Dynamically injects converter and registration config based on detected ORMs.
|
21
|
+
#
|
22
|
+
# @return [void]
|
23
|
+
def create_initializer_file
|
24
|
+
@use_ar = gem_used?('activerecord')
|
25
|
+
@use_mongoid = gem_used?('mongoid')
|
26
|
+
@use_sequel = gem_used?('sequel')
|
27
|
+
|
28
|
+
template 'initializer.rb.erb', 'config/initializers/mongory.rb'
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Checks whether a specific gem is listed in the locked dependencies.
|
34
|
+
#
|
35
|
+
# @param gem_name [String] the name of the gem to check
|
36
|
+
# @return [Boolean] true if the gem is present in the lockfile
|
37
|
+
def gem_used?(gem_name)
|
38
|
+
Bundler.locked_gems.dependencies.key?(gem_name)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -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::AbstractMatcher
|
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,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'query_builder'
|
4
|
+
|
5
|
+
module Mongory
|
6
|
+
# Mongory::CQueryBuilder is a query builder for Mongory::CMatcher.
|
7
|
+
# It is used to build a query for a Mongory::CMatcher.
|
8
|
+
class CQueryBuilder < QueryBuilder
|
9
|
+
def each
|
10
|
+
return to_enum(:each) unless block_given?
|
11
|
+
|
12
|
+
@records.each do |record|
|
13
|
+
@context.current_record = record
|
14
|
+
yield record if @matcher.match?(record)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method :fast, :each
|
19
|
+
|
20
|
+
def explain
|
21
|
+
@matcher.match?(@records.first)
|
22
|
+
@matcher.explain
|
23
|
+
end
|
24
|
+
|
25
|
+
def trace
|
26
|
+
return to_enum(:trace) unless block_given?
|
27
|
+
|
28
|
+
@matcher.enable_trace
|
29
|
+
@records.each do |record|
|
30
|
+
@context.current_record = record
|
31
|
+
yield record if @matcher.match?(record)
|
32
|
+
end
|
33
|
+
ensure
|
34
|
+
@matcher.print_trace
|
35
|
+
@matcher.disable_trace
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def set_matcher(condition = {})
|
41
|
+
@matcher = CMatcher.new(condition, context: @context)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,111 @@
|
|
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
|
+
@convert_strategy_map = {}.compare_by_identity
|
37
|
+
end
|
38
|
+
|
39
|
+
# Applies the registered conversion to the given target object.
|
40
|
+
#
|
41
|
+
# @param target [Object] the object to convert
|
42
|
+
# @param other [Object] optional secondary value
|
43
|
+
# @return [Object] converted result
|
44
|
+
def convert(target, other = NOTHING)
|
45
|
+
convert_strategy = @convert_strategy_map[target.class] ||= find_strategy(target)
|
46
|
+
|
47
|
+
return fallback(target, other) if convert_strategy == NOTHING
|
48
|
+
return target.instance_exec(&convert_strategy) if other == NOTHING
|
49
|
+
|
50
|
+
target.instance_exec(other, &convert_strategy)
|
51
|
+
end
|
52
|
+
|
53
|
+
def fallback(target, _)
|
54
|
+
target
|
55
|
+
end
|
56
|
+
|
57
|
+
# Finds the appropriate conversion strategy for the target object.
|
58
|
+
# Searches through registered rules and returns the first matching one,
|
59
|
+
# or the fallback strategy if no match is found.
|
60
|
+
#
|
61
|
+
# @param target [Object] the object to find a strategy for
|
62
|
+
# @return [Proc] the conversion strategy to use
|
63
|
+
def find_strategy(target)
|
64
|
+
@registries.each do |registry|
|
65
|
+
next unless target.is_a?(registry.klass)
|
66
|
+
|
67
|
+
return registry.exec
|
68
|
+
end
|
69
|
+
|
70
|
+
NOTHING
|
71
|
+
end
|
72
|
+
|
73
|
+
# Opens a configuration block to register more converters.
|
74
|
+
#
|
75
|
+
# @yield DSL block to configure more rules
|
76
|
+
# @return [void]
|
77
|
+
def configure
|
78
|
+
yield self
|
79
|
+
freeze
|
80
|
+
end
|
81
|
+
|
82
|
+
# Freezes all internal registries.
|
83
|
+
#
|
84
|
+
# @return [void]
|
85
|
+
def freeze
|
86
|
+
@registries.freeze
|
87
|
+
end
|
88
|
+
|
89
|
+
# Registers a conversion rule for a given class.
|
90
|
+
#
|
91
|
+
# @param klass [Class, Module] the target class
|
92
|
+
# @param converter [Symbol, nil] method name to call as a conversion
|
93
|
+
# @yield [*args] block that performs the conversion
|
94
|
+
# @return [void]
|
95
|
+
# @raise [RuntimeError] if input is invalid
|
96
|
+
def register(klass, converter = nil, &block)
|
97
|
+
raise 'converter or block is required.' if [converter, block].compact.empty?
|
98
|
+
raise 'A class or module is reuqired.' unless klass.is_a?(Module)
|
99
|
+
|
100
|
+
if converter.is_a?(Symbol)
|
101
|
+
register(klass) { |*args, &bl| send(converter, *args, &bl) }
|
102
|
+
elsif block.is_a?(Proc)
|
103
|
+
@registries.unshift(Registry.new(klass, block))
|
104
|
+
@convert_strategy_map[klass] = block
|
105
|
+
else
|
106
|
+
raise 'Support Symbol and block only.'
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,64 @@
|
|
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
|
+
# Applies both key and value conversion, and merges overlapping keys.
|
20
|
+
#
|
21
|
+
# @param condition [Hash] the flat condition hash to convert
|
22
|
+
# @return [Hash] the transformed nested condition
|
23
|
+
def convert(condition)
|
24
|
+
return condition if condition.is_a?(Converted)
|
25
|
+
|
26
|
+
result = Converted::Hash.new
|
27
|
+
condition.each_pair do |k, v|
|
28
|
+
converted_value = value_converter.convert(v)
|
29
|
+
converted_pair = key_converter.convert(k, converted_value)
|
30
|
+
result.deep_merge!(converted_pair)
|
31
|
+
end
|
32
|
+
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
# @note Singleton instance, not configurable after initialization
|
37
|
+
# Returns the key converter used to transform condition keys.
|
38
|
+
#
|
39
|
+
# @return [AbstractConverter] the key converter instance
|
40
|
+
def key_converter
|
41
|
+
KeyConverter.instance
|
42
|
+
end
|
43
|
+
|
44
|
+
# @note Singleton instance, not configurable after initialization
|
45
|
+
# Returns the value converter used to transform condition values.
|
46
|
+
#
|
47
|
+
# @return [AbstractConverter]
|
48
|
+
def value_converter
|
49
|
+
ValueConverter.instance
|
50
|
+
end
|
51
|
+
|
52
|
+
# Freezes internal converters to prevent further modification.
|
53
|
+
#
|
54
|
+
# @return [void]
|
55
|
+
def freeze
|
56
|
+
super
|
57
|
+
key_converter.freeze
|
58
|
+
value_converter.freeze
|
59
|
+
end
|
60
|
+
|
61
|
+
undef_method :register
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Converters
|
5
|
+
# Converted is a module that provides conversion structures marked as
|
6
|
+
# Converted. It is used to convert various types of data into a
|
7
|
+
# standardized form for MongoDB queries. The module includes
|
8
|
+
# classes for converting hashes and arrays into nested structures.
|
9
|
+
# It is used internally by the ConditionConverter and ValueConverter
|
10
|
+
# to handle the conversion of complex data types into a format
|
11
|
+
# suitable for MongoDB queries.
|
12
|
+
module Converted
|
13
|
+
def instance_convert(other)
|
14
|
+
return other if other.is_a?(Converted)
|
15
|
+
|
16
|
+
case other
|
17
|
+
when ::Hash
|
18
|
+
Converted::Hash.new(other)
|
19
|
+
when ::Array
|
20
|
+
Converted::Array.new(other)
|
21
|
+
else
|
22
|
+
other
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Converts a flat condition hash into a nested structure.
|
27
|
+
# Applies value conversion to each element.
|
28
|
+
# This is used for conditions that are hashes of values.
|
29
|
+
# It is used internally by the ConditionConverter and ValueConverter
|
30
|
+
# to handle the conversion of complex data types into a format
|
31
|
+
# suitable for MongoDB queries.
|
32
|
+
class Hash < ::Hash
|
33
|
+
include Converted
|
34
|
+
|
35
|
+
def initialize(other = {})
|
36
|
+
super()
|
37
|
+
other.each_pair do |k, v|
|
38
|
+
self[k] = instance_convert(v)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def deep_merge(other)
|
43
|
+
dup.deep_merge!(Hash.new(other))
|
44
|
+
end
|
45
|
+
|
46
|
+
def deep_merge!(other)
|
47
|
+
_deep_merge!(self, Hash.new(other))
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def _deep_merge!(left, right)
|
53
|
+
left.merge!(right) do |_, a, b|
|
54
|
+
if a.is_a?(::Hash) && b.is_a?(::Hash)
|
55
|
+
_deep_merge!(a.dup, b)
|
56
|
+
else
|
57
|
+
b
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Converts a flat condition array into a nested structure.
|
64
|
+
# Applies value conversion to each element.
|
65
|
+
# This is used for conditions that are arrays of values.
|
66
|
+
# It is used internally by the ConditionConverter and ValueConverter
|
67
|
+
# to handle the conversion of complex data types into a format
|
68
|
+
# suitable for MongoDB queries.
|
69
|
+
class Array < ::Array
|
70
|
+
include Converted
|
71
|
+
|
72
|
+
def initialize(other)
|
73
|
+
super()
|
74
|
+
other.each do |v|
|
75
|
+
self << instance_convert(v)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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
|
+
alias_method :super_convert, :convert
|
17
|
+
|
18
|
+
# Converts a value into its standardized form based on its type.
|
19
|
+
# Handles common primitive types with predefined conversion rules.
|
20
|
+
#
|
21
|
+
# @param target [Object] the value to convert
|
22
|
+
# @return [Object] the converted value
|
23
|
+
def convert(target)
|
24
|
+
case target
|
25
|
+
when String, Integer, Hash, Array
|
26
|
+
target
|
27
|
+
when Symbol, Date
|
28
|
+
target.to_s
|
29
|
+
when Time, DateTime
|
30
|
+
target.iso8601
|
31
|
+
else
|
32
|
+
super_convert(target)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|