dry-schema 1.4.2 → 1.5.3

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +217 -78
  3. data/LICENSE +1 -1
  4. data/README.md +4 -4
  5. data/config/errors.yml +4 -0
  6. data/dry-schema.gemspec +46 -0
  7. data/lib/dry-schema.rb +1 -1
  8. data/lib/dry/schema.rb +19 -6
  9. data/lib/dry/schema/compiler.rb +5 -5
  10. data/lib/dry/schema/config.rb +15 -6
  11. data/lib/dry/schema/constants.rb +16 -7
  12. data/lib/dry/schema/dsl.rb +87 -27
  13. data/lib/dry/schema/extensions.rb +10 -2
  14. data/lib/dry/schema/extensions/hints.rb +15 -8
  15. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +1 -1
  16. data/lib/dry/schema/extensions/hints/message_set_methods.rb +0 -47
  17. data/lib/dry/schema/extensions/info.rb +27 -0
  18. data/lib/dry/schema/extensions/info/schema_compiler.rb +105 -0
  19. data/lib/dry/schema/extensions/monads.rb +1 -1
  20. data/lib/dry/schema/extensions/struct.rb +32 -0
  21. data/lib/dry/schema/json.rb +1 -1
  22. data/lib/dry/schema/key.rb +16 -1
  23. data/lib/dry/schema/key_coercer.rb +4 -4
  24. data/lib/dry/schema/key_map.rb +9 -4
  25. data/lib/dry/schema/key_validator.rb +67 -0
  26. data/lib/dry/schema/macros.rb +8 -8
  27. data/lib/dry/schema/macros/array.rb +17 -4
  28. data/lib/dry/schema/macros/core.rb +9 -4
  29. data/lib/dry/schema/macros/dsl.rb +34 -19
  30. data/lib/dry/schema/macros/each.rb +4 -4
  31. data/lib/dry/schema/macros/filled.rb +5 -5
  32. data/lib/dry/schema/macros/hash.rb +21 -3
  33. data/lib/dry/schema/macros/key.rb +9 -9
  34. data/lib/dry/schema/macros/maybe.rb +4 -5
  35. data/lib/dry/schema/macros/optional.rb +1 -1
  36. data/lib/dry/schema/macros/required.rb +1 -1
  37. data/lib/dry/schema/macros/schema.rb +23 -2
  38. data/lib/dry/schema/macros/value.rb +34 -7
  39. data/lib/dry/schema/message.rb +35 -9
  40. data/lib/dry/schema/message/or.rb +18 -39
  41. data/lib/dry/schema/message/or/abstract.rb +28 -0
  42. data/lib/dry/schema/message/or/multi_path.rb +37 -0
  43. data/lib/dry/schema/message/or/single_path.rb +64 -0
  44. data/lib/dry/schema/message_compiler.rb +55 -19
  45. data/lib/dry/schema/message_compiler/visitor_opts.rb +2 -2
  46. data/lib/dry/schema/message_set.rb +26 -37
  47. data/lib/dry/schema/messages.rb +6 -6
  48. data/lib/dry/schema/messages/abstract.rb +54 -56
  49. data/lib/dry/schema/messages/i18n.rb +29 -27
  50. data/lib/dry/schema/messages/namespaced.rb +12 -2
  51. data/lib/dry/schema/messages/template.rb +19 -44
  52. data/lib/dry/schema/messages/yaml.rb +61 -14
  53. data/lib/dry/schema/params.rb +1 -1
  54. data/lib/dry/schema/path.rb +44 -5
  55. data/lib/dry/schema/predicate.rb +4 -2
  56. data/lib/dry/schema/predicate_inferrer.rb +4 -184
  57. data/lib/dry/schema/predicate_registry.rb +2 -2
  58. data/lib/dry/schema/primitive_inferrer.rb +16 -0
  59. data/lib/dry/schema/processor.rb +49 -28
  60. data/lib/dry/schema/processor_steps.rb +50 -27
  61. data/lib/dry/schema/result.rb +52 -5
  62. data/lib/dry/schema/rule_applier.rb +7 -7
  63. data/lib/dry/schema/step.rb +79 -0
  64. data/lib/dry/schema/trace.rb +5 -4
  65. data/lib/dry/schema/type_container.rb +3 -3
  66. data/lib/dry/schema/type_registry.rb +2 -2
  67. data/lib/dry/schema/types.rb +1 -1
  68. data/lib/dry/schema/value_coercer.rb +2 -2
  69. data/lib/dry/schema/version.rb +1 -1
  70. metadata +21 -7
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015-2019 dry-rb team
3
+ Copyright (c) 2015-2020 dry-rb team
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  [gem]: https://rubygems.org/gems/dry-schema
2
2
  [actions]: https://github.com/dry-rb/dry-schema/actions
3
- [codeclimate]: https://codeclimate.com/github/dry-rb/dry-schema
3
+ [codacy]: https://www.codacy.com/gh/dry-rb/dry-schema
4
4
  [chat]: https://dry-rb.zulipchat.com
5
5
  [inchpages]: http://inch-ci.org/github/dry-rb/dry-schema
6
6
 
@@ -8,8 +8,8 @@
8
8
 
9
9
  [![Gem Version](https://badge.fury.io/rb/dry-schema.svg)][gem]
10
10
  [![CI Status](https://github.com/dry-rb/dry-schema/workflows/ci/badge.svg)][actions]
11
- [![Code Climate](https://codeclimate.com/github/dry-rb/dry-schema/badges/gpa.svg)][codeclimate]
12
- [![Test Coverage](https://codeclimate.com/github/dry-rb/dry-schema/badges/coverage.svg)][codeclimate]
11
+ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/961f5c776f1d49218b2cede3745e059c)][codacy]
12
+ [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/961f5c776f1d49218b2cede3745e059c)][codacy]
13
13
  [![Inline docs](http://inch-ci.org/github/dry-rb/dry-schema.svg?branch=master)][inchpages]
14
14
 
15
15
  ## Links
@@ -19,7 +19,7 @@
19
19
 
20
20
  ## Supported Ruby versions
21
21
 
22
- This library officially supports following Ruby versions:
22
+ This library officially supports the following Ruby versions:
23
23
 
24
24
  * MRI >= `2.4`
25
25
  * jruby >= `9.2`
@@ -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