dry-types 0.9.0 → 0.15.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 (61) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +15 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +43 -0
  5. data/.travis.yml +15 -14
  6. data/.yardopts +5 -0
  7. data/CHANGELOG.md +494 -88
  8. data/CONTRIBUTING.md +29 -0
  9. data/Gemfile +7 -6
  10. data/README.md +1 -3
  11. data/Rakefile +8 -3
  12. data/benchmarks/hash_schemas.rb +7 -7
  13. data/dry-types.gemspec +11 -9
  14. data/lib/dry/types/any.rb +36 -0
  15. data/lib/dry/types/array/member.rb +29 -4
  16. data/lib/dry/types/array.rb +6 -4
  17. data/lib/dry/types/builder.rb +48 -6
  18. data/lib/dry/types/builder_methods.rb +111 -0
  19. data/lib/dry/types/coercions/json.rb +3 -0
  20. data/lib/dry/types/coercions/{form.rb → params.rb} +23 -3
  21. data/lib/dry/types/coercions.rb +16 -3
  22. data/lib/dry/types/compat.rb +0 -0
  23. data/lib/dry/types/compiler.rb +66 -39
  24. data/lib/dry/types/constrained/coercible.rb +7 -1
  25. data/lib/dry/types/constrained.rb +42 -3
  26. data/lib/dry/types/constraints.rb +3 -0
  27. data/lib/dry/types/constructor.rb +98 -16
  28. data/lib/dry/types/container.rb +2 -0
  29. data/lib/dry/types/core.rb +30 -14
  30. data/lib/dry/types/decorator.rb +31 -5
  31. data/lib/dry/types/default.rb +34 -8
  32. data/lib/dry/types/enum.rb +71 -14
  33. data/lib/dry/types/errors.rb +23 -6
  34. data/lib/dry/types/extensions/maybe.rb +35 -16
  35. data/lib/dry/types/fn_container.rb +34 -0
  36. data/lib/dry/types/hash/constructor.rb +20 -0
  37. data/lib/dry/types/hash.rb +103 -23
  38. data/lib/dry/types/inflector.rb +7 -0
  39. data/lib/dry/types/json.rb +7 -7
  40. data/lib/dry/types/map.rb +98 -0
  41. data/lib/dry/types/module.rb +115 -0
  42. data/lib/dry/types/nominal.rb +119 -0
  43. data/lib/dry/types/options.rb +29 -7
  44. data/lib/dry/types/params.rb +53 -0
  45. data/lib/dry/types/printable.rb +12 -0
  46. data/lib/dry/types/printer.rb +309 -0
  47. data/lib/dry/types/result.rb +12 -2
  48. data/lib/dry/types/safe.rb +27 -1
  49. data/lib/dry/types/schema/key.rb +130 -0
  50. data/lib/dry/types/schema.rb +298 -0
  51. data/lib/dry/types/spec/types.rb +102 -0
  52. data/lib/dry/types/sum.rb +75 -13
  53. data/lib/dry/types/type.rb +6 -0
  54. data/lib/dry/types/version.rb +1 -1
  55. data/lib/dry/types.rb +104 -38
  56. data/log/.gitkeep +0 -0
  57. metadata +81 -50
  58. data/lib/dry/types/definition.rb +0 -79
  59. data/lib/dry/types/form.rb +0 -53
  60. data/lib/dry/types/hash/schema.rb +0 -156
  61. data/lib/spec/dry/types.rb +0 -56
@@ -3,22 +3,29 @@ require 'dry/types/decorator'
3
3
  module Dry
4
4
  module Types
5
5
  class Default
6
- include Dry::Equalizer(:type, :options, :value)
6
+ include Type
7
7
  include Decorator
8
8
  include Builder
9
+ include Printable
10
+ include Dry::Equalizer(:type, :options, :value, inspect: false)
9
11
 
10
12
  class Callable < Default
11
- include Dry::Equalizer(:type, :options)
13
+ include Dry::Equalizer(:type, :options, inspect: false)
12
14
 
15
+ # Evaluates given callable
16
+ # @return [Object]
13
17
  def evaluate
14
- value.call
18
+ value.call(type)
15
19
  end
16
20
  end
17
21
 
22
+ # @return [Object]
18
23
  attr_reader :value
19
24
 
20
25
  alias_method :evaluate, :value
