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.
- 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
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'date'
|
2
4
|
require 'bigdecimal'
|
3
5
|
require 'bigdecimal/util'
|
@@ -6,14 +8,31 @@ require 'time'
|
|
6
8
|
module Dry
|
7
9
|
module Types
|
8
10
|
module Coercions
|
11
|
+
# JSON-specific coercions
|
12
|
+
#
|
13
|
+
# @api public
|
9
14
|
module JSON
|
10
15
|
extend Coercions
|
11
16
|
|
12
17
|
# @param [#to_d, Object] input
|
18
|
+
#
|
13
19
|
# @return [BigDecimal,nil]
|
14
|
-
|
15
|
-
|
16
|
-
|
20
|
+
#
|
21
|
+
# @raise CoercionError
|
22
|
+
#
|
23
|
+
# @api public
|
24
|
+
def self.to_decimal(input, &block)
|
25
|
+
if input.is_a?(::Float)
|
26
|
+
input.to_d
|
27
|
+
else
|
28
|
+
BigDecimal(input)
|
29
|
+
end
|
30
|
+
rescue ArgumentError, TypeError
|
31
|
+
if block_given?
|
32
|
+
yield
|
33
|
+
else
|
34
|
+
raise CoercionError, "#{input} cannot be coerced to decimal"
|
35
|
+
end
|
17
36
|
end
|
18
37
|
end
|
19
38
|
end
|
@@ -1,80 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'bigdecimal'
|
2
4
|
require 'bigdecimal/util'
|
3
5
|
|
4
6
|
module Dry
|
5
7
|
module Types
|
6
8
|
module Coercions
|
9
|
+
# Params-specific coercions
|
10
|
+
#
|
11
|
+
# @api public
|
7
12
|
module Params
|
8
13
|
TRUE_VALUES = %w[1 on On ON t true True TRUE T y yes Yes YES Y].freeze
|
9
14
|
FALSE_VALUES = %w[0 off Off OFF f false False FALSE F n no No NO N].freeze
|
10
|
-
BOOLEAN_MAP = ::Hash[
|
15
|
+
BOOLEAN_MAP = ::Hash[
|
16
|
+
TRUE_VALUES.product([true]) + FALSE_VALUES.product([false])
|
17
|
+
].freeze
|
11
18
|
|
12
19
|
extend Coercions
|
13
20
|
|
14
21
|
# @param [String, Object] input
|
22
|
+
#
|
15
23
|
# @return [Boolean,Object]
|
24
|
+
#
|
16
25
|
# @see TRUE_VALUES
|
17
26
|
# @see FALSE_VALUES
|
18
|
-
|
19
|
-
|
27
|
+
#
|
28
|
+
# @raise CoercionError
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def self.to_true(input, &_block)
|
32
|
+
BOOLEAN_MAP.fetch(input.to_s) do
|
33
|
+
if block_given?
|
34
|
+
yield
|
35
|
+
else
|
36
|
+
raise CoercionError, "#{input} cannot be coerced to true"
|
37
|
+
end
|
38
|
+
end
|
20
39
|
end
|
21
40
|
|
22
41
|
# @param [String, Object] input
|
42
|
+
#
|
23
43
|
# @return [Boolean,Object]
|
44
|
+
#
|
24
45
|
# @see TRUE_VALUES
|
25
46
|
# @see FALSE_VALUES
|
26
|
-
|
27
|
-
|
47
|
+
#
|
48
|
+
# @raise CoercionError
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def self.to_false(input, &_block)
|
52
|
+
BOOLEAN_MAP.fetch(input.to_s) do
|
53
|
+
if block_given?
|
54
|
+
yield
|
55
|
+
else
|
56
|
+
raise CoercionError, "#{input} cannot be coerced to false"
|
57
|
+
end
|
58
|
+
end
|
28
59
|
end
|
29
60
|
|
30
61
|
# @param [#to_int, #to_i, Object] input
|
62
|
+
#
|
31
63
|
# @return [Integer, nil, Object]
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
64
|
+
#
|
65
|
+
# @raise CoercionError
|
66
|
+
#
|
67
|
+
# @api public
|
68
|
+
def self.to_int(input, &block)
|
69
|
+
if input.is_a? String
|
36
70
|
Integer(input, 10)
|
37
71
|
else
|
38
72
|
Integer(input)
|
39
73
|
end
|
40
|
-
rescue ArgumentError, TypeError
|
41
|
-
|
74
|
+
rescue ArgumentError, TypeError => error
|
75
|
+
CoercionError.handle(error, &block)
|
42
76
|
end
|
43
77
|
|
44
78
|
# @param [#to_f, Object] input
|
79
|
+
#
|
45
80
|
# @return [Float, nil, Object]
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
rescue ArgumentError, TypeError
|
53
|
-
|
81
|
+
#
|
82
|
+
# @raise CoercionError
|
83
|
+
#
|
84
|
+
# @api public
|
85
|
+
def self.to_float(input, &block)
|
86
|
+
Float(input)
|
87
|
+
rescue ArgumentError, TypeError => error
|
88
|
+
CoercionError.handle(error, &block)
|
54
89
|
end
|
55
90
|
|
56
91
|
# @param [#to_d, Object] input
|
92
|
+
#
|
57
93
|
# @return [BigDecimal, nil, Object]
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
94
|
+
#
|
95
|
+
# @raise CoercionError
|
96
|
+
#
|
97
|
+
# @api public
|
98
|
+
def self.to_decimal(input, &block)
|
99
|
+
to_float(input) do
|
100
|
+
if block_given?
|
101
|
+
return yield
|
102
|
+
else
|
103
|
+
raise CoercionError, "#{input.inspect} cannot be coerced to decimal"
|
104
|
+
end
|
65
105
|
end
|
106
|
+
|
107
|
+
input.to_d
|
66
108
|
end
|
67
109
|
|
68
110
|
# @param [Array, String, Object] input
|
111
|
+
#
|
69
112
|
# @return [Array, Object]
|
70
|
-
|
71
|
-
|
113
|
+
#
|
114
|
+
# @raise CoercionError
|
115
|
+
#
|
116
|
+
# @api public
|
117
|
+
def self.to_ary(input, &_block)
|
118
|
+
if empty_str?(input)
|
119
|
+
[]
|
120
|
+
elsif input.is_a?(::Array)
|
121
|
+
input
|
122
|
+
elsif block_given?
|
123
|
+
yield
|
124
|
+
else
|
125
|
+
raise CoercionError, "#{input.inspect} cannot be coerced to array"
|
126
|
+
end
|
72
127
|
end
|
73
128
|
|
74
129
|
# @param [Hash, String, Object] input
|
130
|
+
#
|
75
131
|
# @return [Hash, Object]
|
76
|
-
|
77
|
-
|
132
|
+
#
|
133
|
+
# @raise CoercionError
|
134
|
+
#
|
135
|
+
# @api public
|
136
|
+
def self.to_hash(input, &_block)
|
137
|
+
if empty_str?(input)
|
138
|
+
{}
|
139
|
+
elsif input.is_a?(::Hash)
|
140
|
+
input
|
141
|
+
elsif block_given?
|
142
|
+
yield
|
143
|
+
else
|
144
|
+
raise CoercionError, "#{input.inspect} cannot be coerced to hash"
|
145
|
+
end
|
78
146
|
end
|
79
147
|
end
|
80
148
|
end
|
data/lib/dry/types/compiler.rb
CHANGED
@@ -1,6 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/core/deprecations'
|
4
|
+
|
1
5
|
module Dry
|
2
6
|
module Types
|
7
|
+
# @api private
|
3
8
|
class Compiler
|
9
|
+
extend ::Dry::Core::Deprecations[:'dry-types']
|
10
|
+
|
4
11
|
attr_reader :registry
|
5
12
|
|
6
13
|
def initialize(registry)
|
@@ -13,29 +20,29 @@ module Dry
|
|
13
20
|
|
14
21
|
def visit(node)
|
15
22
|
type, body = node
|
16
|
-
send(:"visit_#{
|
23
|
+
send(:"visit_#{type}", body)
|
17
24
|
end
|
18
25
|
|
19
26
|
def visit_constrained(node)
|
20
|
-
nominal, rule
|
21
|
-
|
27
|
+
nominal, rule = node
|
28
|
+
type = visit(nominal)
|
29
|
+
type.constrained_type.new(type, rule: visit_rule(rule))
|
22
30
|
end
|
23
31
|
|
24
32
|
def visit_constructor(node)
|
25
|
-
nominal,
|
26
|
-
fn = Dry::Types::FnContainer[fn_register_name]
|
33
|
+
nominal, fn = node
|
27
34
|
primitive = visit(nominal)
|
28
|
-
|
35
|
+
primitive.constructor(compile_fn(fn))
|
29
36
|
end
|
30
37
|
|
31
|
-
def
|
32
|
-
|
33
|
-
Types::Safe.new(visit(ast), meta: meta)
|
38
|
+
def visit_lax(node)
|
39
|
+
Types::Lax.new(visit(node))
|
34
40
|
end
|
41
|
+
deprecate(:visit_safe, :visit_lax)
|
35
42
|
|
36
43
|
def visit_nominal(node)
|
37
44
|
type, meta = node
|
38
|
-
nominal_name = "nominal.#{
|
45
|
+
nominal_name = "nominal.#{Types.identifier(type)}"
|
39
46
|
|
40
47
|
if registry.registered?(nominal_name)
|
41
48
|
registry[nominal_name].meta(meta)
|
@@ -95,8 +102,8 @@ module Dry
|
|
95
102
|
end
|
96
103
|
|
97
104
|
def visit_enum(node)
|
98
|
-
type, mapping
|
99
|
-
Enum.new(visit(type), mapping: mapping
|
105
|
+
type, mapping = node
|
106
|
+
Enum.new(visit(type), mapping: mapping)
|
100
107
|
end
|
101
108
|
|
102
109
|
def visit_map(node)
|
@@ -107,6 +114,22 @@ module Dry
|
|
107
114
|
def visit_any(meta)
|
108
115
|
registry['any'].meta(meta)
|
109
116
|
end
|
117
|
+
|
118
|
+
def compile_fn(fn)
|
119
|
+
type, *node = fn
|
120
|
+
|
121
|
+
case type
|
122
|
+
when :id
|
123
|
+
Dry::Types::FnContainer[node.fetch(0)]
|
124
|
+
when :callable
|
125
|
+
node.fetch(0)
|
126
|
+
when :method
|
127
|
+
target, method = node
|
128
|
+
target.method(method)
|
129
|
+
else
|
130
|
+
raise ArgumentError, "Cannot build callable from #{fn.inspect}"
|
131
|
+
end
|
132
|
+
end
|
110
133
|
end
|
111
134
|
end
|
112
135
|
end
|
@@ -1,94 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dry/types/decorator'
|
2
4
|
require 'dry/types/constraints'
|
3
5
|
require 'dry/types/constrained/coercible'
|
4
6
|
|
5
7
|
module Dry
|
6
8
|
module Types
|
9
|
+
# Constrained types apply rules to the input
|
10
|
+
#
|
11
|
+
# @api public
|
7
12
|
class Constrained
|
8
13
|
include Type
|
9
14
|
include Decorator
|
10
15
|
include Builder
|
11
16
|
include Printable
|
12
|
-
include Dry::Equalizer(:type, :
|
17
|
+
include Dry::Equalizer(:type, :rule, inspect: false)
|
13
18
|
|
14
19
|
# @return [Dry::Logic::Rule]
|
15
20
|
attr_reader :rule
|
16
21
|
|
17
22
|
# @param [Type] type
|
23
|
+
#
|
18
24
|
# @param [Hash] options
|
25
|
+
#
|
26
|
+
# @api public
|
19
27
|
def initialize(type, options)
|
20
28
|
super
|
21
29
|
@rule = options.fetch(:rule)
|
22
30
|
end
|
23
31
|
|
24
|
-
# @
|
32
|
+
# @api private
|
33
|
+
#
|
25
34
|
# @return [Object]
|
26
|
-
#
|
27
|
-
|
28
|
-
|
35
|
+
#
|
36
|
+
# @api public
|
37
|
+
def call_unsafe(input)
|
38
|
+
result = rule.(input)
|
39
|
+
|
40
|
+
if result.success?
|
41
|
+
type.call_unsafe(input)
|
42
|
+
else
|
29
43
|
raise ConstraintError.new(result, input)
|
30
|
-
|
44
|
+
end
|
31
45
|
end
|
32
|
-
|
33
|
-
|
34
|
-
#
|
35
|
-
# @
|
36
|
-
#
|
37
|
-
# @
|
38
|
-
|
39
|
-
|
46
|
+
|
47
|
+
# @api private
|
48
|
+
#
|
49
|
+
# @return [Object]
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def call_safe(input, &block)
|
53
|
+
if rule[input]
|
54
|
+
type.call_safe(input, &block)
|
55
|
+
else
|
56
|
+
yield
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Safe coercion attempt. It is similar to #call with a
|
61
|
+
# block given but returns a Result instance with metadata
|
62
|
+
# about errors (if any).
|
63
|
+
#
|
64
|
+
# @overload try(input)
|
65
|
+
# @param [Object] input
|
66
|
+
# @return [Logic::Result]
|
67
|
+
#
|
68
|
+
# @overload try(input)
|
69
|
+
# @param [Object] input
|
70
|
+
# @yieldparam [Failure] failure
|
71
|
+
# @yieldreturn [Object]
|
72
|
+
# @return [Object]
|
73
|
+
#
|
74
|
+
# @api public
|
40
75
|
def try(input, &block)
|
41
76
|
result = rule.(input)
|
42
77
|
|
43
78
|
if result.success?
|
44
79
|
type.try(input, &block)
|
45
80
|
else
|
46
|
-
failure = failure(input, result)
|
47
|
-
|
81
|
+
failure = failure(input, ConstraintError.new(result, input))
|
82
|
+
block_given? ? yield(failure) : failure
|
48
83
|
end
|
49
84
|
end
|
50
85
|
|
51
|
-
# @param [Object] value
|
52
|
-
# @return [Boolean]
|
53
|
-
def valid?(value)
|
54
|
-
rule.(value).success? && type.valid?(value)
|
55
|
-
end
|
56
|
-
|
57
86
|
# @param [Hash] options
|
58
87
|
# The options hash provided to {Types.Rule} and combined
|
59
88
|
# using {&} with previous {#rule}
|
89
|
+
#
|
60
90
|
# @return [Constrained]
|
91
|
+
#
|
61
92
|
# @see Dry::Logic::Operators#and
|
93
|
+
#
|
94
|
+
# @api public
|
62
95
|
def constrained(options)
|
63
96
|
with(rule: rule & Types.Rule(options))
|
64
97
|
end
|
65
98
|
|
66
99
|
# @return [true]
|
100
|
+
#
|
101
|
+
# @api public
|
67
102
|
def constrained?
|
68
103
|
true
|
69
104
|
end
|
70
105
|
|
71
106
|
# @param [Object] value
|
107
|
+
#
|
72
108
|
# @return [Boolean]
|
109
|
+
#
|
110
|
+
# @api public
|
73
111
|
def ===(value)
|
74
112
|
valid?(value)
|
75
113
|
end
|
76
114
|
|
77
|
-
#
|
115
|
+
# Build lax type. Constraints are not applicable to lax types hence unwrapping
|
78
116
|
#
|
117
|
+
# @return [Lax]
|
118
|
+
# @api public
|
119
|
+
def lax
|
120
|
+
type.lax
|
121
|
+
end
|
122
|
+
|
79
123
|
# @see Nominal#to_ast
|
124
|
+
# @api public
|
80
125
|
def to_ast(meta: true)
|
81
|
-
[:constrained, [type.to_ast(meta: meta),
|
82
|
-
rule.to_ast,
|
83
|
-
meta ? self.meta : EMPTY_HASH]]
|
126
|
+
[:constrained, [type.to_ast(meta: meta), rule.to_ast]]
|
84
127
|
end
|
85
128
|
|
86
129
|
private
|
87
130
|
|
88
131
|
# @param [Object] response
|
132
|
+
#
|
89
133
|
# @return [Boolean]
|
134
|
+
#
|
135
|
+
# @api private
|
90
136
|
def decorate?(response)
|
91
|
-
super || response.
|
137
|
+
super || response.is_a?(Constructor)
|
92
138
|
end
|
93
139
|
end
|
94
140
|
end
|