dry-schema 1.4.3 → 1.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +219 -97
  3. data/config/errors.yml +4 -0
  4. data/dry-schema.gemspec +46 -0
  5. data/lib/dry-schema.rb +1 -1
  6. data/lib/dry/schema.rb +19 -6
  7. data/lib/dry/schema/compiler.rb +5 -5
  8. data/lib/dry/schema/config.rb +15 -6
  9. data/lib/dry/schema/constants.rb +16 -7
  10. data/lib/dry/schema/dsl.rb +87 -27
  11. data/lib/dry/schema/extensions.rb +10 -2
  12. data/lib/dry/schema/extensions/hints.rb +15 -8
  13. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +1 -1
  14. data/lib/dry/schema/extensions/hints/message_set_methods.rb +0 -47
  15. data/lib/dry/schema/extensions/info.rb +27 -0
  16. data/lib/dry/schema/extensions/info/schema_compiler.rb +105 -0
  17. data/lib/dry/schema/extensions/monads.rb +1 -1
  18. data/lib/dry/schema/extensions/struct.rb +32 -0
  19. data/lib/dry/schema/json.rb +1 -1
  20. data/lib/dry/schema/key.rb +16 -1
  21. data/lib/dry/schema/key_coercer.rb +4 -4
  22. data/lib/dry/schema/key_map.rb +9 -4
  23. data/lib/dry/schema/key_validator.rb +67 -0
  24. data/lib/dry/schema/macros.rb +8 -8
  25. data/lib/dry/schema/macros/array.rb +17 -4
  26. data/lib/dry/schema/macros/core.rb +9 -4
  27. data/lib/dry/schema/macros/dsl.rb +34 -19
  28. data/lib/dry/schema/macros/each.rb +4 -4
  29. data/lib/dry/schema/macros/filled.rb +5 -5
  30. data/lib/dry/schema/macros/hash.rb +21 -3
  31. data/lib/dry/schema/macros/key.rb +9 -9
  32. data/lib/dry/schema/macros/maybe.rb +3 -3
  33. data/lib/dry/schema/macros/optional.rb +1 -1
  34. data/lib/dry/schema/macros/required.rb +1 -1
  35. data/lib/dry/schema/macros/schema.rb +23 -2
  36. data/lib/dry/schema/macros/value.rb +32 -10
  37. data/lib/dry/schema/message.rb +35 -9
  38. data/lib/dry/schema/message/or.rb +18 -39
  39. data/lib/dry/schema/message/or/abstract.rb +28 -0
  40. data/lib/dry/schema/message/or/multi_path.rb +37 -0
  41. data/lib/dry/schema/message/or/single_path.rb +64 -0
  42. data/lib/dry/schema/message_compiler.rb +55 -19
  43. data/lib/dry/schema/message_compiler/visitor_opts.rb +2 -2
  44. data/lib/dry/schema/message_set.rb +25 -36
  45. data/lib/dry/schema/messages.rb +6 -6
  46. data/lib/dry/schema/messages/abstract.rb +54 -56
  47. data/lib/dry/schema/messages/i18n.rb +29 -27
  48. data/lib/dry/schema/messages/namespaced.rb +12 -2
  49. data/lib/dry/schema/messages/template.rb +19 -44
  50. data/lib/dry/schema/messages/yaml.rb +60 -13
  51. data/lib/dry/schema/params.rb +1 -1
  52. data/lib/dry/schema/path.rb +44 -5
  53. data/lib/dry/schema/predicate.rb +4 -2
  54. data/lib/dry/schema/predicate_inferrer.rb +4 -184
  55. data/lib/dry/schema/predicate_registry.rb +2 -2
  56. data/lib/dry/schema/primitive_inferrer.rb +16 -0
  57. data/lib/dry/schema/processor.rb +49 -28
  58. data/lib/dry/schema/processor_steps.rb +50 -27
  59. data/lib/dry/schema/result.rb +44 -6
  60. data/lib/dry/schema/rule_applier.rb +7 -7
  61. data/lib/dry/schema/step.rb +79 -0
  62. data/lib/dry/schema/trace.rb +5 -4
  63. data/lib/dry/schema/type_container.rb +3 -3
  64. data/lib/dry/schema/type_registry.rb +2 -2
  65. data/lib/dry/schema/types.rb +1 -1
  66. data/lib/dry/schema/value_coercer.rb +2 -2
  67. data/lib/dry/schema/version.rb +1 -1
  68. metadata +22 -8
