dry-schema 1.4.1 → 1.5.2

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 +210 -73
  3. data/LICENSE +1 -1
  4. data/README.md +4 -6
  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 +20 -7
  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 +89 -31
  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 +2 -2
  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 +20 -5
  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 +11 -6
  29. data/lib/dry/schema/macros/dsl.rb +44 -23
  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 +10 -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 +58 -22
  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 -62
  49. data/lib/dry/schema/messages/i18n.rb +36 -10
  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 +50 -29
  60. data/lib/dry/schema/processor_steps.rb +50 -27
  61. data/lib/dry/schema/result.rb +53 -6
  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) 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,17 +1,15 @@
1
1
  [gem]: https://rubygems.org/gems/dry-schema
2
- [travis]: https://travis-ci.com/dry-rb/dry-schema
3
2
  [actions]: https://github.com/dry-rb/dry-schema/actions
4
- [codeclimate]: https://codeclimate.com/github/dry-rb/dry-schema
3
+ [codacy]: https://www.codacy.com/gh/dry-rb/dry-schema
5
4
  [chat]: https://dry-rb.zulipchat.com
6
5
  [inchpages]: http://inch-ci.org/github/dry-rb/dry-schema
7
6
 
8
7
  # dry-schema [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
9
8
 
10
9
  [![Gem Version](https://badge.fury.io/rb/dry-schema.svg)][gem]
11
- [![Travis Status](https://travis-ci.com/dry-rb/dry-schema.svg?branch=master)][travis]
12
10
  [![CI Status](https://github.com/dry-rb/dry-schema/workflows/ci/badge.svg)][actions]
13
- [![Code Climate](https://codeclimate.com/github/dry-rb/dry-schema/badges/gpa.svg)][codeclimate]
14
- [![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]
15
13
  [![Inline docs](http://inch-ci.org/github/dry-rb/dry-schema.svg?branch=master)][inchpages]
16
14
 
17
15
  ## Links
@@ -21,7 +19,7 @@
21
19
 
22
20
  ## Supported Ruby versions
23
21
 
24
- This library officially supports following Ruby versions:
22
+ This library officially supports the following Ruby versions:
25
23
 
26
24
  * MRI >= `2.4`
27
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
@@ -30,7 +43,7 @@ module Dry
30
43
  #
31
44
  # @api public
32
45
  def self.define(**options, &block)
33
- DSL.new(options, &block).call
46
+ DSL.new(**options, &block).call
34
47
  end
35
48
 
36
49
  # Define a schema suitable for HTTP params
@@ -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
@@ -51,8 +53,6 @@ module Dry
51
53
 
52
54
  extend Dry::Initializer
53
55
 
54
- include ::Dry::Equalizer(:options)
55
-
56
56
  # @return [Compiler] The rule compiler object
57
57
  option :compiler, default: -> { Compiler.new }
58
58
 
@@ -69,10 +69,13 @@ module Dry
69
69
  option :parent, Types::Coercible::Array, default: -> { EMPTY_ARRAY.dup }, as: :parents
70
70
 
71
71
  # @return [Config] Configuration object exposed via `#configure` method
72
- option :config, optional: true, default: proc { parent ? parent.config.dup : Config.new }
72
+ option :config, optional: true, default: proc { default_config }
73
73
 
74
74
  # @return [ProcessorSteps] Steps for the processor
75
- 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 }
76
79
 
77
80
  # Build a new DSL object and evaluate provided block
78
81
  #
@@ -173,7 +176,7 @@ module Dry
173
176
  def key(name, macro:, &block)
174
177
  raise ArgumentError, "Key +#{name}+ is not a symbol" unless name.is_a?(::Symbol)
175
178
 
176
- set_type(name, Types::Any)
179
+ set_type(name, Types::Any.meta(default: true))
177
180
 
178
181
  macro = macro.new(
179
182
  name: name,
@@ -193,12 +196,31 @@ module Dry
193
196
  #
194
197
  # @api private
195
198
  def call
196
- steps[:key_coercer] = key_coercer
197
- steps[:value_coercer] = value_coercer
198
- steps[:rule_applier] = rule_applier
199
- steps[:filter_schema] = filter_schema.rule_applier if filter_rules?
199
+ all_steps = parents.map(&:steps) + [steps]
200
200
 
201
- processor_type.new(schema_dsl: self, steps: steps)
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
211
+
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
+ )
202
224
  end
203
225
 
204
226
  # Cast this DSL into a rule object
@@ -217,7 +239,7 @@ module Dry
217
239
  #
218
240
  # @api public
219
241
  def array
220
- -> member_type { type_registry['array'].of(resolve_type(member_type)) }
242
+ -> member_type { type_registry["array"].of(resolve_type(member_type)) }
221
243
  end
222
244
 
223
245
  # Method allows steps injection to the processor
@@ -265,7 +287,7 @@ module Dry
265
287
  #
266
288
  # @api private
267
289
  def type_schema
268
- our_schema = type_registry['hash'].schema(types).lax
290
+ our_schema = type_registry["hash"].schema(types).lax
269
291
  schemas = [*parents.map(&:type_schema), our_schema]
270
292
  schemas.inject { |result, schema| result.schema(schema.to_a) }
271
293
  end
@@ -275,8 +297,8 @@ module Dry
275
297
  # @return [Dry::Types::Safe]
276
298
  #
277
299
  # @api private
278
- def new(options = EMPTY_HASH, &block)
279
- self.class.new(options.merge(processor_type: processor_type, config: config), &block)
300
+ def new(**options, &block)
301
+ self.class.new(**options, processor_type: processor_type, config: config, &block)
280
302
  end
281
303
 
282
304
  # Set a type for the given key name
@@ -289,11 +311,20 @@ module Dry
289
311
  # @api private
290
312
  def set_type(name, spec)
291
313
  type = resolve_type(spec)
292
- meta = { required: false, maybe: type.optional? }
314
+ meta = {required: false, maybe: type.optional?}
293
315
 
294
316
  types[name] = type.meta(meta)
295
317
  end
296
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
+
297
328
  # Resolve type object from the provided spec
298
329
  #
299
330
  # @param [Symbol, Array<Symbol>, Dry::Types::Type] spec
@@ -328,7 +359,7 @@ module Dry
328
359
  #
329
360
  # @api private
330
361
  def filter_rules?
331
- 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?
332
363
  return true
333
364
  end
334
365
 
@@ -376,6 +407,15 @@ module Dry
376
407
  parents.select(&:filter_rules?).map(&:filter_schema)
377
408
  end
378
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
+
379
419
  # Build a key coercer
380
420
  #
381
421
  # @return [KeyCoercer]
@@ -413,13 +453,19 @@ module Dry
413
453
 
414
454
  # Build a key spec needed by the key map
415
455
  #
456
+ # TODO: we need a key-map compiler using Types AST
457
+ #
416
458
  # @api private
417
459
  def key_spec(name, type)
418
460
  if type.respond_to?(:keys)
419
- { name => key_map(type.name_key_map) }
461
+ {name => key_map(type.name_key_map)}
420
462
  elsif type.respond_to?(:member)
421
463
  kv = key_spec(name, type.member)
422
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)
423
469
  else
424
470
  name
425
471
  end
@@ -434,6 +480,18 @@ module Dry
434
480
  def parent_key_map
435
481
  parents.reduce([]) { |key_map, parent| parent.key_map + key_map }
436
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
437
495
  end
438
496
  end
439
497
  end