dry-types 0.15.0 → 1.0.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +18 -2
  4. data/.travis.yml +4 -5
  5. data/.yardopts +6 -2
  6. data/CHANGELOG.md +69 -1
  7. data/Gemfile +3 -0
  8. data/README.md +2 -1
  9. data/Rakefile +2 -0
  10. data/benchmarks/hash_schemas.rb +2 -0
  11. data/benchmarks/lax_schema.rb +16 -0
  12. data/benchmarks/profile_invalid_input.rb +15 -0
  13. data/benchmarks/profile_lax_schema_valid.rb +16 -0
  14. data/benchmarks/profile_valid_input.rb +15 -0
  15. data/benchmarks/schema_valid_vs_invalid.rb +21 -0
  16. data/benchmarks/setup.rb +17 -0
  17. data/dry-types.gemspec +4 -2
  18. data/lib/dry-types.rb +2 -0
  19. data/lib/dry/types.rb +51 -13
  20. data/lib/dry/types/any.rb +21 -10
  21. data/lib/dry/types/array.rb +11 -1
  22. data/lib/dry/types/array/member.rb +65 -13
  23. data/lib/dry/types/builder.rb +48 -4
  24. data/lib/dry/types/builder_methods.rb +9 -8
  25. data/lib/dry/types/coercions.rb +71 -19
  26. data/lib/dry/types/coercions/json.rb +22 -3
  27. data/lib/dry/types/coercions/params.rb +98 -30
  28. data/lib/dry/types/compiler.rb +35 -12
  29. data/lib/dry/types/constrained.rb +73 -27
  30. data/lib/dry/types/constrained/coercible.rb +36 -6
  31. data/lib/dry/types/constraints.rb +15 -1
  32. data/lib/dry/types/constructor.rb +90 -43
  33. data/lib/dry/types/constructor/function.rb +201 -0
  34. data/lib/dry/types/container.rb +5 -0
  35. data/lib/dry/types/core.rb +7 -5
  36. data/lib/dry/types/decorator.rb +36 -9
  37. data/lib/dry/types/default.rb +48 -16
  38. data/lib/dry/types/enum.rb +30 -16
  39. data/lib/dry/types/errors.rb +73 -7
  40. data/lib/dry/types/extensions.rb +2 -0
  41. data/lib/dry/types/extensions/maybe.rb +43 -4
  42. data/lib/dry/types/fn_container.rb +5 -0
  43. data/lib/dry/types/hash.rb +22 -3
  44. data/lib/dry/types/hash/constructor.rb +13 -0
  45. data/lib/dry/types/inflector.rb +2 -0
  46. data/lib/dry/types/json.rb +4 -6
  47. data/lib/dry/types/{safe.rb → lax.rb} +34 -17
  48. data/lib/dry/types/map.rb +63 -29
  49. data/lib/dry/types/meta.rb +51 -0
  50. data/lib/dry/types/module.rb +7 -2
  51. data/lib/dry/types/nominal.rb +105 -13
  52. data/lib/dry/types/options.rb +12 -25
  53. data/lib/dry/types/params.rb +5 -3
  54. data/lib/dry/types/printable.rb +5 -1
  55. data/lib/dry/types/printer.rb +58 -57
  56. data/lib/dry/types/result.rb +26 -0
  57. data/lib/dry/types/schema.rb +169 -66
  58. data/lib/dry/types/schema/key.rb +34 -39
  59. data/lib/dry/types/spec/types.rb +41 -1
  60. data/lib/dry/types/sum.rb +70 -21
  61. data/lib/dry/types/type.rb +49 -0
  62. data/lib/dry/types/version.rb +3 -1
  63. metadata +14 -12
@@ -1,7 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/container'
2
4
 
3
5
  module Dry
4
6
  module Types
7
+ # Internal container for the built-in types
8
+ #
9
+ # @api private
5
10
  class Container
6
11
  include Dry::Container::Mixin
7
12
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/any'
2
4
 
3
5
  module Dry
@@ -60,14 +62,14 @@ module Dry
60
62
  end
61
63
 
62
64
  # Register `:bool` since it's common and not a built-in Ruby type :(
63
- register("nominal.bool", self["nominal.true"] | self["nominal.false"])
64
- bool = self["strict.true"] | self["strict.false"]
65
+ register('nominal.bool', self['nominal.true'] | self['nominal.false'])
66
+ bool = self['strict.true'] | self['strict.false']
65
67
  register("strict.bool", bool)