21
26
 
27
+ # @param [Object, #call] value
28
+ # @return [Class] {Default} or {Default::Callable}
22
29
  def self.[](value)
23
30
  if value.respond_to?(:call)
24
31
  Callable
@@ -27,32 +34,51 @@ module Dry
27
34
  end
28
35
  end
29
36
 
30
- def initialize(type, value, *)
37
+ # @param [Type] type
38
+ # @param [Object] value
39
+ def initialize(type, value, **options)
31
40
  super
32
41
  @value = value
33
42
  end
34
43
 
44
+ # @param [Array] args see {Dry::Types::Builder#constrained}
45
+ # @return [Default]
35
46
  def constrained(*args)
36
47
  type.constrained(*args).default(value)
37
48
  end
38
49
 
50
+ # @return [true]
39
51
  def default?
40
52
  true
41
53
  end
42
54
 
55
+ # @param [Object] input
56
+ # @return [Result::Success]
43
57
  def try(input)
44
58
  success(call(input))
45
59
  end
46
60
 
47
- def call(input)
48
- if input.nil?
61
+ def valid?(value = Undefined)
62
+ value.equal?(Undefined) || super
63
+ end
64
+
65
+ # @param [Object] input
66
+ # @return [Object] value passed through {#type} or {#default} value
67
+ def call(input = Undefined)
68
+ if input.equal?(Undefined)
49
69
  evaluate
50
70
  else
51
- output = type[input]
52
- output.nil? ? evaluate : output
71
+ Undefined.default(type[input]) { evaluate }
53
72
  end
54
73
  end
55
74
  alias_method :[], :call
75
+
76
+ private
77
+
78
+ # Replace underlying type
79
+ def __new__(type)
80
+ self.class.new(type, value, options)
81
+ end
56
82
  end
57
83
  end
58
84
  end
@@ -3,29 +3,86 @@ require 'dry/types/decorator'
3
3
  module Dry
4
4
  module Types
5
5
  class Enum
6
- include Dry::Equalizer(:type, :options, :values)
6
+ include Type
7
+ include Dry::Equalizer(:type, :options, :mapping, inspect: false)
7
8
  include Decorator
8
9
 
9
- attr_reader :values, :mapping
10
+ # @return [Array]
11
+ attr_reader :values
10
12
 
13
+ # @return [Hash]
14
+ attr_reader :mapping
15
+
16
+ # @return [Hash]
17
+ attr_reader :inverted_mapping
18
+
19
+ # @param [Type] type
20
+ # @param [Hash] options
21
+ # @option options [Array] :values
11
22
  def initialize(type, options)
12
23
  super
13
- @values = options.fetch(:values).freeze
14
- @values.each(&:freeze)
15
- @mapping = values.each_with_object({}) { |v, h| h[values.index(v)] = v }.freeze
24
+ @mapping = options.fetch(:mapping).freeze
25
+ @values = @mapping.keys.freeze
26
+ @inverted_mapping = @mapping.invert.freeze
27
+ freeze
16
28
  end
17
29
 
18
- def call(input)
19
- value =
20
- if values.include?(input)
21
- input
22
- elsif mapping.key?(input)
23
- mapping[input]
24
- end
25
-
26
- type[value || input]
30
+ # @param [Object] input
31
+ # @return [Object]
32
+ def call(input = Undefined)
33
+ type[map_value(input)]
27
34
  end
28
35
  alias_method :[], :call
36
+
37
+ # @param [Object] input
38
+ # @yieldparam [Failure] failure
39
+ # @yieldreturn [Result]
40
+ # @return [Logic::Result]
41
+ # @return [Object] if coercion fails and a block is given
42
+ def try(input)
43
+ super(map_value(input))
44
+ end
45
+
46
+ def default(*)
47
+ raise '.enum(*values).default(value) is not supported. Call '\
48
+ '.default(value).enum(*values) instead'
49
+ end
50
+
51
+ # Check whether a value is in the enum
52
+ alias_method :include?, :valid?
53
+
54
+ # @api public
55
+ #
56
+ # @see Nominal#to_ast
57
+ def to_ast(meta: true)
58
+ [:enum, [type.to_ast(meta: meta),
59
+ mapping,
60
+ meta ? self.meta : EMPTY_HASH]]
61
+ end
62
+
63
+ # @return [String]
64
+ # @api public
65
+ def to_s
66
+ PRINTER.(self)
67
+ end
68
+ alias_method :inspect, :to_s
69
+
70
+ private
71
+
72
+ # Maps a value
73
+ #
74
+ # @param [Object]
75
+ # @return [Object]
76
+ # @api private
77
+ def map_value(input)
78
+ if input.equal?(Undefined)
79
+ type.call
80
+ elsif mapping.key?(input)
81
+ input
82
+ else
83
+ inverted_mapping.fetch(input, input)
84
+ end
85
+ end
29
86
  end
