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,36 +1,47 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Types
3
- Any = Class.new(Nominal) do
5
+ # Any is a nominal type that defines Object as the primitive class
6
+ #
7
+ # This type is useful in places where you can't be specific about the type
8
+ # and anything is acceptable.
9
+ #
10
+ # @api public
11
+ class AnyClass < Nominal
4
12
  def self.name
5
13
  'Any'
6
14
  end
7
15
 
16
+ # @api private
8
17
  def initialize(**options)
9
18
  super(::Object, options)
10
19
  end
11
20
 
12
21
  # @return [String]
22
+ #
23
+ # @api public
13
24
  def name
14
25
  'Any'
15
26
  end
16
27
 
17
- # @param [Object] any input is valid
18
- # @return [true]
19
- def valid?(_)
20
- true
21
- end
22
- alias_method :===, :valid?
23
-
24
28
  # @param [Hash] new_options
29
+ #
25
30
  # @return [Type]
26
- def with(**new_options)
31
+ #
32
+ # @api public
33
+ def with(new_options)
27
34
  self.class.new(**options, meta: @meta, **new_options)
28
35
  end
29
36
 
30
37
  # @return [Array]
38
+ #
39
+ # @api public
31
40
  def to_ast(meta: true)
32
41
  [:any, meta ? self.meta : EMPTY_HASH]
33
42
  end
34
- end.new
43
+ end
44
+
45
+ Any = AnyClass.new
35
46
  end
36
47
  end
@@ -1,10 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/array/member'
2
4
 
3
5
  module Dry
4
6
  module Types
7
+ # Array type can be used to define an array with optional member type
8
+ #
9
+ # @api public
5
10
  class Array < Nominal
6
- # @param [Type] type
11
+ # Build an array type with a member type
12
+ #
13
+ # @param [Type,#call] type
14
+ #
7
15
  # @return [Array::Member]
16
+ #
17
+ # @api public
8
18
  def of(type)
9
19
  member =
10
20
  case type
@@ -1,46 +1,89 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Types
3
5
  class Array < Nominal
6
+ # Member arrays define their member type that is applied to each element
7
+ #
8
+ # @api public
4
9
  class Member < Array
5
10
  # @return [Type]
6
11
  attr_reader :member
7
12
 
8
13
  # @param [Class] primitive
9
14
  # @param [Hash] options
15
+ #
10
16
  # @option options [Type] :member
17
+ #
18
+ # @api private
11
19
  def initialize(primitive, options = {})
12
20
  @member = options.fetch(:member)
13
21
  super
14
22
  end
15
23
 
16
24
  # @param [Object] input
17
- # @param [Symbol] meth
25
+ #
18
26
  # @return [Array]
19
- def call(input, meth = :call)
20
- input.map { |el| member.__send__(meth, el) }
27
+ #
28
+ # @api private
29
+ def call_unsafe(input)
30
+ if primitive?(input)
31
+ input.each_with_object([]) do |el, output|
32
+ coerced = member.call_unsafe(el)
33
+
34
+ output << coerced unless Undefined.equal?(coerced)
35
+ end
36
+ else
37
+ super
38
+ end
21
39
  end
22
- alias_method :[], :call
23
40
 
24
- # @param [Array, #all?, Object] value
25
- # @return [Boolean]
26
- def valid?(value)
27
- super && value.all? { |el| member.valid?(el) }
41
+ # @param [Object] input
42
+ # @return [Array]
43
+ #
44
+ # @api private
45
+ def call_safe(input)
46
+ if primitive?(input)
47
+ failed = false
48
+
49
+ result = input.each_with_object([]) do |el, output|
50
+ coerced = member.call_safe(el) { |out = el|
51
+ failed = true
52
+ out
53
+ }
54
+
55
+ output << coerced unless Undefined.equal?(coerced)
56
+ end
57
+
58
+ failed ? yield(result) : result
59
+ else
60
+ yield
61
+ end
28
62
  end
