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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +18 -2
- data/.travis.yml +4 -5
- data/.yardopts +6 -2
- data/CHANGELOG.md +69 -1
- data/Gemfile +3 -0
- data/README.md +2 -1
- data/Rakefile +2 -0
- data/benchmarks/hash_schemas.rb +2 -0
- data/benchmarks/lax_schema.rb +16 -0
- data/benchmarks/profile_invalid_input.rb +15 -0
- data/benchmarks/profile_lax_schema_valid.rb +16 -0
- data/benchmarks/profile_valid_input.rb +15 -0
- data/benchmarks/schema_valid_vs_invalid.rb +21 -0
- data/benchmarks/setup.rb +17 -0
- data/dry-types.gemspec +4 -2
- data/lib/dry-types.rb +2 -0
- data/lib/dry/types.rb +51 -13
- data/lib/dry/types/any.rb +21 -10
- data/lib/dry/types/array.rb +11 -1
- data/lib/dry/types/array/member.rb +65 -13
- data/lib/dry/types/builder.rb +48 -4
- data/lib/dry/types/builder_methods.rb +9 -8
- data/lib/dry/types/coercions.rb +71 -19
- data/lib/dry/types/coercions/json.rb +22 -3
- data/lib/dry/types/coercions/params.rb +98 -30
- data/lib/dry/types/compiler.rb +35 -12
- data/lib/dry/types/constrained.rb +73 -27
- data/lib/dry/types/constrained/coercible.rb +36 -6
- data/lib/dry/types/constraints.rb +15 -1
- data/lib/dry/types/constructor.rb +90 -43
- data/lib/dry/types/constructor/function.rb +201 -0
- data/lib/dry/types/container.rb +5 -0
- data/lib/dry/types/core.rb +7 -5
- data/lib/dry/types/decorator.rb +36 -9
- data/lib/dry/types/default.rb +48 -16
- data/lib/dry/types/enum.rb +30 -16
- data/lib/dry/types/errors.rb +73 -7
- data/lib/dry/types/extensions.rb +2 -0
- data/lib/dry/types/extensions/maybe.rb +43 -4
- data/lib/dry/types/fn_container.rb +5 -0
- data/lib/dry/types/hash.rb +22 -3
- data/lib/dry/types/hash/constructor.rb +13 -0
- data/lib/dry/types/inflector.rb +2 -0
- data/lib/dry/types/json.rb +4 -6
- data/lib/dry/types/{safe.rb → lax.rb} +34 -17
- data/lib/dry/types/map.rb +63 -29
- data/lib/dry/types/meta.rb +51 -0
- data/lib/dry/types/module.rb +7 -2
- data/lib/dry/types/nominal.rb +105 -13
- data/lib/dry/types/options.rb +12 -25
- data/lib/dry/types/params.rb +5 -3
- data/lib/dry/types/printable.rb +5 -1
- data/lib/dry/types/printer.rb +58 -57
- data/lib/dry/types/result.rb +26 -0
- data/lib/dry/types/schema.rb +169 -66
- data/lib/dry/types/schema/key.rb +34 -39
- data/lib/dry/types/spec/types.rb +41 -1
- data/lib/dry/types/sum.rb +70 -21
- data/lib/dry/types/type.rb +49 -0
- data/lib/dry/types/version.rb +3 -1
- metadata +14 -12
data/lib/dry/types/any.rb
CHANGED
@@ -1,36 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Dry
|
2
4
|
module Types
|
3
|
-
Any
|
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
|
-
|
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
|
43
|
+
end
|
44
|
+
|
45
|
+
Any = AnyClass.new
|
35
46
|
end
|
36
47
|
end
|
data/lib/dry/types/array.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
#
|
25
|
+
#
|
18
26
|
# @return [Array]
|
19
|
-
|
20
|
-
|
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 [
|
25
|
-
# @return [
|
26
|
-
|
27
|
-
|
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
|
37
|
-
|
38
|
-
|
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
|
-
|
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
|
-
#
|
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]]
|
data/lib/dry/types/builder.rb
CHANGED
@@ -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
|
-
#
|
77
|
-
|
78
|
-
|
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/
|
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
|
-
#
|
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
|
data/lib/dry/types/coercions.rb
CHANGED
@@ -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
|
-
#
|
8
|
-
# @return [
|
9
|
-
|
10
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|