@@ -3,6 +3,8 @@ en:
3
3
  or: "or"
4
4
 
5
5
  errors:
6
+ unexpected_key: "is not allowed"
7
+
6
8
  array?: "must be an array"
7
9
 
8
10
  empty?: "must be empty"
@@ -101,5 +103,7 @@ en:
101
103
  default: "must be %{size} bytes long"
102
104
  range: "must be within %{size_left} - %{size_right} bytes long"
103
105
 
106
+ uuid_v4?: "is not a valid UUID"
107
+
104
108
  not:
105
109
  empty?: "cannot be empty"
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ # this file is managed by dry-rb/devtools project
3
+
4
+ lib = File.expand_path('lib', __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'dry/schema/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'dry-schema'
10
+ spec.authors = ["Piotr Solnica"]
11
+ spec.email = ["piotr.solnica@gmail.com"]
12
+ spec.license = 'MIT'
13
+ spec.version = Dry::Schema::VERSION.dup
14
+
15
+ spec.summary = "Coercion and validation for data structures"
16
+ spec.description = <<~TEXT
17
+ dry-schema provides a DSL for defining schemas with keys and rules that should be applied to
18
+ values. It supports coercion, input sanitization, custom types and localized error messages
19
+ (with or without I18n gem). It's also used as the schema engine in dry-validation.
20
+ TEXT
21
+ spec.homepage = 'https://dry-rb.org/gems/dry-schema'
22
+ spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-schema.gemspec", "lib/**/*", "config/*.yml"]
23
+ spec.bindir = 'bin'
24
+ spec.executables = []
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
28
+ spec.metadata['changelog_uri'] = 'https://github.com/dry-rb/dry-schema/blob/master/CHANGELOG.md'
29
+ spec.metadata['source_code_uri'] = 'https://github.com/dry-rb/dry-schema'
30
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/dry-rb/dry-schema/issues'
31
+
32
+ spec.required_ruby_version = ">= 2.4.0"
33
+
34
+ # to update dependencies edit project.yml
35
+ spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
36
+ spec.add_runtime_dependency "dry-configurable", "~> 0.8", ">= 0.8.3"
37
+ spec.add_runtime_dependency "dry-core", "~> 0.4"
38
+ spec.add_runtime_dependency "dry-equalizer", "~> 0.2"
39
+ spec.add_runtime_dependency "dry-initializer", "~> 3.0"
40
+ spec.add_runtime_dependency "dry-logic", "~> 1.0"
41
+ spec.add_runtime_dependency "dry-types", "~> 1.4"
42
+
43
+ spec.add_development_dependency "bundler"
44
+ spec.add_development_dependency "rake"
45
+ spec.add_development_dependency "rspec"
46
+ end
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema'
3
+ require "dry/schema"
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/extensions'
3
+ require "dry/core/extensions"
4
4
 
5
- require 'dry/schema/constants'
6
- require 'dry/schema/dsl'
7
- require 'dry/schema/params'
8
- require 'dry/schema/json'
5
+ require "dry/schema/config"
6
+ require "dry/schema/constants"
7
+ require "dry/schema/dsl"
8
+ require "dry/schema/params"
9
+ require "dry/schema/json"
9
10
 
10
11
  module Dry
11
12
  # Main interface
@@ -14,6 +15,18 @@ module Dry
14
15
  module Schema
15
16
  extend Dry::Core::Extensions
16
17
 
18
+ # Configuration
19
+ #
20
+ # @example
21
+ # Dry::Schema.config.messages.backend = :i18n
22
+ #
23
+ # @return [Config]
24
+ #
25
+ # @api public
26
+ def self.config
27
+ @config ||= Config.new
28
+ end
29
+
17
30
  # Define a schema
18
31
  #
19
32
  # @example
@@ -74,4 +87,4 @@ module Dry
74
87
  end
75
88
  end
76
89
 
77
- require 'dry/schema/extensions'
90
+ require "dry/schema/extensions"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/logic/rule_compiler'
4
- require 'dry/schema/namespaced_rule'
5
- require 'dry/schema/predicate_registry'
3
+ require "dry/logic/rule_compiler"
4
+ require "dry/schema/namespaced_rule"
5
+ require "dry/schema/predicate_registry"
6
6
 
7
7
  module Dry
8
8
  module Schema
@@ -30,12 +30,12 @@ module Dry
30
30
  # used as nested schemas
31
31
  #
32
32
  # @param [Array] node
33
- # @param [Hash] opts
33
+ # @param [Hash] _opts Unused
34
34
  #
35
35
  # @return [NamespacedRule]
36
36
  #
37
37
  # @api private
38
- def visit_namespace(node, opts = EMPTY_HASH)
38
+ def visit_namespace(node, _opts = EMPTY_HASH)
39
39
  namespace, rest = node
40
40
  NamespacedRule.new(namespace, visit(rest))
41
41
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/equalizer'
4
- require 'dry/configurable'
3
+ require "dry/equalizer"
4
+ require "dry/configurable"
5
5
 
6
- require 'dry/schema/constants'
7
- require 'dry/schema/predicate_registry'
8
- require 'dry/schema/type_container'
6
+ require "dry/schema/constants"
7
+ require "dry/schema/predicate_registry"
8
+ require "dry/schema/type_container"
9
9
 
10
10
  module Dry
11
11
  module Schema
@@ -51,6 +51,15 @@ module Dry
51
51
  setting(:default_locale, nil)
52
52
  end
53
53
 
54
+ # @!method validate_keys
55
+ #
56
+ # On/off switch for key validator
57
+ #
58
+ # @return [Boolean]
59
+ #
60
+ # @api public
61
+ setting(:validate_keys, false)
62
+
54
63
  # @api private
55
64
  def respond_to_missing?(meth, include_private = false)
56
65
  super || config.respond_to?(meth, include_private)
@@ -58,7 +67,7 @@ module Dry
58
67
 
59
68
  # @api private
60
69
  def inspect
61
- "#<#{self.class} #{to_h.map { |k, v| ["#{k}=", v.inspect] }.map(&:join).join(' ')}>"
70
+ "#<#{self.class} #{to_h.map { |k, v| ["#{k}=", v.inspect] }.map(&:join).join(" ")}>"
62
71
  end
63
72
 
64
73
  private
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pathname'
4
- require 'dry/core/constants'
3
+ require "pathname"
4
+ require "dry/core/constants"
5
5
 
6
6
  module Dry
7
7
  # Common constants used across the library
@@ -10,15 +10,24 @@ module Dry
10
10
  module Schema
11
11
  include Core::Constants
12
12
 
13
- LIST_SEPARATOR = ', '
14
- QUESTION_MARK = '?'
15
- DOT = '.'
13
+ LIST_SEPARATOR = ", "
14
+ QUESTION_MARK = "?"
15
+ DOT = "."
16
+
17
+ # core processor steps in the default execution order
18
+ STEPS_IN_ORDER = %i[
19
+ key_validator
20
+ key_coercer
21
+ filter_schema
22
+ value_coercer
23
+ rule_applier
24
+ ].freeze
16
25
 
17
26
  # Path to the default set of localized messages bundled within the gem
18
- DEFAULT_MESSAGES_PATH = Pathname(__dir__).join('../../../config/errors.yml').realpath.freeze
27
+ DEFAULT_MESSAGES_PATH = Pathname(__dir__).join("../../../config/errors.yml").realpath.freeze
19
28
 
20
29
  # Default namespace used for localized messages in YAML files
21
- DEFAULT_MESSAGES_ROOT = 'dry_schema'
30
+ DEFAULT_MESSAGES_ROOT = "dry_schema"
22
31
 
23
32
  # An error raised when DSL is used in an incorrect way
24
33
  InvalidSchemaError = Class.new(StandardError)
@@ -1,19 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/initializer'
4
-
5
- require 'dry/schema/constants'
6
- require 'dry/schema/config'
7
- require 'dry/schema/compiler'
8
- require 'dry/schema/types'
9
- require 'dry/schema/macros'
10
-
11
- require 'dry/schema/processor'
12
- require 'dry/schema/processor_steps'
13
- require 'dry/schema/key_map'
14
- require 'dry/schema/key_coercer'
15
- require 'dry/schema/value_coercer'
16
- require 'dry/schema/rule_applier'
3
+ require "dry/initializer"
4
+
5
+ require "dry/schema/constants"
6
+ require "dry/schema/path"
7
+ require "dry/schema/config"
8
+ require "dry/schema/compiler"
9
+ require "dry/schema/types"
10
+ require "dry/schema/macros"
11
+
12
+ require "dry/schema/processor"
13
+ require "dry/schema/processor_steps"
14
+ require "dry/schema/key_map"
15
+ require "dry/schema/key_coercer"
16
+ require "dry/schema/key_validator"
17
+ require "dry/schema/value_coercer"
18
+ require "dry/schema/rule_applier"
17
19
 
18
20
  module Dry
19
21
  module Schema
@@ -67,10 +69,13 @@ module Dry
67
69
  option :parent, Types::Coercible::Array, default: -> { EMPTY_ARRAY.dup }, as: :parents
68
70
 
69
71
  # @return [Config] Configuration object exposed via `#configure` method
70
- option :config, optional: true, default: proc { parent ? parent.config.dup : Config.new }
72
+ option :config, optional: true, default: proc { default_config }
71
73
 
72
74
  # @return [ProcessorSteps] Steps for the processor
73
- option :steps, default: proc { parent ? parent.steps.dup : ProcessorSteps.new }
75
+ option :steps, default: proc { ProcessorSteps.new }
76
+
77
+ # @return [Path, Array] Path under which the schema is defined
78
+ option :path, -> *args { Path[*args] if args.any? }, default: proc { EMPTY_ARRAY }
74
79
 
75
80
  # Build a new DSL object and evaluate provided block
76
81
  #
@@ -171,7 +176,7 @@ module Dry
171
176
  def key(name, macro:, &block)
172
177
  raise ArgumentError, "Key +#{name}+ is not a symbol" unless name.is_a?(::Symbol)
173
178
 
174
- set_type(name, Types::Any)
179
+ set_type(name, Types::Any.meta(default: true))
175
180
 
176
181
  macro = macro.new(
177
182
  name: name,
@@ -191,12 +196,31 @@ module Dry
191
196
  #
192
197
  # @api private
193
198
  def call
194
- steps[:key_coercer] = key_coercer
195
- steps[:value_coercer] = value_coercer
196
- steps[:rule_applier] = rule_applier
197
- steps[:filter_schema] = filter_schema.rule_applier if filter_rules?
199
+ all_steps = parents.map(&:steps) + [steps]
200
+
201
+ result_steps = all_steps.inject { |result, steps| result.merge(steps) }
202
+
203
+ result_steps[:key_validator] = key_validator if config.validate_keys
204
+ result_steps[:key_coercer] = key_coercer
205
+ result_steps[:value_coercer] = value_coercer
206
+ result_steps[:rule_applier] = rule_applier
207
+ result_steps[:filter_schema] = filter_schema.rule_applier if filter_rules?
208
+
209
+ processor_type.new(schema_dsl: self, steps: result_steps)
210
+ end
198
211
 
199
- processor_type.new(schema_dsl: self, steps: steps)
212
+ # Merge with another dsl
213
+ #
214
+ # @return [DSL]
215
+ #
216
+ # @api private
217
+ def merge(other)
218
+ new(
219
+ parent: parents + other.parents,
220
+ macros: macros + other.macros,
221
+ types: types.merge(other.types),
222
+ steps: steps.merge(other.steps)
223
+ )
200
224
  end
201
225
 
202
226
  # Cast this DSL into a rule object
@@ -215,7 +239,7 @@ module Dry
215
239
  #
216
240
  # @api public
217
241
  def array
218
- -> member_type { type_registry['array'].of(resolve_type(member_type)) }
242
+ -> member_type { type_registry["array"].of(resolve_type(member_type)) }
219
243
  end
220
244
 
221
245
  # Method allows steps injection to the processor
@@ -263,7 +287,7 @@ module Dry
263
287
  #
264
288
  # @api private
265
289
  def type_schema
266
- our_schema = type_registry['hash'].schema(types).lax
290
+ our_schema = type_registry["hash"].schema(types).lax
267
291
  schemas = [*parents.map(&:type_schema), our_schema]
268
292
  schemas.inject { |result, schema| result.schema(schema.to_a) }
269
293
  end
@@ -287,11 +311,20 @@ module Dry
287
311
  # @api private
288
312
  def set_type(name, spec)
289
313
  type = resolve_type(spec)
290
- meta = { required: false, maybe: type.optional? }
314
+ meta = {required: false, maybe: type.optional?}
291
315
 
292
316
  types[name] = type.meta(meta)
293
317
  end
294
318
 
319
+ # Check if a custom type was set under provided key name
320
+ #
321
+ # @return [Bool]
322
+ #
323
+ # @api private
324
+ def custom_type?(name)
325
+ !types[name].meta[:default].equal?(true)
326
+ end
327
+
295
328
  # Resolve type object from the provided spec
296
329
  #
297
330
  # @param [Symbol, Array<Symbol>, Dry::Types::Type] spec
@@ -326,7 +359,7 @@ module Dry
326
359
  #
327
360
  # @api private
328
361
  def filter_rules?
329
- if instance_variable_defined?('@filter_schema_dsl') && !filter_schema_dsl.macros.empty?
362
+ if instance_variable_defined?("@filter_schema_dsl") && !filter_schema_dsl.macros.empty?
330
363
  return true
331
364
  end
332
365
 
@@ -374,6 +407,15 @@ module Dry
374
407
  parents.select(&:filter_rules?).map(&:filter_schema)
375
408
  end
376
409
 
410
+ # Build a key validator
411
+ #
412
+ # @return [KeyValidator]
413
+ #
414
+ # @api private
415
+ def key_validator
416
+ KeyValidator.new(key_map: key_map + parent_key_map)
417
+ end
418
+
377
419
  # Build a key coercer
378
420
  #
379
421
  # @return [KeyCoercer]
@@ -411,13 +453,19 @@ module Dry
411
453
 
412
454
  # Build a key spec needed by the key map
413
455
  #
456
+ # TODO: we need a key-map compiler using Types AST
457
+ #
414
458
  # @api private
415
459
  def key_spec(name, type)
416
460
  if type.respond_to?(:keys)
417
- { name => key_map(type.name_key_map) }
461
+ {name => key_map(type.name_key_map)}
418
462
  elsif type.respond_to?(:member)
419
463
  kv = key_spec(name, type.member)
420
464
  kv.equal?(name) ? name : kv.flatten(1)
465
+ elsif type.meta[:maybe] && type.respond_to?(:right)
466
+ key_spec(name, type.right)
467
+ elsif type.respond_to?(:type)
468
+ key_spec(name, type.type)
421
469
  else
422
470
  name
423
471
  end
@@ -432,6 +480,18 @@ module Dry
432
480
  def parent_key_map
433
481
  parents.reduce([]) { |key_map, parent| parent.key_map + key_map }
434
482
  end
483
+
484
+ # @api private
485
+ def default_config
486
+ parents.each_cons(2) do |left, right|
487
+ unless left.config == right.config
488
+ raise ArgumentError,
489
+ "Parent configs differ, left=#{left.inspect}, right=#{right.inspect}"
490
+ end
491
+ end
492
+
493
+ (parent || Schema).config.dup
494
+ end
435
495
  end
436
496
  end
437
497
  end
@@ -1,9 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Dry::Schema.register_extension(:monads) do
4
- require 'dry/schema/extensions/monads'
4
+ require "dry/schema/extensions/monads"
5
5
  end
6
6
 
7
7
  Dry::Schema.register_extension(:hints) do
8
- require 'dry/schema/extensions/hints'
8
+ require "dry/schema/extensions/hints"
9
+ end
10
+
11
+ Dry::Schema.register_extension(:struct) do
12
+ require "dry/schema/extensions/struct"
13
+ end
14
+
15
+ Dry::Schema.register_extension(:info) do
16
+ require "dry/schema/extensions/info"
9
17
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/compiler'
4
- require 'dry/schema/message'
5
- require 'dry/schema/message_compiler'
3
+ require "dry/schema/compiler"
4
+ require "dry/schema/message"
5
+ require "dry/schema/message_compiler"
6
6
 
7
- require 'dry/schema/extensions/hints/compiler_methods'
8
- require 'dry/schema/extensions/hints/message_compiler_methods'
9
- require 'dry/schema/extensions/hints/message_set_methods'
10
- require 'dry/schema/extensions/hints/result_methods'
7
+ require "dry/schema/extensions/hints/compiler_methods"
8
+ require "dry/schema/extensions/hints/message_compiler_methods"
9
+ require "dry/schema/extensions/hints/message_set_methods"
10
+ require "dry/schema/extensions/hints/result_methods"
11
11
 
12
12
  module Dry
13
13
  module Schema
@@ -22,7 +22,14 @@ module Dry
22
22
  # @see Message::Or
23
23
  #
24
24
  # @api public
25
- class Or
25
+ class Or::SinglePath
26
+ # @api private
27
+ def hint?
28
+ false
29
+ end
30
+ end
31
+
32
+ class Or::MultiPath
26
33
  # @api private
27
34
  def hint?
28
35
  false