30
87
  end
31
88
  end
@@ -1,33 +1,49 @@
1
1
  module Dry
2
2
  module Types
3
- extend Dry::Configurable
3
+ extend Dry::Core::ClassAttributes
4
4
 
5
- setting :namespace, self
5
+ # @!attribute [r] namespace
6
+ # @return [Container{String => Nominal}]
7
+ defines :namespace
8
+
9
+ namespace self
6
10
 
7
11
  class SchemaError < TypeError
8
- def initialize(key, value)
9
- super("#{value.inspect} (#{value.class}) has invalid type for :#{key}")
12
+ # @param [String,Symbol] key
13
+ # @param [Object] value
14
+ # @param [String, #to_s] result
15
+ def initialize(key, value, result)
16
+ super("#{value.inspect} (#{value.class}) has invalid type for :#{key} violates constraints (#{result} failed)")
10
17
  end
11
18
  end
12
19
 
20
+ MapError = Class.new(TypeError)
21
+
13
22
  SchemaKeyError = Class.new(KeyError)
14
23
  private_constant(:SchemaKeyError)
15
24
 
16
25
  class MissingKeyError < SchemaKeyError
26
+ # @param [String,Symbol] key
17
27
  def initialize(key)
18
28
  super(":#{key} is missing in Hash input")
19
29
  end
20
30
  end
21
31
 
22
32
  class UnknownKeysError < SchemaKeyError
33
+ # @param [<String, Symbol>] keys
23
34
  def initialize(*keys)
24
35
  super("unexpected keys #{keys.inspect} in Hash input")
25
36
  end
26
37
  end
27
38
 
28
- ConstraintError = Class.new(TypeError) do
29
- attr_reader :result, :input
39
+ class ConstraintError < TypeError
40
+ # @return [String, #to_s]
41
+ attr_reader :result
42
+ # @return [Object]
43
+ attr_reader :input
30
44
 
45
+ # @param [String, #to_s] result
46
+ # @param [Object] input
31
47
  def initialize(result, input)
32
48
  @result = result
33
49
  @input = input
@@ -39,6 +55,7 @@ module Dry
39
55
  end
40
56
  end
41
57
 
58
+ # @return [String]
42
59
  def to_s
43
60
  "#{input.inspect} violates constraints (#{result} failed)"
44
61
  end
@@ -4,24 +4,46 @@ require 'dry/types/decorator'
4
4
  module Dry
5
5
  module Types
6
6
  class Maybe
7
- include Dry::Equalizer(:type, :options)
7
+ include Type
8
+ include Dry::Equalizer(:type, :options, inspect: false)
8
9
  include Decorator
9
10
  include Builder
10
11
  include Dry::Monads::Maybe::Mixin
11
12
 
12
- def call(input)
13
- input.is_a?(Dry::Monads::Maybe) ? input : Maybe(type[input])
13
+ # @param [Dry::Monads::Maybe, Object] input
14
+ # @return [Dry::Monads::Maybe]
15
+ def call(input = Undefined)
16
+ case input
17
+ when Dry::Monads::Maybe
18
+ input
19
+ when Undefined
20
+ None()
21
+ else
22
+ Maybe(type[input])
23
+ end
14
24
  end
15
25
  alias_method :[], :call
16
26
 
17
- def try(input)
18
- Result::Success.new(Maybe(type[input]))
27
+ # @param [Object] input
28
+ # @return [Result::Success]
29
+ def try(input = Undefined)
30
+ res = if input.equal?(Undefined)
31
+ None()
32
+ else
33
+ Maybe(type[input])
34
+ end
35
+
36
+ Result::Success.new(res)
19
37
  end
20
38
 
21
- def maybe?
39
+ # @return [true]
40
+ def default?
22
41
  true