29
63
 
30
64
  # @param [Array, Object] input
31
65
  # @param [#call,nil] block
66
+ #
32
67
  # @yieldparam [Failure] failure
33
68
  # @yieldreturn [Result]
69
+ #
34
70
  # @return [Result,Logic::Result]
71
+ #
72
+ # @api public
35
73
  def try(input, &block)
36
- if input.is_a?(::Array)
37
- result = call(input, :try).reject { |r| r.input.equal?(Undefined) }
38
- output = result.map(&:input)
74
+ if primitive?(input)
75
+ output = []
76
+
77
+ result = input.map { |el| member.try(el) }
78
+ result.each do |r|
79
+ output << r.input unless Undefined.equal?(r.input)
80
+ end
39
81
 
40
82
  if result.all?(&:success?)
41
83
  success(output)
42
84
  else
43
- failure = failure(output, result.select(&:failure?))
85
+ error = result.find(&:failure?).error
86
+ failure = failure(output, error)
44
87
  block ? yield(failure) : failure
45
88
  end
46
89
  else
@@ -49,9 +92,18 @@ module Dry
49
92
  end
50
93
  end
51
94
 
52
- # @api public
95
+ # Build a lax type
96
+ #
97
+ # @return [Lax]
53
98
  #
99
+ # @api public
100
+ def lax
101
+ Lax.new(Member.new(primitive, { **options, member: member.lax }))
102
+ end
103
+
54
104
  # @see Nominal#to_ast
105
+ #
106
+ # @api public
55
107
  def to_ast(meta: true)
56
108
  if member.respond_to?(:to_ast)
57
109
  [:array, [member.to_ast(meta: meta), meta ? self.meta : EMPTY_HASH]]
@@ -1,43 +1,72 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/core/deprecations'
2
4
 
3
5
  module Dry
4
6
  module Types
7
+ # Common API for building types and composition
8
+ #
9
+ # @api public
5
10
  module Builder
6
11
  include Dry::Core::Constants
7
12
 
8
13
  # @return [Class]
14
+ #
15
+ # @api private
9
16
  def constrained_type
10
17
  Constrained
11
18
  end
12
19
 
13
20
  # @return [Class]
21
+ #
22
+ # @api private
14
23
  def constructor_type
15
24
  Constructor
16
25
  end
17
26
 
27
+ # Compose two types into a Sum type
28
+ #
18
29
  # @param [Type] other
30
+ #
19
31
  # @return [Sum, Sum::Constrained]
32
+ #
33
+ # @api private
20
34
  def |(other)
21
35
  klass = constrained? && other.constrained? ? Sum::Constrained : Sum
22
36
  klass.new(self, other)
23
37
  end
24
38
 
39
+ # Turn a type into an optional type
40
+ #
25
41
  # @return [Sum]
42
+ #
43
+ # @api public
26
44
  def optional
27
45
  Types['strict.nil'] | self
28
46
  end
29
47
 
48
+ # Turn a type into a constrained type
49
+ #
30
50
  # @param [Hash] options constraining rule (see {Types.Rule})
51
+ #
31
52
  # @return [Constrained]
53
+ #
54
+ # @api public
32
55
  def constrained(options)
33
56
  constrained_type.new(self, rule: Types.Rule(options))
34
57
  end
35
58
 
59
+ # Turn a type into a type with a default value
60
+ #
36
61
  # @param [Object] input
37
62
  # @param [Hash] options
38
63
  # @param [#call,nil] block
64
+ #
39
65
  # @raise [ConstraintError]
66
+ #
40
67
  # @return [Default]
68
+ #
69
+ # @api public
41
70
  def default(input = Undefined, options = EMPTY_HASH, &block)
42
71
  unless input.frozen? || options[:shared]
43
72
  where = Dry::Core::Deprecations::STACK.()
@@ -60,8 +89,13 @@ module Dry
60
89
  end
61
90
  end
62
91
 
