dry-types 0.15.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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