dry-types 0.15.0 → 1.2.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 (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