92
+ # Define an enum on top of the existing type
93
+ #
63
94
  # @param [Array] values
95
+ #
64
96
  # @return [Enum]
97
+ #
98
+ # @api public
65
99
  def enum(*values)
66
100
  mapping =
67
101
  if values.length == 1 && values[0].is_a?(::Hash)
@@ -73,15 +107,25 @@ module Dry
73
107
  Enum.new(constrained(included_in: mapping.keys), mapping: mapping)
74
108
  end
75
109
 
76
- # @return [Safe]
77
- def safe
78
- Safe.new(self)
110
+ # Turn a type into a lax type that will rescue from type-errors and
111
+ # return the original input
112
+ #
113
+ # @return [Lax]
114
+ #
115
+ # @api public
116
+ def lax
117
+ Lax.new(self)
79
118
  end
80
119
 
120
+ # Define a constructor for the type
121
+ #
81
122
  # @param [#call,nil] constructor
82
123
  # @param [Hash] options
83
124
  # @param [#call,nil] block
125
+ #
84
126
  # @return [Constructor]
127
+ #
128
+ # @api public
85
129
  def constructor(constructor = nil, **options, &block)
86
130
  constructor_type.new(with(options), fn: constructor || block)
87
131
  end
@@ -92,5 +136,5 @@ end
92
136
  require 'dry/types/default'
93
137
  require 'dry/types/constrained'
94
138
  require 'dry/types/enum'
95
- require 'dry/types/safe'
139
+ require 'dry/types/lax'
96
140
  require 'dry/types/sum'
@@ -1,5 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Types
5
+ # Common API for building type objects in a convenient way
6
+ #
7
+ # rubocop:disable Naming/MethodName
8
+ #
9
+ # @api public
3
10
  module BuilderMethods
4
11
  # @api private
5
12
  def included(base)
@@ -8,7 +15,8 @@ module Dry
8
15
  end
9
16
 
10
17
  # Build an array type.
11
- # It is a shortcut for Array.of
18
+ #
19
+ # Shortcut for Array#of.
12
20
  #
13
21
  # @example
14
22
  # Types::Strings = Types.Array(Types::String)
@@ -25,7 +33,6 @@ module Dry
25
33
  # @param [Hash{Symbol => Dry::Types::Type}] type_map
26
34
  #
27
35
  # @return [Dry::Types::Array]
28
- # @api public
29
36
  def Hash(type_map)
30
37
  self::Hash.schema(type_map)
31
38
  end
@@ -41,7 +48,6 @@ module Dry
41
48
  # @param [Class,Module] klass Class or module
42
49
  #
43
50
  # @return [Dry::Types::Type]
44
- # @api public
45
51
  def Instance(klass)
46
52
  Nominal.new(klass).constrained(type: klass)
47
53
  end
@@ -53,7 +59,6 @@ module Dry
53
59
  # @param [Object] value
54
60
  #
55
61
  # @return [Dry::Types::Type]
56
- # @api public
57
62
  def Value(value)
58
63
  Nominal.new(value.class).constrained(eql: value)
59
64
  end
@@ -64,7 +69,6 @@ module Dry
64
69
  # @param [Object] object
65
70
  #
66
71
  # @return [Dry::Types::Type]
67
- # @api public
68
72
  def Constant(object)
69
73
  Nominal.new(object.class).constrained(is: object)
70
74
  end
@@ -77,7 +81,6 @@ module Dry
77
81
  # @param [#call,nil] block Value constructor
78
82
  #
79
83
  # @return [Dry::Types::Type]
80
- # @api public
81
84
  def Constructor(klass, cons = nil, &block)
82
85
  Nominal.new(klass).constructor(cons || block || klass.method(:new))
83
86
  end
@@ -87,7 +90,6 @@ module Dry
87
90
  # @param [Class] klass
88
91
  #
89
92
  # @return [Dry::Types::Type]
90
- # @api public
91
93
  def Nominal(klass)
92
94
  Nominal.new(klass)
