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,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