dry-validation-rust 0.1.0.pre5

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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +74 -0
  3. data/LICENSE +21 -0
  4. data/NOTICE.md +28 -0
  5. data/README.md +459 -0
  6. data/docs/ARCHITECTURE.md +256 -0
  7. data/docs/COMPATIBILITY.md +198 -0
  8. data/docs/FEASIBILITY.md +207 -0
  9. data/docs/SUPPORT_MATRIX.md +66 -0
  10. data/docs/VERIFICATION.md +128 -0
  11. data/dry-validation-rust.gemspec +58 -0
  12. data/ext/dry_validation_rust/Cargo.lock +809 -0
  13. data/ext/dry_validation_rust/Cargo.toml +44 -0
  14. data/ext/dry_validation_rust/benches/coercion.rs +77 -0
  15. data/ext/dry_validation_rust/benches/full_schema.rs +189 -0
  16. data/ext/dry_validation_rust/benches/plan_compile.rs +37 -0
  17. data/ext/dry_validation_rust/benches/predicates.rs +105 -0
  18. data/ext/dry_validation_rust/extconf.rb +29 -0
  19. data/ext/dry_validation_rust/src/coercion.rs +515 -0
  20. data/ext/dry_validation_rust/src/engine.rs +416 -0
  21. data/ext/dry_validation_rust/src/error.rs +82 -0
  22. data/ext/dry_validation_rust/src/extract_primitive.rs +23 -0
  23. data/ext/dry_validation_rust/src/generated_predicates.rs +33 -0
  24. data/ext/dry_validation_rust/src/lib.rs +228 -0
  25. data/ext/dry_validation_rust/src/plan.rs +611 -0
  26. data/ext/dry_validation_rust/src/predicates.rs +449 -0
  27. data/ext/dry_validation_rust/src/ruby_bridge.rs +78 -0
  28. data/lib/dry/schema.rb +6 -0
  29. data/lib/dry/validation/rust/block_keyword_parameters.rb +20 -0
  30. data/lib/dry/validation/rust/config.rb +74 -0
  31. data/lib/dry/validation/rust/contract/result.rb +180 -0
  32. data/lib/dry/validation/rust/contract/values.rb +73 -0
  33. data/lib/dry/validation/rust/contract.rb +400 -0
  34. data/lib/dry/validation/rust/errors.rb +14 -0
  35. data/lib/dry/validation/rust/evaluator.rb +295 -0
  36. data/lib/dry/validation/rust/failures.rb +57 -0
  37. data/lib/dry/validation/rust/generated_predicates.rb +14 -0
  38. data/lib/dry/validation/rust/macros.rb +45 -0
  39. data/lib/dry/validation/rust/message.rb +41 -0
  40. data/lib/dry/validation/rust/message_backend.rb +115 -0
  41. data/lib/dry/validation/rust/message_set.rb +159 -0
  42. data/lib/dry/validation/rust/native.rb +25 -0
  43. data/lib/dry/validation/rust/path.rb +65 -0
  44. data/lib/dry/validation/rust/path_trie.rb +57 -0
  45. data/lib/dry/validation/rust/result.rb +3 -0
  46. data/lib/dry/validation/rust/rule.rb +62 -0
  47. data/lib/dry/validation/rust/schema/dsl.rb +76 -0
  48. data/lib/dry/validation/rust/schema/field_builder.rb +156 -0
  49. data/lib/dry/validation/rust/schema/field_definition.rb +99 -0
  50. data/lib/dry/validation/rust/schema/predicate_block.rb +56 -0
  51. data/lib/dry/validation/rust/schema/processor_hooks.rb +46 -0
  52. data/lib/dry/validation/rust/schema/result.rb +67 -0
  53. data/lib/dry/validation/rust/schema/ruby_type_processor.rb +44 -0
  54. data/lib/dry/validation/rust/schema.rb +323 -0
  55. data/lib/dry/validation/rust/values.rb +3 -0
  56. data/lib/dry/validation/rust/version.rb +10 -0
  57. data/lib/dry/validation/rust.rb +55 -0
  58. data/lib/dry/validation.rb +66 -0
  59. data/lib/dry-schema.rb +3 -0
  60. data/lib/dry-validation.rb +3 -0
  61. data/lib/dry_validation_rust.rb +3 -0
  62. data/predicates.yml +67 -0
  63. data/rust-toolchain.toml +9 -0
  64. metadata +260 -0
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Validation
5
+ module Rust
6
+ class Schema
7
+ # @api private
8
+ class PredicateBlock
9
+ ARITY_MAP = {
10
+ gt: 1, gteq: 1, lt: 1, lteq: 1, min_size: 1, max_size: 1, size: 1,
11
+ format: 1, included_in: 1, excluded_from: 1, eql: 1, not_eql: 1,
12
+ odd: 0, even: 0
13
+ }.freeze
14
+
15
+ def initialize(definition)
16
+ @definition = definition
17
+ end
18
+
19
+ def method_missing(name, *args, **kwargs, &block)
20
+ if name.to_s.end_with?('?') && block.nil?
21
+ validate_arity(name, args, kwargs)
22
+ argument = if kwargs.empty?
23
+ args.length <= 1 ? args.first : args
24
+ else
25
+ kwargs
26
+ end
27
+ @definition.add_predicate(name, argument: argument.nil? || argument)
28
+ return self
29
+ end
30
+
31
+ raise UnsupportedFeatureError,
32
+ "unsupported predicate composition expression: #{name.inspect}"
33
+ end
34
+
35
+ def respond_to_missing?(name, include_private = false)
36
+ name.to_s.end_with?('?') || super
37
+ end
38
+
39
+ private
40
+
41
+ def validate_arity(name, args, kwargs)
42
+ normalized_name = name.to_s.delete_suffix('?').to_sym
43
+ expected = ARITY_MAP[normalized_name]
44
+ return unless expected
45
+
46
+ argument_count = args.length + (kwargs.empty? ? 0 : 1)
47
+ return if argument_count == expected
48
+
49
+ article = expected == 1 ? 'exactly one argument' : 'no arguments'
50
+ raise ArgumentError, "#{name} expects #{article}, got #{argument_count}"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Validation
5
+ module Rust
6
+ class Schema
7
+ # @api private
8
+ class ProcessorHooks
9
+ STAGES = %i[value_coercer].freeze
10
+
11
+ def self.deep_dup(value)
12
+ case value
13
+ when Hash
14
+ value.each_with_object({}) do |(key, item), copy|
15
+ copy[deep_dup(key)] = deep_dup(item)
16
+ end
17
+ when Array
18
+ value.map { |item| deep_dup(item) }
19
+ else
20
+ value.dup
21
+ end
22
+ rescue TypeError
23
+ value
24
+ end
25
+
26
+ def self.register(hooks, name, block)
27
+ unless STAGES.include?(name)
28
+ raise ArgumentError, "Undefined step name #{name.inspect}. Available names: #{STAGES.inspect}"
29
+ end
30
+ raise ArgumentError, 'processor hooks require a block' unless block
31
+
32
+ hooks << block
33
+ end
34
+
35
+ def self.apply(hooks, data)
36
+ hooks.each do |hook|
37
+ replacement = hook.call(data)
38
+ data = replacement if replacement.is_a?(Hash)
39
+ end
40
+ data
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Validation
5
+ module Rust
6
+ class Schema
7
+ Result = Data.define(:output, :messages, :error_prefixes) do
8
+ class << self
9
+ alias_method :build, :new
10
+ private :build
11
+
12
+ # Creates a structural validation result and caches its schema-error path index.
13
+ #
14
+ # @param output [Hash] coerced schema output
15
+ # @param messages [Array<Message>] schema validation failures
16
+ # @return [Result] immutable schema validation result
17
+ def new(output = nil, messages = nil, **kwargs)
18
+ unknown = kwargs.keys - %i[output messages]
19
+ unless unknown.empty?
20
+ raise ArgumentError,
21
+ "unknown keyword#{'s' if unknown.length > 1}: #{unknown.map(&:inspect).join(', ')}"
22
+ end
23
+
24
+ output = kwargs.fetch(:output) if kwargs.key?(:output)
25
+ messages = kwargs.fetch(:messages) if kwargs.key?(:messages)
26
+ prefixes = PathTrie.new
27
+ messages.each { |message| prefixes.add(message.path) }
28
+
29
+ build(output, messages, prefixes.freeze)
30
+ end
31
+ end
32
+
33
+ # Returns the immutable schema-error path index.
34
+ #
35
+ # @return [PathTrie] cached schema-error path index
36
+
37
+ alias_method :to_h, :output
38
+
39
+ def success?
40
+ messages.empty?
41
+ end
42
+
43
+ def failure?
44
+ !success?
45
+ end
46
+
47
+ def errors(options = {})
48
+ MessageSet.new(messages, options).with(options)
49
+ end
50
+
51
+ def error?(spec)
52
+ path = Path.parse(spec)
53
+ messages.any? { |message| Path.prefix?(message.path, path) }
54
+ end
55
+
56
+ def [](key)
57
+ output[key]
58
+ end
59
+
60
+ def key?(key)
61
+ output.key?(key)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Validation
5
+ module Rust
6
+ class Schema
7
+ # @api private
8
+ class RubyTypeProcessor
9
+ def self.apply(definitions, data, messages, message_backend)
10
+ error_paths = messages.to_set(&:path)
11
+ apply_at(definitions, data, [], messages, error_paths, message_backend)
12
+ end
13
+
14
+ def self.apply_at(definitions, data, prefix, messages, error_paths, message_backend)
15
+ return unless data.is_a?(Hash)
16
+
17
+ definitions.each do |field|
18
+ next unless data.key?(field.name)
19
+
20
+ path = [*prefix, field.name]
21
+ if field.ruby_type && !error_paths.include?(path)
22
+ result = field.ruby_type.try(data[field.name])
23
+ data[field.name] = result.input
24
+ unless result.success?
25
+ messages << Message.new(
26
+ text: message_backend.message(
27
+ code: :type, predicate: nil, args: [], type: field.type, fallback: 'is invalid'
28
+ ),
29
+ path: path, code: :type, source: :schema
30
+ )
31
+ error_paths << path
32
+ end
33
+ end
34
+
35
+ apply_at(field.children, data[field.name], path, messages, error_paths, message_backend)
36
+ end
37
+ end
38
+
39
+ private_class_method :apply_at
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'date'
5
+ require 'time'
6
+ require 'bigdecimal'
7
+ require_relative 'generated_predicates'
8
+
9
+ module Dry
10
+ module Validation
11
+ module Rust
12
+ # A compiled schema that validates and coerces input hashes.
13
+ #
14
+ # @example Define and call a schema
15
+ # schema = Dry::Validation::Rust::Schema.Params do
16
+ # required(:age).value(:integer)
17
+ # end
18
+ # result = schema.call("age" => "25")
19
+ # result.to_h # => { age: 25 }
20
+ class Schema
21
+ # Type symbols supported by schema fields.
22
+ #
23
+ # @return [Array<Symbol>]
24
+ TYPES = %i[
25
+ any nil bool true false integer float decimal string symbol array hash
26
+ date date_time datetime time
27
+ ].freeze
28
+ # @api private
29
+ Predicate = Data.define(:name, :argument) do
30
+ def initialize(name:, argument: true)
31
+ super(name: name.to_s.delete_suffix('?').to_sym, argument: argument)
32
+ end
33
+ end
34
+ end
35
+
36
+ require_relative 'schema/result'
37
+ require_relative 'schema/processor_hooks'
38
+ require_relative 'schema/field_definition'
39
+ require_relative 'schema/predicate_block'
40
+ require_relative 'schema/field_builder'
41
+ require_relative 'schema/ruby_type_processor'
42
+ require_relative 'schema/dsl'
43
+
44
+ class Schema
45
+ # @return [Symbol] the schema input mode.
46
+ attr_reader :mode
47
+
48
+ # @api private
49
+ # @return [Array<FieldDefinition>] the compiled top-level field definitions.
50
+ attr_reader :fields
51
+
52
+ # @api private
53
+ # @return [Native::Engine] the native engine that executes this schema.
54
+ attr_reader :engine
55
+
56
+ # Builds a schema from a DSL block and optional schemas to import.
57
+ #
58
+ # @param mode [Symbol] the input mode, such as `:schema`, `:params`, or `:json`.
59
+ # @param external_schemas [Array<Schema>] compiled schemas whose fields are imported.
60
+ # @yield the schema DSL block.
61
+ # @return [Schema] the compiled schema.
62
+ def self.define(mode = :schema, *external_schemas, &block)
63
+ dsl = DSL.new(mode: mode)
64
+ external_schemas.each { |schema| dsl.import(schema) }
65
+ dsl.instance_eval(&block) if block
66
+ dsl.compile
67
+ end
68
+
69
+ # Builds a schema that coerces web request parameter input.
70
+ #
71
+ # @param external_schemas [Array<Schema>] compiled schemas whose fields are imported.
72
+ # @yield the schema DSL block.
73
+ # @return [Schema] the compiled params-mode schema.
74
+ def self.Params(*external_schemas, &) = define(:params, *external_schemas, &)
75
+
76
+ # Builds a schema that coerces JSON-compatible input.
77
+ #
78
+ # @param external_schemas [Array<Schema>] compiled schemas whose fields are imported.
79
+ # @yield the schema DSL block.
80
+ # @return [Schema] the compiled JSON-mode schema.
81
+ def self.JSON(*external_schemas, &) = define(:json, *external_schemas, &)
82
+
83
+ # Compiles field definitions into a native schema plan.
84
+ #
85
+ # @api private
86
+ #
87
+ # @param mode [Symbol] the input mode.
88
+ # @param fields [Array<FieldDefinition>] field definitions to compile.
89
+ # @param before_hooks [Array<#call>] processors run before native validation.
90
+ # @param after_hooks [Array<#call>] processors run after native validation.
91
+ # @param validate_keys [Boolean] whether unknown keys are validation errors.
92
+ # @param messages [MessageConfig] validation message configuration.
93
+ # @raise [NativeExtensionError] if the native schema plan cannot be compiled.
94
+ def initialize(mode:, fields:, before_hooks: [], after_hooks: [], validate_keys: false,
95
+ messages: MessageConfig.new)
96
+ @mode = mode.to_sym
97
+ @fields = fields.freeze
98
+ @fields_by_name = fields.to_h { |field| [field.name, field] }.freeze
99
+ @has_ruby_predicates = ruby_predicates?(fields)
100
+ @before_hooks, @after_hooks = [before_hooks, after_hooks].map { _1.dup.freeze }
101
+ @message_backend = messages.backend_class.new(messages)
102
+ begin
103
+ plan = {
104
+ engine_version: ENGINE_VERSION,
105
+ mode: mode.to_s,
106
+ validate_keys: validate_keys,
107
+ fields: fields.map(&:to_native_h)
108
+ }
109
+ @engine = Native::Engine.new(JSON.generate(plan, max_nesting: false))
110
+ rescue StandardError => e
111
+ raise NativeExtensionError, "could not compile native schema plan: #{e.message}"
112
+ end
113
+ end
114
+
115
+ # Validates and coerces a Hash.
116
+ #
117
+ # @param input [Hash] input to validate.
118
+ # @return [Result] the output and validation messages.
119
+ # @raise [ArgumentError] if +input+ is not a Hash.
120
+ def call(input)
121
+ raise ArgumentError, "Input must be a Hash. #{input.class} was given." unless input.is_a?(Hash)
122
+
123
+ # Before hooks receive an isolated copy and may safely mutate nested values.
124
+ prepared_input = before_hooks.empty? ? input.dup : ProcessorHooks.deep_dup(input)
125
+ prepared_input = ProcessorHooks.apply(before_hooks, prepared_input)
126
+ result = engine.call(prepared_input)
127
+ output = ProcessorHooks.apply(after_hooks, result.output)
128
+ messages = result.errors.map do |error|
129
+ path = error[:path]
130
+ code = error[:code]
131
+ text = error[:text]
132
+ predicate, args = native_predicate_details(path, code)
133
+ native_message(path, code, text, predicate, args)
134
+ end
135
+ RubyTypeProcessor.apply(fields, output, messages, @message_backend)
136
+ apply_ruby_predicates(fields, output, [], messages) if @has_ruby_predicates
137
+ Result.new(output, messages.freeze)
138
+ end
139
+
140
+ # Validates and coerces a Hash.
141
+ #
142
+ # Alias for {#call}.
143
+ #
144
+ # @param input [Hash] input to validate.
145
+ # @return [Result] the output and validation messages.
146
+ # @raise [ArgumentError] if +input+ is not a Hash.
147
+ def [](input)
148
+ call(input)
149
+ end
150
+
151
+ # Returns all declared field paths, including nested array paths.
152
+ #
153
+ # @api private
154
+ #
155
+ # @return [Array<Array<Symbol, Integer>>] declared field paths. Array members
156
+ # use +:__index__+ as an index placeholder.
157
+ def key_paths
158
+ paths_for(fields)
159
+ end
160
+
161
+ # Returns a diagnostic representation of this compiled schema.
162
+ #
163
+ # @return [String] the schema mode, field names, and native-engine marker.
164
+ def inspect
165
+ "#<#{self.class} mode=#{mode.inspect} fields=#{fields.map(&:name).inspect} native=true>"
166
+ end
167
+
168
+ private
169
+
170
+ attr_reader :before_hooks, :after_hooks
171
+
172
+ # @api private
173
+ def native_message(path, code, text, predicate, args)
174
+ Message.new(
175
+ text: native_error_message(code, text, predicate, args, path),
176
+ path: path, code: code, source: :schema, predicate: predicate, args: args
177
+ )
178
+ end
179
+
180
+ # @api private
181
+ def native_error_message(code, native_text, predicate, args, path)
182
+ field = field_at_path(path)
183
+ @message_backend.message(
184
+ code: code, predicate: predicate&.to_s&.delete_suffix('?'), args: args,
185
+ type: field&.normalized_type, fallback: native_text
186
+ )
187
+ end
188
+
189
+ # @api private
190
+ def paths_for(definitions, prefix = [])
191
+ definitions.flat_map do |field|
192
+ current = [*prefix, field.name]
193
+ nested = paths_for(field.children, current)
194
+ member_nested = field.member ? paths_for(field.member.children, [*current, :__index__]) : []
195
+ [current, *nested, *member_nested]
196
+ end
197
+ end
198
+
199
+ # @api private
200
+ def ruby_predicates?(definitions)
201
+ definitions.any? do |field|
202
+ field.predicates.any? { |predicate| !NATIVE_PREDICATES.include?(predicate.name) } ||
203
+ ruby_predicates?(field.children) ||
204
+ (field.member && ruby_predicates?([field.member]))
205
+ end
206
+ end
207
+
208
+ # @api private
209
+ def apply_ruby_predicates(definitions, data, prefix, messages)
210
+ error_paths = messages.to_set(&:path)
211
+ apply_ruby_predicates_at(definitions, data, prefix, messages, error_paths)
212
+ end
213
+
214
+ # @api private
215
+ def apply_ruby_predicates_at(definitions, data, prefix, messages, error_paths)
216
+ stack = [[:definitions, definitions, data, prefix]]
217
+
218
+ until stack.empty?
219
+ kind, *arguments = stack.pop
220
+ case kind
221
+ when :definitions
222
+ current_definitions, current_data, current_prefix = arguments
223
+ next unless current_data.is_a?(Hash)
224
+
225
+ current_definitions.reverse_each do |field|
226
+ stack << [:field, field, current_data, current_prefix]
227
+ end
228
+ when :field
229
+ field, current_data, current_prefix = arguments
230
+ next unless current_data.key?(field.name)
231
+
232
+ path = [*current_prefix, field.name]
233
+ value = current_data[field.name]
234
+ apply_ruby_predicates_to(field, value, path, messages, error_paths)
235
+
236
+ if value.is_a?(Hash)
237
+ stack << [:definitions, field.children, value, path]
238
+ elsif value.is_a?(Array) && field.member
239
+ value.each_index.reverse_each do |index|
240
+ stack << [:member, field.member, value[index], [*path, index]]
241
+ end
242
+ end
243
+ when :member
244
+ member, value, path = arguments
245
+ apply_ruby_predicates_to(member, value, path, messages, error_paths)
246
+ stack << [:definitions, member.children, value, path]
247
+ end
248
+ end
249
+ end
250
+
251
+ # @api private
252
+ def apply_ruby_predicates_to(field, value, path, messages, error_paths)
253
+ return if error_paths.include?(path)
254
+
255
+ field.predicates.each do |predicate|
256
+ next if NATIVE_PREDICATES.include?(predicate.name)
257
+
258
+ unless predicate_valid?(predicate, value)
259
+ messages << predicate_message(predicate, path)
260
+ error_paths << path
261
+ end
262
+ end
263
+ end
264
+
265
+ # @api private
266
+ def predicate_valid?(predicate, value)
267
+ case predicate.name
268
+ when :format then value.respond_to?(:match?) && predicate.argument.match?(value)
269
+ when :included_in then predicate.argument.include?(value)
270
+ when :excluded_from then !predicate.argument.include?(value)
271
+ when :eql then value.eql?(predicate.argument)
272
+ when :not_eql then !value.eql?(predicate.argument)
273
+ else
274
+ raise UnsupportedFeatureError,
275
+ "predicate #{predicate.name.inspect} is not supported natively; move it to a contract rule"
276
+ end
277
+ end
278
+
279
+ # @api private
280
+ def predicate_message(predicate, path)
281
+ text = case predicate.name
282
+ when :format then 'is in invalid format'
283
+ when :included_in then "must be one of: #{Array(predicate.argument).join(', ')}"
284
+ when :excluded_from then "must not be one of: #{Array(predicate.argument).join(', ')}"
285
+ when :eql then "must be equal to #{predicate.argument}"
286
+ when :not_eql then "must not be equal to #{predicate.argument}"
287
+ else 'is invalid'
288
+ end
289
+ text = @message_backend.message(
290
+ code: predicate.name, predicate: predicate.name, args: [predicate.argument], type: nil, fallback: text
291
+ )
292
+ Message.new(
293
+ text: text, path: path, code: predicate.name, source: :schema,
294
+ predicate: "#{predicate.name}?", args: [predicate.argument]
295
+ )
296
+ end
297
+
298
+ def native_predicate_details(path, code)
299
+ field = field_at_path(path)
300
+ predicate = field&.predicates&.find { |candidate| candidate.name == code.to_sym }
301
+ predicate ? [:"#{predicate.name}?", [predicate.argument]] : [nil, []]
302
+ end
303
+
304
+ def field_at_path(path)
305
+ definition = nil
306
+
307
+ path.each do |part|
308
+ if part.is_a?(Integer)
309
+ return nil unless definition&.member
310
+
311
+ definition = definition.member
312
+ else
313
+ definition = definition ? definition.child_at(part) : @fields_by_name[part.to_sym]
314
+ return nil unless definition
315
+ end
316
+ end
317
+
318
+ definition
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'contract/values'
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Validation
5
+ module Rust
6
+ VERSION = '0.1.0.pre5'
7
+ ENGINE_VERSION = 1
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rust/version'
4
+ require_relative 'rust/errors'
5
+ require_relative 'rust/native'
6
+ require_relative 'rust/path'
7
+ require_relative 'rust/path_trie'
8
+ require_relative 'rust/message'
9
+ require_relative 'rust/message_set'
10
+ require_relative 'rust/contract/values'
11
+ require_relative 'rust/macros'
12
+ require_relative 'rust/failures'
13
+ require_relative 'rust/message_backend'
14
+ require_relative 'rust/config'
15
+ require_relative 'rust/schema'
16
+ require_relative 'rust/rule'
17
+ require_relative 'rust/evaluator'
18
+ require_relative 'rust/contract/result'
19
+ require_relative 'rust/contract'
20
+
21
+ module Dry
22
+ module Validation
23
+ module Rust
24
+ class << self
25
+ # @api private
26
+ def global_macros
27
+ @global_macros ||= MacroRegistry.new
28
+ end
29
+
30
+ def register_macro(name, *, &)
31
+ global_macros.register(name, *, &)
32
+ self
33
+ end
34
+
35
+ def Contract(options = {}, &)
36
+ Contract.build(options, &)
37
+ end
38
+
39
+ def load_extensions(*names)
40
+ names.each do |name|
41
+ next if name.to_sym == :predicates_as_macros
42
+
43
+ raise UnsupportedFeatureError,
44
+ "extension #{name.inspect} is not implemented in the experimental Rust compatibility layer"
45
+ end
46
+ self
47
+ end
48
+ end
49
+
50
+ register_macro(:acceptance) do
51
+ key.failure(:acceptance) unless value.equal?(true)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'validation/rust'
4
+
5
+ module Dry
6
+ if const_defined?(:Schema, false) &&
7
+ !const_get(:Schema, false).const_defined?(:RUST_COMPATIBILITY_LAYER, false)
8
+ raise LoadError, <<~MESSAGE
9
+ dry-schema and dry-validation-rust cannot both own Dry::Schema in exact compatibility mode.
10
+ Remove the upstream dry-schema/dry-validation gems, or use the side-by-side
11
+ dry/validation/rust namespace.
12
+ MESSAGE
13
+ end
14
+
15
+ unless const_defined?(:Schema, false)
16
+ module Schema
17
+ RUST_COMPATIBILITY_LAYER = true
18
+ VERSION = Validation::Rust::VERSION
19
+
20
+ class << self
21
+ def Params(*external_schemas, &)
22
+ Validation::Rust::Schema.Params(*external_schemas, &)
23
+ end
24
+
25
+ def JSON(*external_schemas, &)
26
+ Validation::Rust::Schema.JSON(*external_schemas, &)
27
+ end
28
+
29
+ def define(*external_schemas, &)
30
+ Validation::Rust::Schema.define(:schema, *external_schemas, &)
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ module Validation
37
+ if const_defined?(:Contract, false) && const_get(:Contract, false) != Rust::Contract
38
+ raise LoadError, <<~MESSAGE
39
+ dry-validation and dry-validation-rust cannot both own Dry::Validation::Contract.
40
+ Remove the upstream dry-validation gem when using exact compatibility mode, or
41
+ require "dry/validation/rust" and inherit from Dry::Validation::Rust::Contract.
42
+ MESSAGE
43
+ end
44
+
45
+ Contract = Rust::Contract unless const_defined?(:Contract, false)
46
+ Result = Rust::Contract::Result unless const_defined?(:Result, false)
47
+ Message = Rust::Message unless const_defined?(:Message, false)
48
+ MessageSet = Rust::MessageSet unless const_defined?(:MessageSet, false)
49
+ Evaluator = Rust::Evaluator unless const_defined?(:Evaluator, false)
50
+ VERSION = Rust::VERSION unless const_defined?(:VERSION, false)
51
+
52
+ class << self
53
+ def Contract(options = {}, &)
54
+ Rust.Contract(options, &)
55
+ end
56
+
57
+ def register_macro(name, *, &)
58
+ Rust.register_macro(name, *, &)
59
+ end
60
+
61
+ def load_extensions(*names)
62
+ Rust.load_extensions(*names)
63
+ end
64
+ end
65
+ end
66
+ end
data/lib/dry-schema.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/schema'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/validation'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/validation/rust'