dry-types 0.15.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  3. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  4. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +18 -2
  7. data/.travis.yml +10 -5
  8. data/.yardopts +6 -2
  9. data/CHANGELOG.md +186 -3
  10. data/Gemfile +11 -5
  11. data/README.md +4 -3
  12. data/Rakefile +4 -2
  13. data/benchmarks/hash_schemas.rb +10 -6
  14. data/benchmarks/lax_schema.rb +15 -0
  15. data/benchmarks/profile_invalid_input.rb +15 -0
  16. data/benchmarks/profile_lax_schema_valid.rb +16 -0
  17. data/benchmarks/profile_valid_input.rb +15 -0
  18. data/benchmarks/schema_valid_vs_invalid.rb +21 -0
  19. data/benchmarks/setup.rb +17 -0
  20. data/docsite/source/array-with-member.html.md +13 -0
  21. data/docsite/source/built-in-types.html.md +116 -0
  22. data/docsite/source/constraints.html.md +31 -0
  23. data/docsite/source/custom-types.html.md +93 -0
  24. data/docsite/source/default-values.html.md +91 -0
  25. data/docsite/source/enum.html.md +69 -0
  26. data/docsite/source/getting-started.html.md +57 -0
  27. data/docsite/source/hash-schemas.html.md +169 -0
  28. data/docsite/source/index.html.md +155 -0
  29. data/docsite/source/map.html.md +17 -0
  30. data/docsite/source/optional-values.html.md +96 -0
  31. data/docsite/source/sum.html.md +21 -0
  32. data/dry-types.gemspec +21 -19
  33. data/lib/dry-types.rb +2 -0
  34. data/lib/dry/types.rb +60 -17
  35. data/lib/dry/types/any.rb +21 -10
  36. data/lib/dry/types/array.rb +17 -1
  37. data/lib/dry/types/array/constructor.rb +32 -0
  38. data/lib/dry/types/array/member.rb +72 -13
  39. data/lib/dry/types/builder.rb +49 -5
  40. data/lib/dry/types/builder_methods.rb +43 -16
  41. data/lib/dry/types/coercions.rb +84 -19
  42. data/lib/dry/types/coercions/json.rb +22 -3
  43. data/lib/dry/types/coercions/params.rb +98 -30
  44. data/lib/dry/types/compiler.rb +35 -12
  45. data/lib/dry/types/constrained.rb +78 -27
  46. data/lib/dry/types/constrained/coercible.rb +36 -6
  47. data/lib/dry/types/constraints.rb +15 -1
  48. data/lib/dry/types/constructor.rb +77 -62
  49. data/lib/dry/types/constructor/function.rb +200 -0
  50. data/lib/dry/types/container.rb +5 -0
  51. data/lib/dry/types/core.rb +35 -14
  52. data/lib/dry/types/decorator.rb +37 -10
  53. data/lib/dry/types/default.rb +48 -16
  54. data/lib/dry/types/enum.rb +31 -16
  55. data/lib/dry/types/errors.rb +73 -7
  56. data/lib/dry/types/extensions.rb +6 -0
  57. data/lib/dry/types/extensions/maybe.rb +52 -5
  58. data/lib/dry/types/extensions/monads.rb +29 -0
  59. data/lib/dry/types/fn_container.rb +5 -0
  60. data/lib/dry/types/hash.rb +32 -14
  61. data/lib/dry/types/hash/constructor.rb +16 -3
  62. data/lib/dry/types/inflector.rb +2 -0
  63. data/lib/dry/types/json.rb +7 -5
  64. data/lib/dry/types/{safe.rb → lax.rb} +33 -16
  65. data/lib/dry/types/map.rb +70 -32
  66. data/lib/dry/types/meta.rb +51 -0
  67. data/lib/dry/types/module.rb +10 -5
  68. data/lib/dry/types/nominal.rb +105 -14
  69. data/lib/dry/types/options.rb +12 -25
  70. data/lib/dry/types/params.rb +14 -3
  71. data/lib/dry/types/predicate_inferrer.rb +197 -0
  72. data/lib/dry/types/predicate_registry.rb +34 -0
  73. data/lib/dry/types/primitive_inferrer.rb +97 -0
  74. data/lib/dry/types/printable.rb +5 -1
  75. data/lib/dry/types/printer.rb +70 -64
  76. data/lib/dry/types/result.rb +26 -0
  77. data/lib/dry/types/schema.rb +177 -80
  78. data/lib/dry/types/schema/key.rb +48 -35
  79. data/lib/dry/types/spec/types.rb +43 -6
  80. data/lib/dry/types/sum.rb +70 -21
  81. data/lib/dry/types/type.rb +49 -0
  82. data/lib/dry/types/version.rb +3 -1
  83. metadata +91 -62
@@ -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,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/any'
2
4
 
3
5
  module Dry
4
6
  module Types
5
7
  # Primitives with {Kernel} coercion methods