66
68
  register("bool", bool)
67
69
 
68
- register("any", Any)
69
- register("nominal.any", Any)
70
- register("strict.any", Any)
70
+ register('any', Any)
71
+ register('nominal.any', Any)
72
+ register('strict.any', Any)
71
73
  end
72
74
  end
73
75
 
@@ -1,7 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/options'
2
4
 
3
5
  module Dry
4
6
  module Types
7
+ # Common API for types
8
+ #
9
+ # @api public
5
10
  module Decorator
6
11
  include Options
7
12
 
@@ -16,53 +21,73 @@ module Dry
16
21
 
17
22
  # @param [Object] input
18
23
  # @param [#call, nil] block
24
+ #
19
25
  # @return [Result,Logic::Result]
20
26
  # @return [Object] if block given and try fails
27
+ #
28
+ # @api public
21
29
  def try(input, &block)
22
30
  type.try(input, &block)
23
31
  end
24
32
 
25
- # @param [Object] value
26
- # @return [Boolean]
27
- def valid?(value)
28
- type.valid?(value)
29
- end
30
- alias_method :===, :valid?
31
-
32
33
  # @return [Boolean]
34
+ #
35
+ # @api public
33
36
  def default?
34
37
  type.default?
35
38
  end
36
39
 
37
40
  # @return [Boolean]
41
+ #
42
+ # @api public
38
43
  def constrained?
39
44
  type.constrained?
40
45
  end
41
46
 
42
47
  # @return [Sum]
48
+ #
49
+ # @api public
43
50
  def optional
44
51
  Types['strict.nil'] | self
45
52
  end
46
53
 
47
54
  # @param [Symbol] meth
48
55
  # @param [Boolean] include_private
56
+ #
49
57
  # @return [Boolean]
58
+ #
59
+ # @api public
50
60
  def respond_to_missing?(meth, include_private = false)
51
61
  super || type.respond_to?(meth)
52
62
  end
53
63
 
64
+ # Wrap the type with a proc
65
+ #
66
+ # @return [Proc]
67
+ #
68
+ # @api public
69
+ def to_proc
70
+ proc { |value| self.(value) }
71
+ end
72
+
54
73
  private
55
74
 
56
75
  # @param [Object] response
76
+ #
57
77
  # @return [Boolean]
78
+ #
79
+ # @api private
58
80
  def decorate?(response)
59
- response.kind_of?(type.class)
81
+ response.is_a?(type.class)
60
82
  end
61
83
 
62
84
  # Delegates missing methods to {#type}
85
+ #
63
86
  # @param [Symbol] meth
64
87
  # @param [Array] args
65
88
  # @param [#call, nil] block
89
+ #
90
+ # @api private
66
91
  def method_missing(meth, *args, &block)
67
92
  if type.respond_to?(meth)
68
93
  response = type.__send__(meth, *args, &block)
@@ -78,8 +103,10 @@ module Dry
78
103
  end
79
104
 
80
105
  # Replace underlying type
106
+ #
107
+ # @api private
81
108
  def __new__(type)
82
- self.class.new(type, options)
109
+ self.class.new(type, *@__args__[1..-1], **@options)
83
110
  end
84
111
  end
85
112
  end
@@ -1,16 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/decorator'
2
4
 
3
5
  module Dry
4
6
  module Types
7
+ # Default types are useful when a missing value should be replaced by a default one
8
+ #
9
+ # @api public
5
10
  class Default
6
- include Type
7
- include Decorator
8
- include Builder
9
- include Printable
10
- include Dry::Equalizer(:type, :options, :value, inspect: false)
11
-
11
+ # @api private
12
12
  class Callable < Default
13
- include Dry::Equalizer(:type, :options, inspect: false)
13
+ include Dry::Equalizer(:type, inspect: false)
14
14
 
15
15
  # Evaluates given callable
16
16
  # @return [Object]
@@ -19,13 +19,22 @@ module Dry
19
19
  end
20
20
  end
21
21
 
22
+ include Type
23
+ include Decorator
24
+ include Builder
25
+ include Printable
26
+ include Dry::Equalizer(:type, :value, inspect: false)
27
+
22
28
  # @return [Object]
23
29
  attr_reader :value
24
30
 
25
31
  alias_method :evaluate, :value
26
32
 
27
33
  # @param [Object, #call] value
34
+ #
28
35
  # @return [Class] {Default} or {Default::Callable}
36
+ #
37
+ # @api private
29
38
  def self.[](value)
