dry-schema 1.4.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +170 -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 +4 -4
  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 +88 -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 +66 -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 +37 -17
  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 +2 -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 +43 -5
  60. data/lib/dry/schema/rule_applier.rb +8 -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
@@ -35,7 +35,7 @@ module Dry
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,22 @@
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"
6
+ require "dry/schema/constants"
7
+ require "dry/schema/path"
8
+ require "dry/schema/config"
9
+ require "dry/schema/compiler"
10
+ require "dry/schema/types"
11
+ require "dry/schema/macros"
12
+
13
+ require "dry/schema/processor"
14
+ require "dry/schema/processor_steps"
15
+ require "dry/schema/key_map"
16
+ require "dry/schema/key_coercer"
17
+ require "dry/schema/key_validator"
18
+ require "dry/schema/value_coercer"
19
+ require "dry/schema/rule_applier"
17
20
 
18
21
  module Dry
19
22
  module Schema
@@ -67,10 +70,13 @@ module Dry
67
70
  option :parent, Types::Coercible::Array, default: -> { EMPTY_ARRAY.dup }, as: :parents
68
71
 
69
72
  # @return [Config] Configuration object exposed via `#configure` method
70
- option :config, optional: true, default: proc { parent ? parent.config.dup : Config.new }
73
+ option :config, optional: true, default: proc { default_config }
71
74
 
72
75
  # @return [ProcessorSteps] Steps for the processor
73
- option :steps, default: proc { parent ? parent.steps.dup : ProcessorSteps.new }
76
+ option :steps, default: proc { ProcessorSteps.new }
77
+
78
+ # @return [Path, Array] Path under which the schema is defined
79
+ option :path, -> *args { Path[*args] if args.any? }, default: proc { EMPTY_ARRAY }
74
80
 
75
81
  # Build a new DSL object and evaluate provided block
76
82
  #
@@ -171,7 +177,7 @@ module Dry
171
177
  def key(name, macro:, &block)
172
178
  raise ArgumentError, "Key +#{name}+ is not a symbol" unless name.is_a?(::Symbol)
173
179
 
174
- set_type(name, Types::Any)
180
+ set_type(name, Types::Any.meta(default: true))
175
181
 
176
182
  macro = macro.new(
177
183
  name: name,
@@ -191,12 +197,31 @@ module Dry
191
197
  #
192
198
  # @api private
193
199
  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?
200
+ all_steps = parents.map(&:steps) + [steps]
201
+
202
+ result_steps = all_steps.inject { |result, steps| result.merge(steps) }
203
+
204
+ result_steps[:key_validator] = key_validator if config.validate_keys
205
+ result_steps[:key_coercer] = key_coercer
206
+ result_steps[:value_coercer] = value_coercer
207
+ result_steps[:rule_applier] = rule_applier
208
+ result_steps[:filter_schema] = filter_schema.rule_applier if filter_rules?
209
+
210
+ processor_type.new(schema_dsl: self, steps: result_steps)
211
+ end
198
212
 
199
- processor_type.new(schema_dsl: self, steps: steps)
213
+ # Merge with another dsl
214
+ #
215
+ # @return [DSL]
216
+ #
217
+ # @api private
218
+ def merge(other)
219
+ new(
220
+ parent: parents + other.parents,
221
+ macros: macros + other.macros,
222
+ types: types.merge(other.types),
223
+ steps: steps.merge(other.steps)
224
+ )
200
225
  end
201
226
 
202
227
  # Cast this DSL into a rule object
@@ -215,7 +240,7 @@ module Dry
215
240
  #
216
241
  # @api public
217
242
  def array
218
- -> member_type { type_registry['array'].of(resolve_type(member_type)) }
243
+ -> member_type { type_registry["array"].of(resolve_type(member_type)) }
219
244
  end
220
245
 
221
246
  # Method allows steps injection to the processor
@@ -263,7 +288,7 @@ module Dry
263
288
  #
264
289
  # @api private
265
290
  def type_schema
266
- our_schema = type_registry['hash'].schema(types).lax
291
+ our_schema = type_registry["hash"].schema(types).lax
267
292
  schemas = [*parents.map(&:type_schema), our_schema]
268
293
  schemas.inject { |result, schema| result.schema(schema.to_a) }
269
294
  end
@@ -287,11 +312,20 @@ module Dry
287
312
  # @api private
288
313
  def set_type(name, spec)
289
314
  type = resolve_type(spec)
290
- meta = { required: false, maybe: type.optional? }
315
+ meta = {required: false, maybe: type.optional?}
291
316
 
292
317
  types[name] = type.meta(meta)
293
318
  end
294
319
 
320
+ # Check if a custom type was set under provided key name
321
+ #
322
+ # @return [Bool]
323
+ #
324
+ # @api private
325
+ def custom_type?(name)
326
+ !types[name].meta[:default].equal?(true)
327
+ end
328
+
295
329
  # Resolve type object from the provided spec
296
330
  #
297
331
  # @param [Symbol, Array<Symbol>, Dry::Types::Type] spec
@@ -326,7 +360,7 @@ module Dry
326
360
  #
327
361
  # @api private
328
362
  def filter_rules?
329
- if instance_variable_defined?('@filter_schema_dsl') && !filter_schema_dsl.macros.empty?
363
+ if instance_variable_defined?("@filter_schema_dsl") && !filter_schema_dsl.macros.empty?
330
364
  return true
331
365
  end
332
366
 
@@ -374,6 +408,15 @@ module Dry
374
408
  parents.select(&:filter_rules?).map(&:filter_schema)
375
409
  end
376
410
 
411
+ # Build a key validator
412
+ #
413
+ # @return [KeyValidator]
414
+ #
415
+ # @api private
416
+ def key_validator
417
+ KeyValidator.new(key_map: key_map + parent_key_map)
418
+ end
419
+
377
420
  # Build a key coercer
378
421
  #
379
422
  # @return [KeyCoercer]
@@ -411,13 +454,19 @@ module Dry
411
454
 
412
455
  # Build a key spec needed by the key map
413
456
  #
457
+ # TODO: we need a key-map compiler using Types AST
458
+ #
414
459
  # @api private
415
460
  def key_spec(name, type)
416
461
  if type.respond_to?(:keys)
417
- { name => key_map(type.name_key_map) }
462
+ {name => key_map(type.name_key_map)}
418
463
  elsif type.respond_to?(:member)
419
464
  kv = key_spec(name, type.member)
420
465
  kv.equal?(name) ? name : kv.flatten(1)
466
+ elsif type.meta[:maybe] && type.respond_to?(:right)
467
+ key_spec(name, type.right)
468
+ elsif type.respond_to?(:type)
469
+ key_spec(name, type.type)
421
470
  else
422
471
  name
423
472
  end
@@ -432,6 +481,18 @@ module Dry
432
481
  def parent_key_map
433
482
  parents.reduce([]) { |key_map, parent| parent.key_map + key_map }
434
483
  end
484
+
485
+ # @api private
486
+ def default_config
487
+ parents.each_cons(2) do |left, right|
488
+ unless left.config == right.config
489
+ raise ArgumentError,
490
+ "Parent configs differ, left=#{left.inspect}, right=#{right.inspect}"
491
+ end
492
+ end
493
+
494
+ (parent || Schema).config.dup
495
+ end
435
496
  end
436
497
  end
437
498
  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