6
- COERCIBLE = {
8
+ KERNEL_COERCIBLE = {
7
9
  string: String,
8
10
  integer: Integer,
9
11
  float: Float,
@@ -12,10 +14,19 @@ module Dry
12
14
  hash: ::Hash
13
15
  }.freeze
14
16
 
15
- # Primitives that are non-coercible through {Kernel} methods
17
+ # Primitives with coercions through by convention `to_*` methods
18
+ METHOD_COERCIBLE = {
19
+ symbol: Symbol
20
+ }.freeze
21
+
22
+ # By convention methods to coerce {METHOD_COERCIBLE} primitives
23
+ METHOD_COERCIBLE_METHODS = {
24
+ symbol: :to_sym
25
+ }.freeze
26
+
27
+ # Primitives that are non-coercible
16
28
  NON_COERCIBLE = {
17
29
  nil: NilClass,
18
- symbol: Symbol,
19
30
  class: Class,
20
31
  true: TrueClass,
21
32
  false: FalseClass,
@@ -26,7 +37,10 @@ module Dry
26
37
  }.freeze
27
38
 
28
39
  # All built-in primitives
29
- ALL_PRIMITIVES = COERCIBLE.merge(NON_COERCIBLE).freeze
40
+ ALL_PRIMITIVES = [KERNEL_COERCIBLE, METHOD_COERCIBLE, NON_COERCIBLE].reduce(&:merge).freeze
41
+
42
+ # All coercible types
43
+ COERCIBLE = KERNEL_COERCIBLE.merge(METHOD_COERCIBLE).freeze
30
44
 
31
45
  # All built-in primitives except {NilClass}
32
46
  NON_NIL = ALL_PRIMITIVES.reject { |name, _| name == :nil }.freeze
@@ -44,11 +58,18 @@ module Dry
44
58
  register("strict.#{name}", type)
45
59
  end
46
60
 
47
- # Register {COERCIBLE} types
48
- COERCIBLE.each do |name, primitive|
61
+ # Register {KERNEL_COERCIBLE} types
62
+ KERNEL_COERCIBLE.each do |name, primitive|
49
63
  register("coercible.#{name}", self["nominal.#{name}"].constructor(Kernel.method(primitive.name)))
50
64
  end
51
65
 
66
+ # Register {METHOD_COERCIBLE} types
67
+ METHOD_COERCIBLE.each_key do |name|
68
+ register(
69
+ "coercible.#{name}", self["nominal.#{name}"].constructor(&METHOD_COERCIBLE_METHODS[name])
70
+ )
71
+ end
72
+
52
73
  # Register optional strict {NON_NIL} types
53
74
  NON_NIL.each_key do |name|
54
75
  register("optional.strict.#{name}", self["strict.#{name}"].optional)
@@ -60,14 +81,14 @@ module Dry
60
81
  end
61
82
 
62
83
  # 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("strict.bool", bool)
66
- register("bool", bool)
67
-
68
- register("any", Any)
69
- register("nominal.any", Any)
70
- register("strict.any", Any)
84
+ register('nominal.bool', self['nominal.true'] | self['nominal.false'])
85
+ bool = self['strict.true'] | self['strict.false']
86
+ register('strict.bool', bool)
87
+ register('bool', bool)
88
+
89
+ register('any', Any)
90
+ register('nominal.any', Any)
91
+ register('strict.any', Any)
71
92
  end
72
93
  end
73
94
 
@@ -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,56 +21,76 @@ 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
- response = type.__send__(meth, *args, &block)
93
+ response = type.public_send(meth, *args, &block)
69
94
 
70
95
  if decorate?(response)
71
96
  __new__(response)
@@ -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,11 +1,17 @@
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
14
+ include Builder
9
15
 
10
16
  # @return [Array]
11
17
  attr_reader :values
@@ -19,6 +25,8 @@ module Dry
19
25
  # @param [Type] type
20
26
  # @param [Hash] options
21
27
  # @option options [Array] :values
28
+ #
29
+ # @api private
22
30
  def initialize(type, options)
23
31
  super
24
32
  @mapping = options.fetch(:mapping).freeze
@@ -27,22 +35,28 @@ module Dry
27
35
  freeze
28
36
  end
29
37
 
30
- # @param [Object] input
31
38
  # @return [Object]
32
- def call(input = Undefined)
33
- type[map_value(input)]
39
+ #
40
+ # @api private
41
+ def call_unsafe(input)
42
+ type.call_unsafe(map_value(input))
43
+ end
44
+
45
+ # @return [Object]
46
+ #
47
+ # @api private
48
+ def call_safe(input, &block)
49
+ type.call_safe(map_value(input), &block)
34
50
  end
35
- alias_method :[], :call
36
51
 
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
52
+ # @see Dry::Types::Constrained#try
53
+ #
54
+ # @api public
42
55
  def try(input)
43
56
  super(map_value(input))
44
57
  end
45
58
 
59
+ # @api private
46
60
  def default(*)
47
61
  raise '.enum(*values).default(value) is not supported. Call '\
48
62
  '.default(value).enum(*values) instead'
@@ -51,16 +65,15 @@ module Dry
51
65
  # Check whether a value is in the enum
52
66
  alias_method :include?, :valid?
53
67
 
54
- # @api public
55
- #
56
68
  # @see Nominal#to_ast
69
+ #
70
+ # @api public
57
71
  def to_ast(meta: true)
58
- [:enum, [type.to_ast(meta: meta),
59
- mapping,
60
- meta ? self.meta : EMPTY_HASH]]
72
+ [:enum, [type.to_ast(meta: meta), mapping]]
61
73
  end
62
74
 
63
75
  # @return [String]
76
+ #
64
77
  # @api public
65
78
  def to_s
66
79
  PRINTER.(self)
@@ -71,8 +84,10 @@ module Dry
71
84
 
72
85
  # Maps a value
73
86
  #
74
- # @param [Object]
87
+ # @param [Object] input
88
+ #
75
89
  # @return [Object]
90
+ #
76
91
  # @api private
77
92
  def map_value(input)
78
93
  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