30
39
  if value.respond_to?(:call)
31
40
  Callable
@@ -36,48 +45,71 @@ module Dry
36
45
 
37
46
  # @param [Type] type
38
47
  # @param [Object] value
48
+ #
49
+ # @api private
39
50
  def initialize(type, value, **options)
40
51
  super
41
52
  @value = value
42
53
  end
43
54
 
55
+ # Build a constrained type
56
+ #
44
57
  # @param [Array] args see {Dry::Types::Builder#constrained}
58
+ #
45
59
  # @return [Default]
60
+ #
61
+ # @api public
46
62
  def constrained(*args)
47
63
  type.constrained(*args).default(value)
48
64
  end
49
65
 
50
66
  # @return [true]
67
+ #
68
+ # @api public
51
69
  def default?
52
70
  true
53
71
  end
54
72
 
55
73
  # @param [Object] input
74
+ #
56
75
  # @return [Result::Success]
76
+ #
77
+ # @api public
57
78
  def try(input)
58
79
  success(call(input))
59
80
  end
60
81
 
82
+ # @return [Boolean]
83
+ #
84
+ # @api public
61
85
  def valid?(value = Undefined)
62
- value.equal?(Undefined) || super
86
+ Undefined.equal?(value) || super
63
87
  end
64
88
 
65
89
  # @param [Object] input
90
+ #
66
91
  # @return [Object] value passed through {#type} or {#default} value
67
- def call(input = Undefined)
92
+ #
93
+ # @api private
94
+ def call_unsafe(input = Undefined)
68
95
  if input.equal?(Undefined)
69
96
  evaluate
70
97
  else
71
- Undefined.default(type[input]) { evaluate }
98
+ Undefined.default(type.call_unsafe(input)) { evaluate }
72
99
  end
73
100
  end
74
- alias_method :[], :call
75
-
76
- private
77
101
 
78
- # Replace underlying type
79
- def __new__(type)
80
- self.class.new(type, value, options)
102
+ # @param [Object] input
103
+ #
104
+ # @return [Object] value passed through {#type} or {#default} value
105
+ #
106
+ # @api private
107
+ def call_safe(input = Undefined, &block)
108
+ if input.equal?(Undefined)
109
+ evaluate
110
+ else
111
+ Undefined.default(type.call_safe(input, &block)) { evaluate }
112
+ end
81
113
  end
82
114
  end
83
115
  end
@@ -1,10 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/decorator'
2
4
 
3
5
  module Dry
4
6
  module Types
7
+ # Enum types can be used to define an enum on top of an existing type
8
+ #
9
+ # @api public
5
10
  class Enum
6
11
  include Type
7
- include Dry::Equalizer(:type, :options, :mapping, inspect: false)
12
+ include Dry::Equalizer(:type, :mapping, inspect: false)
8
13
  include Decorator
9
14
 
10
15
  # @return [Array]
@@ -19,6 +24,8 @@ module Dry
19
24
  # @param [Type] type
20
25
  # @param [Hash] options
21
26
  # @option options [Array] :values
27
+ #
28
+ # @api private
22
29
  def initialize(type, options)
23
30
  super
24
31
  @mapping = options.fetch(:mapping).freeze
@@ -27,22 +34,28 @@ module Dry
27
34
  freeze
28
35
  end
29
36
 
30
- # @param [Object] input
31
37
  # @return [Object]
32
- def call(input = Undefined)
33
- type[map_value(input)]
38
+ #
39
+ # @api private
40
+ def call_unsafe(input)
41
+ type.call_unsafe(map_value(input))
42
+ end
43
+
44
+ # @return [Object]
45
+ #
46
+ # @api private
47
+ def call_safe(input, &block)
48
+ type.call_safe(map_value(input), &block)
34
49
  end
35
- alias_method :[], :call
36
50
 
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
51
+ # @see Dry::Types::Constrained#try
52
+ #
53
+ # @api public
42
54
  def try(input)
43
55
  super(map_value(input))
44
56
  end
45
57
 
58
+ # @api private
46
59
  def default(*)
47
60
  raise '.enum(*values).default(value) is not supported. Call '\
48
61
  '.default(value).enum(*values) instead'
@@ -51,16 +64,15 @@ module Dry
51
64
  # Check whether a value is in the enum
52
65
  alias_method :include?, :valid?
53
66
 
54
- # @api public
55
- #
56
67
  # @see Nominal#to_ast