93
95
  end
@@ -102,7 +104,6 @@ module Dry
102
104
  # @param [Type] value_type Value type
103
105
  #
104
106
  # @return [Dry::Types::Map]
105
- # @api public
106
107
  def Map(key_type, value_type)
107
108
  Types['nominal.hash'].map(key_type, value_type)
108
109
  end
@@ -1,50 +1,102 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Types
5
+ # Common coercion functions used by the built-in `Params` and `JSON` types
6
+ #
7
+ # @api public
3
8
  module Coercions
4
9
  include Dry::Core::Constants
5
10
 
6
11
  # @param [String, Object] input
7
- # @return [nil] if the input is an empty string
8
- # @return [Object] otherwise the input object is returned
9
- def to_nil(input)
10
- input unless empty_str?(input)
12
+ #
13
+ # @return [nil] if the input is an empty string or nil
14
+ #
15
+ # @raise CoercionError
16
+ #
17
+ # @api public
18
+ def to_nil(input, &_block)
19
+ if input.nil? || empty_str?(input)
20
+ nil
21
+ elsif block_given?
22
+ yield
23
+ else
24
+ raise CoercionError, "#{input.inspect} is not nil"
25
+ end
11
26
  end
12
27
 
13
28
  # @param [#to_str, Object] input
29
+ #
14
30
  # @return [Date, Object]
31
+ #
15
32
  # @see Date.parse
16
- def to_date(input)
17
- return input unless input.respond_to?(:to_str)
18
- Date.parse(input)
19
- rescue ArgumentError, RangeError
20
- input
33
+ #
34
+ # @api public
35
+ def to_date(input, &block)
36
+ if input.respond_to?(:to_str)
37
+ begin
38
+ ::Date.parse(input)
39
+ rescue ArgumentError, RangeError => error
40
+ CoercionError.handle(error, &block)
41
+ end
42
+ elsif block_given?
43
+ yield
44
+ else
45
+ raise CoercionError, "#{input.inspect} is not a string"
46
+ end
21
47
  end
22
48
 
23
49
  # @param [#to_str, Object] input
50
+ #
24
51
  # @return [DateTime, Object]
52
+ #
25
53
  # @see DateTime.parse
26
- def to_date_time(input)
27
- return input unless input.respond_to?(:to_str)
28
- DateTime.parse(input)
29
- rescue ArgumentError
30
- input
54
+ #
55
+ # @api public
56
+ def to_date_time(input, &block)
57
+ if input.respond_to?(:to_str)
58
+ begin
59
+ ::DateTime.parse(input)
60
+ rescue ArgumentError => error
61
+ CoercionError.handle(error, &block)
62
+ end
63
+ elsif block_given?
64
+ yield
65
+ else
66
+ raise CoercionError, "#{input.inspect} is not a string"
67
+ end
31
68
  end
32
69
 
33
70
  # @param [#to_str, Object] input
71
+ #
34
72
  # @return [Time, Object]
73
+ #
35
74
  # @see Time.parse
36
- def to_time(input)
37
- return input unless input.respond_to?(:to_str)
38
- Time.parse(input)
39
- rescue ArgumentError
40
- input
75
+ #
76
+ # @api public
77
+ def to_time(input, &block)
78
+ if input.respond_to?(:to_str)
79
+ begin
80
+ ::Time.parse(input)
81
+ rescue ArgumentError => error
82
+ CoercionError.handle(error, &block)
83
+ end
84
+ elsif block_given?
85
+ yield
86
+ else
87
+ raise CoercionError, "#{input.inspect} is not a string"
88
+ end
41
89
  end
42
90
 
43
91
  private
44
92
 
45
93
  # Checks whether String is empty
94
+ #
46
95
  # @param [String, Object] value
96
+ #
47
97
  # @return [Boolean]
98
+ #
99
+ # @api private
48
100
  def empty_str?(value)
49
101
  EMPTY_STRING.eql?(value)
50
102
  end