23
42
  end
24
43
 
44
+ # @param [Object] value
45
+ # @see Dry::Types::Builder#default
46
+ # @raise [ArgumentError] if nil provided as default value
25
47
  def default(value)
26
48
  if value.nil?
27
49
  raise ArgumentError, "nil cannot be used as a default of a maybe type"
@@ -32,23 +54,20 @@ module Dry
32
54
  end
33
55
 
34
56
  module Builder
57
+ # @return [Maybe]
35
58
  def maybe
36
59
  Maybe.new(Types['strict.nil'] | self)
37
60
  end
38
61
  end
39
62
 
40
- class Hash
41
- module MaybeTypes
42
- def resolve_missing_value(result, key, type)
43
- if type.respond_to?(:maybe?) && type.maybe?
44
- result[key] = type[nil]
45
- else
46
- super
47
- end
63
+ class Printer
64
+ MAPPING[Maybe] = :visit_maybe
65
+
66
+ def visit_maybe(maybe)
67
+ visit(maybe.type) do |type|
68
+ yield "Maybe<#{ type }>"
48
69
  end
49
70
  end
50
-
51
- Schema.include MaybeTypes
52
71
  end
53
72
 
54
73
  # Register non-coercible maybe types
@@ -0,0 +1,34 @@
1
+ require 'dry/types/container'
2
+
3
+ module Dry
4
+ module Types
5
+ class FnContainer
6
+ # @api private
7
+ def self.container
8
+ @container ||= Container.new
9
+ end
10
+
11
+ # @api private
12
+ def self.register(function = Dry::Core::Constants::Undefined, &block)
13
+ fn = Dry::Core::Constants::Undefined.default(function, block)
14
+ fn_name = register_name(fn)
15
+ container.register(fn_name, fn) unless container.key?(fn_name)
16
+ fn_name
17
+ end
18
+
19
+ # @api private
20
+ def self.[](fn_name)
21
+ if container.key?(fn_name)
22
+ container[fn_name]
23
+ else
24
+ fn_name
25
+ end
26
+ end
27
+
28
+ # @api private
29
+ def self.register_name(function)
30
+ "fn_#{function.object_id}"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+ require 'dry/types/constructor'
2
+
3
+ module Dry
4
+ module Types
5
+ class Hash < Nominal
6
+ class Constructor < ::Dry::Types::Constructor
7
+ # @api private
8
+ def constructor_type
9
+ ::Dry::Types::Hash::Constructor
10
+ end
11
+
12
+ private
13
+
14
+ def composable?(value)
15
+ super && !value.is_a?(Schema::Key)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,45 +1,125 @@
1
- require 'dry/types/hash/schema'
1
+ require 'dry/types/hash/constructor'
2
2
 
3
3
  module Dry
4
4
  module Types
5
- class Hash < Definition
6
- def schema(type_map, klass = Schema)
7
- member_types = type_map.each_with_object({}) { |(name, type), result|
8
- result[name] =
9
- case type
10
- when String, Class then Types[type]
11
- else type
12
- end
13
- }
5
+ class Hash < Nominal
6
+ NOT_REQUIRED = { required: false }.freeze
14
7
 
15
- klass.new(primitive, options.merge(member_types: member_types))
8
+ # @overload schmea(type_map, meta = EMPTY_HASH)
9
+ # @param [{Symbol => Dry::Types::Nominal}] type_map
10
+ # @param [Hash] meta
11
+ # @return [Dry::Types::Schema]
12
+ # @overload schema(keys)
13
+ # @param [Array<Dry::Types::Schema::Key>] key List of schema keys
14
+ # @param [Hash] meta
15
+ # @return [Dry::Types::Schema]
16
+ def schema(keys_or_map, meta = EMPTY_HASH)
17
+ if keys_or_map.is_a?(::Array)
18
+ keys = keys_or_map
19
+ else
20
+ keys = build_keys(keys_or_map)
21
+ end
22
+
23
+ Schema.new(primitive, keys: keys, **options, meta: self.meta.merge(meta))
24
+ end
25
+
26
+ # Build a map type
27
+ #
28
+ # @param [Type] key_type
29
+ # @param [Type] value_type
30
+ # @return [Map]
31
+ def map(key_type, value_type)
32
+ Map.new(
33
+ primitive,
34
+ key_type: resolve_type(key_type),
35
+ value_type: resolve_type(value_type),
36
+ meta: meta
37
+ )
16
38
  end
