mongory 0.7.2-arm64-darwin-23

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 (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.bundle +0 -0
  112. data/mongory.gemspec +50 -0
  113. data/scripts/build_with_core.sh +292 -0
  114. data/sig/mongory.rbs +4 -0
  115. metadata +164 -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