68
+ #
69
+ # @api public
57
70
  def to_ast(meta: true)
58
- [:enum, [type.to_ast(meta: meta),
59
- mapping,
60
- meta ? self.meta : EMPTY_HASH]]
71
+ [:enum, [type.to_ast(meta: meta), mapping]]
61
72
  end
62
73
 
63
74
  # @return [String]
75
+ #
64
76
  # @api public
65
77
  def to_s
66
78
  PRINTER.(self)
@@ -71,8 +83,10 @@ module Dry
71
83
 
72
84
  # Maps a value
73
85
  #
74
- # @param [Object]
86
+ # @param [Object] input
87
+ #
75
88
  # @return [Object]
89
+ #
76
90
  # @api private
77
91
  def map_value(input)
78
92
  if input.equal?(Undefined)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Types
3
5
  extend Dry::Core::ClassAttributes
@@ -8,7 +10,62 @@ module Dry
8
10
 
9
11
  namespace self
10
12
 
11
- class SchemaError < TypeError
13
+ # Base class for coercion errors raise by dry-types
14
+ #
15
+ class CoercionError < StandardError
16
+ # @api private
17
+ def self.handle(exception, meta: Undefined)
18
+ if block_given?
19
+ yield
20
+ else
21
+ raise new(
22
+ exception.message,
23
+ meta: meta,
24
+ backtrace: exception.backtrace
25
+ )
26
+ end
27
+ end
28
+
29
+ # Metadata associated with the error
30
+ #
31
+ # @return [Object]
32
+ attr_reader :meta
33
+
34
+ # @api private
35
+ def initialize(message, meta: Undefined, backtrace: Undefined)
36
+ unless message.is_a?(::String)
37
+ raise ArgumentError, "message must be a string, #{message.class} given"
38
+ end
39
+
40
+ super(message)
41
+ @meta = Undefined.default(meta, nil)
42
+ set_backtrace(backtrace) unless Undefined.equal?(backtrace)
43
+ end
44
+ end
45
+
46
+ # Collection of multiple errors
47
+ #
48
+ class MultipleError < CoercionError
49
+ # @return [Array<CoercionError>]
50
+ attr_reader :errors
51
+
52
+ # @param [Array<CoercionError>] errors
53
+ def initialize(errors)
54
+ @errors = errors
55
+ end
56
+
57
+ # @return string
58
+ def message
59
+ errors.map(&:message).join(', ')
60
+ end
61
+
62
+ # @return [Array]
63
+ def meta
64
+ errors.map(&:meta)
65
+ end
66
+ end
67
+
68
+ class SchemaError < CoercionError
12
69
  # @param [String,Symbol] key
13
70
  # @param [Object] value
14
71
  # @param [String, #to_s] result
@@ -17,26 +74,34 @@ module Dry
17
74
  end
18
75
  end
19
76
 
20
- MapError = Class.new(TypeError)
77
+ MapError = Class.new(CoercionError)
21
78
 
22
- SchemaKeyError = Class.new(KeyError)
79
+ SchemaKeyError = Class.new(CoercionError)
23
80
  private_constant(:SchemaKeyError)
24
81
 
25
82
  class MissingKeyError < SchemaKeyError
83
+ # @return [Symbol]
84
+ attr_reader :key
85
+
26
86
  # @param [String,Symbol] key
27
87
  def initialize(key)
28
- super(":#{key} is missing in Hash input")
88
+ @key = key
89
+ super("#{key.inspect} is missing in Hash input")
29
90
  end
30
91
  end
31
92
 
32
93
  class UnknownKeysError < SchemaKeyError
94
+ # @return [Array<Symbol>]
95
+ attr_reader :keys
96
+
33
97
  # @param [<String, Symbol>] keys
34
- def initialize(*keys)
98
+ def initialize(keys)
99
+ @keys = keys
35
100
  super("unexpected keys #{keys.inspect} in Hash input")
36
101
  end
37
102
  end
38
103
 
39
- class ConstraintError < TypeError
104
+ class ConstraintError < CoercionError
40
105
  # @return [String, #to_s]
41
106
  attr_reader :result
42
107
  # @return [Object]
@@ -56,9 +121,10 @@ module Dry
56
121
  end
57
122
 
58
123
  # @return [String]
59
- def to_s
124
+ def message
60
125
  "#{input.inspect} violates constraints (#{result} failed)"
61
126
  end
127
+ alias_method :to_s, :message
62
128
  end
63
129
  end
64
130
  end