17
39
 
18
- def weak(type_map)
19
- schema(type_map, Weak)
40
+ # @param [{Symbol => Nominal}] type_map
41
+ # @return [Schema]
42
+ def weak(*)
43
+ raise "Support for old hash schemas was removed, please refer to the CHANGELOG "\
44
+ "on how to proceed with the new API https://github.com/dry-rb/dry-types/blob/master/CHANGELOG.md"
20
45
  end
46
+ alias_method :permissive, :weak
47
+ alias_method :strict, :weak
48
+ alias_method :strict_with_defaults, :weak
49
+ alias_method :symbolized, :weak
21
50
 
22
- def permissive(type_map)
23
- schema(type_map, Permissive)
51
+ # Injects a type transformation function for building schemas
52
+ # @param [#call,nil] proc
53
+ # @param [#call,nil] block
54
+ # @return [Hash]
55
+ def with_type_transform(proc = nil, &block)
56
+ fn = proc || block
57
+
58
+ if fn.nil?
59
+ raise ArgumentError, "a block or callable argument is required"
60
+ end
61
+
62
+ handle = Dry::Types::FnContainer.register(fn)
63
+ with(type_transform_fn: handle)
24
64
  end
25
65
 
26
- def strict(type_map)
27
- schema(type_map, Strict)
66
+ # @api private
67
+ def constructor_type
68
+ ::Dry::Types::Hash::Constructor
28
69
  end
29
70
 
30
- def strict_with_defaults(type_map)
31
- schema(type_map, StrictWithDefaults)
71
+ # Whether the type transforms types of schemas created by {Dry::Types::Hash#schema}
72
+ # @return [Boolean]
73
+ # @api public
74
+ def transform_types?
75
+ !options[:type_transform_fn].nil?
32
76
  end
33
77
 
34
- def symbolized(type_map)
35
- schema(type_map, Symbolized)
78
+ # @param meta [Boolean] Whether to dump the meta to the AST
79
+ # @return [Array] An AST representation
80
+ def to_ast(meta: true)
81
+ if RUBY_VERSION >= "2.5"
82
+ opts = options.slice(:type_transform_fn)
83
+ else
84
+ opts = options.select { |k, _| k == :type_transform_fn }
85
+ end
86
+
87
+ [:hash, [opts, meta ? self.meta : EMPTY_HASH]]
36
88
  end
37
89
 
38
90
  private
39
91
 
40
- def resolve_missing_value(_result, _key, _type)
41
- # noop
92
+ # @api private
93
+ def build_keys(type_map)
94
+ type_fn = options.fetch(:type_transform_fn, Schema::NO_TRANSFORM)
95
+ type_transform = Dry::Types::FnContainer[type_fn]
96
+
97
+ type_map.map do |map_key, type|
98
+ name, options = key_name(map_key)
99
+ key = Schema::Key.new(resolve_type(type), name, options)
100
+ type_transform.(key)
101
+ end
102
+ end
103
+
104
+ # @api private
105
+ def resolve_type(type)
106
+ case type
107
+ when String, Class then Types[type]
108
+ else type
109
+ end
110
+ end
111
+
112
+ # @api private
113
+ def key_name(key)
114
+ if key.to_s.end_with?('?')
115
+ [key.to_s.chop.to_sym, NOT_REQUIRED]
116
+ else
117
+ [key, EMPTY_HASH]
118
+ end
42
119
  end
43
120
  end
44
121
  end
45
122
  end
123
+
124
+ require 'dry/types/schema/key'
125
+ require 'dry/types/schema'
@@ -0,0 +1,7 @@
1
+ require 'dry/inflector'
2
+
3
+ module Dry
4
+ module Types
5
+ Inflector = Dry::Inflector.new
6
+ end
7
+ end
@@ -3,31 +3,31 @@ require 'dry/types/coercions/json'
3
3
  module Dry
4
4
  module Types
5
5
  register('json.nil') do
6
- self['nil'].constructor(Coercions::JSON.method(:to_nil))
6
+ self['nominal.nil'].constructor(Coercions::JSON.method(:to_nil))
7
7
  end
8
8
 
9
9
  register('json.date') do
10
- self['date'].constructor(Coercions::JSON.method(:to_date))
10
+ self['nominal.date'].constructor(Coercions::JSON.method(:to_date))
11
11
  end
12
12
 
13
13
  register('json.date_time') do
14
- self['date_time'].constructor(Coercions::JSON.method(:to_date_time))
14
+ self['nominal.date_time'].constructor(Coercions::JSON.method(:to_date_time))
15
15
  end
16
16
 
17
17
  register('json.time') do
18
- self['time'].constructor(Coercions::JSON.method(:to_time))
18
+ self['nominal.time'].constructor(Coercions::JSON.method(:to_time))
19
19
  end
20
20
 
21
21
  register('json.decimal') do
22
- self['decimal'].constructor(Coercions::JSON.method(:to_decimal))
22
+ self['nominal.decimal'].constructor(Coercions::JSON.method(:to_decimal))
23
23
  end
24
24
 
25
25
  register('json.array') do
26
- self['array'].safe
26
+ self['nominal.array'].safe
27
27
  end
28
28
 
29
29
  register('json.hash') do
30
- self['hash'].safe
30
+ self['nominal.hash'].safe
31
31
  end
32
32
  end
33
33
  end
@@ -0,0 +1,98 @@
1
+ module Dry
2
+ module Types
3
+ class Map < Nominal
4
+ def initialize(_primitive, key_type: Types['any'], value_type: Types['any'], meta: EMPTY_HASH)
5
+ super(_primitive, key_type: key_type, value_type: value_type, meta: meta)
6
+ validate_options!
7
+ end
8
+
9
+ # @return [Type]
10
+ def key_type
11
+ options[:key_type]
12
+ end
13
+
14
+ # @return [Type]
15
+ def value_type
16
+ options[:value_type]
17
+ end
18
+
19
+ # @return [String]
20
+ def name
21
+ "Map"
22
+ end
23
+
24
+ # @param [Hash] hash
25
+ # @return [Hash]
26
+ def call(hash)
27
+ try(hash) do |failure|
28
+ raise MapError, failure.error.join("\n")
29
+ end.input
30
+ end
31
+ alias_method :[], :call
32
+
33
+ # @param [Hash] hash
34
+ # @return [Boolean]
35
+ def valid?(hash)
36
+ coerce(hash).success?
37
+ end
38
+ alias_method :===, :valid?
39
+
40
+ # @param [Hash] hash
41
+ # @return [Result]
42
+ def try(hash)
43
+ result = coerce(hash)
44
+ return result if result.success? || !block_given?
45
+ yield(result)
46
+ end
47
+
48
+ # @param meta [Boolean] Whether to dump the meta to the AST
49
+ # @return [Array] An AST representation
50
+ def to_ast(meta: true)
51
+ [:map,
52
+ [key_type.to_ast(meta: true), value_type.to_ast(meta: true),
53
+ meta ? self.meta : EMPTY_HASH]]
54
+ end
55
+
56
+ # @return [Boolean]
57
+ def constrained?
58
+ value_type.constrained?
59
+ end
60
+
61
+ private
62
+
63
+ def coerce(input)
64
+ return failure(
65
+ input, "#{input.inspect} must be an instance of #{primitive}"
66
+ ) unless primitive?(input)
67
+
68
+ output, failures = {}, []
69
+
70
+ input.each do |k,v|
71
+ res_k = options[:key_type].try(k)
72
+ res_v = options[:value_type].try(v)
73
+ if res_k.failure?
74
+ failures << "input key #{k.inspect} is invalid: #{res_k.error}"
75
+ elsif output.key?(res_k.input)
76
+ failures << "duplicate coerced hash key #{res_k.input.inspect}"
77
+ elsif res_v.failure?
78
+ failures << "input value #{v.inspect} for key #{k.inspect} is invalid: #{res_v.error}"
79
+ else
80
+ output[res_k.input] = res_v.input
81
+ end
82
+ end
83
+
84
+ return success(output) if failures.empty?
85
+
86
+ failure(input, failures)
87
+ end
88
+
89
+ def validate_options!
90
+ %i(key_type value_type).each do |opt|
91
+ type = send(opt)
92
+ next if type.is_a?(Type)
93
+ raise MapError, ":#{opt} must be a #{Type}, got: #{type.inspect}"
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end