dry-types 1.1.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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.travis.yml +10 -4
- data/CHANGELOG.md +68 -0
- data/Gemfile +8 -6
- data/README.md +2 -2
- data/docsite/source/array-with-member.html.md +13 -0
- data/docsite/source/built-in-types.html.md +116 -0
- data/docsite/source/constraints.html.md +31 -0
- data/docsite/source/custom-types.html.md +93 -0
- data/docsite/source/default-values.html.md +91 -0
- data/docsite/source/enum.html.md +69 -0
- data/docsite/source/getting-started.html.md +57 -0
- data/docsite/source/hash-schemas.html.md +169 -0
- data/docsite/source/index.html.md +155 -0
- data/docsite/source/map.html.md +17 -0
- data/docsite/source/optional-values.html.md +96 -0
- data/docsite/source/sum.html.md +21 -0
- data/lib/dry/types.rb +7 -2
- data/lib/dry/types/array/member.rb +1 -1
- data/lib/dry/types/builder_methods.rb +18 -8
- data/lib/dry/types/constructor.rb +2 -29
- data/lib/dry/types/decorator.rb +1 -1
- data/lib/dry/types/extensions.rb +4 -0
- data/lib/dry/types/extensions/monads.rb +29 -0
- data/lib/dry/types/hash.rb +2 -1
- data/lib/dry/types/lax.rb +1 -1
- data/lib/dry/types/params.rb +5 -0
- data/lib/dry/types/predicate_inferrer.rb +197 -0
- data/lib/dry/types/predicate_registry.rb +34 -0
- data/lib/dry/types/primitive_inferrer.rb +97 -0
- data/lib/dry/types/printer.rb +7 -3
- data/lib/dry/types/version.rb +1 -1
- metadata +48 -28
@@ -0,0 +1,17 @@
|
|
1
|
+
---
|
2
|
+
title: Map
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-types
|
5
|
+
---
|
6
|
+
|
7
|
+
`Map` describes a homogeneous hashmap. This means only types of keys and values are known. You can simply imagine a map input as a list of key-value pairs.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
int_float_hash = Types::Hash.map(Types::Integer, Types::Float)
|
11
|
+
int_float_hash[100 => 300.0, 42 => 70.0]
|
12
|
+
# => {100=>300.0, 42=>70.0}
|
13
|
+
|
14
|
+
# Only accepts mappings of integers to floats
|
15
|
+
int_float_hash[name: 'Jane']
|
16
|
+
# => Dry::Types::MapError: input key :name is invalid: type?(Integer, :name)
|
17
|
+
```
|
@@ -0,0 +1,96 @@
|
|
1
|
+
---
|
2
|
+
title: Type Attributes
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-types
|
5
|
+
---
|
6
|
+
|
7
|
+
Types themselves have optional attributes you can apply to get further functionality.
|
8
|
+
|
9
|
+
### Append `.optional` to a _Type_ to allow `nil`
|
10
|
+
|
11
|
+
By default, nil values raise an error:
|
12
|
+
|
13
|
+
``` ruby
|
14
|
+
Types::Strict::String[nil]
|
15
|
+
# => raises Dry::Types::ConstraintError
|
16
|
+
```
|
17
|
+
|
18
|
+
Add `.optional` and `nil` values become valid:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
optional_string = Types::Strict::String.optional
|
22
|
+
|
23
|
+
optional_string[nil]
|
24
|
+
# => nil
|
25
|
+
optional_string['something']
|
26
|
+
# => "something"
|
27
|
+
optional_string[123]
|
28
|
+
# raises Dry::Types::ConstraintError
|
29
|
+
```
|
30
|
+
|
31
|
+
`Types::String.optional` is just syntactic sugar for `Types::Strict::Nil | Types::Strict::String`.
|
32
|
+
|
33
|
+
### Handle optional values using Monads
|
34
|
+
|
35
|
+
The [dry-monads gem](/gems/dry-monads/) provides another approach to handling optional values by returning a [_Monad_](/gems/dry-monads/) object. This allows you to pass your type to a `Maybe(x)` block that only executes if `x` returns `Some` or `None`.
|
36
|
+
|
37
|
+
> NOTE: Requires the [dry-monads gem](/gems/dry-monads/) to be loaded.
|
38
|
+
|
39
|
+
1. Load the `:maybe` extension in your application.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
require 'dry-types'
|
43
|
+
|
44
|
+
Dry::Types.load_extensions(:maybe)
|
45
|
+
module Types
|
46
|
+
include Dry.Types()
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
2. Append `.maybe` to a _Type_ to return a _Monad_ object
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
x = Types::Maybe::Strict::Integer[nil]
|
54
|
+
Maybe(x) { puts(x) }
|
55
|
+
|
56
|
+
x = Types::Maybe::Coercible::String[nil]
|
57
|
+
Maybe(x) { puts(x) }
|
58
|
+
|
59
|
+
x = Types::Maybe::Strict::Integer[123]
|
60
|
+
Maybe(x) { puts(x) }
|
61
|
+
|
62
|
+
x = Types::Maybe::Strict::String[123]
|
63
|
+
Maybe(x) { puts(x) }
|
64
|
+
```
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
Types::Maybe::Strict::Integer[nil] # None
|
68
|
+
Types::Maybe::Strict::Integer[123] # Some(123)
|
69
|
+
|
70
|
+
Types::Maybe::Coercible::Float[nil] # None
|
71
|
+
Types::Maybe::Coercible::Float['12.3'] # Some(12.3)
|
72
|
+
|
73
|
+
# 'Maybe' types can also accessed by calling '.maybe' on a regular type:
|
74
|
+
Types::Strict::Integer.maybe # equivalent to Types::Maybe::Strict::Integer
|
75
|
+
```
|
76
|
+
|
77
|
+
You can define your own optional types:
|
78
|
+
|
79
|
+
``` ruby
|
80
|
+
maybe_string = Types::Strict::String.maybe
|
81
|
+
|
82
|
+
maybe_string[nil]
|
83
|
+
# => None
|
84
|
+
|
85
|
+
maybe_string[nil].fmap(&:upcase)
|
86
|
+
# => None
|
87
|
+
|
88
|
+
maybe_string['something']
|
89
|
+
# => Some('something')
|
90
|
+
|
91
|
+
maybe_string['something'].fmap(&:upcase)
|
92
|
+
# => Some('SOMETHING')
|
93
|
+
|
94
|
+
maybe_string['something'].fmap(&:upcase).value_or('NOTHING')
|
95
|
+
# => "SOMETHING"
|
96
|
+
```
|
@@ -0,0 +1,21 @@
|
|
1
|
+
---
|
2
|
+
title: Sum
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-types
|
5
|
+
order: 7
|
6
|
+
---
|
7
|
+
|
8
|
+
You can specify sum types using `|` operator, it is an explicit way of defining what the valid types of a value are.
|
9
|
+
|
10
|
+
For example `dry-types` defines the `Bool` type which is a sum consisting of the `True` and `False` types, expressed as `Types::True | Types::False`.
|
11
|
+
|
12
|
+
Another common case is defining that something can be either `nil` or something else:
|
13
|
+
|
14
|
+
``` ruby
|
15
|
+
nil_or_string = Types::Nil | Types::String
|
16
|
+
|
17
|
+
nil_or_string[nil] # => nil
|
18
|
+
nil_or_string["hello"] # => "hello"
|
19
|
+
|
20
|
+
nil_or_string[123] # raises Dry::Types::ConstraintError
|
21
|
+
```
|
data/lib/dry/types.rb
CHANGED
@@ -89,7 +89,7 @@ module Dry
|
|
89
89
|
def self.[](name)
|
90
90
|
type_map.fetch_or_store(name) do
|
91
91
|
case name
|
92
|
-
when String
|
92
|
+
when ::String
|
93
93
|
result = name.match(TYPE_SPEC_REGEX)
|
94
94
|
|
95
95
|
if result
|
@@ -98,7 +98,12 @@ module Dry
|
|
98
98
|
else
|
99
99
|
container[name]
|
100
100
|
end
|
101
|
-
when Class
|
101
|
+
when ::Class
|
102
|
+
warn(<<~DEPRECATION)
|
103
|
+
Using Dry::Types.[] with a class is deprecated, please use string identifiers: Dry::Types[Integer] -> Dry::Types['integer'].
|
104
|
+
If you're using dry-struct this means changing `attribute :counter, Integer` to `attribute :counter, Dry::Types['integer']` or to `attribute :counter, 'integer'`.
|
105
|
+
DEPRECATION
|
106
|
+
|
102
107
|
type_name = identifier(name)
|
103
108
|
|
104
109
|
if container.key?(type_name)
|
@@ -25,7 +25,7 @@ module Dry
|
|
25
25
|
#
|
26
26
|
# @return [Dry::Types::Array]
|
27
27
|
def Array(type)
|
28
|
-
|
28
|
+
Strict(::Array).of(type)
|
29
29
|
end
|
30
30
|
|
31
31
|
# Build a hash schema
|
@@ -34,7 +34,7 @@ module Dry
|
|
34
34
|
#
|
35
35
|
# @return [Dry::Types::Array]
|
36
36
|
def Hash(type_map)
|
37
|
-
|
37
|
+
Strict(::Hash).schema(type_map)
|
38
38
|
end
|
39
39
|
|
40
40
|
# Build a type which values are instances of a given class
|
@@ -49,7 +49,7 @@ module Dry
|
|
49
49
|
#
|
50
50
|
# @return [Dry::Types::Type]
|
51
51
|
def Instance(klass)
|
52
|
-
Nominal
|
52
|
+
Nominal(klass).constrained(type: klass)
|
53
53
|
end
|
54
54
|
alias_method :Strict, :Instance
|
55
55
|
|
@@ -60,7 +60,7 @@ module Dry
|
|
60
60
|
#
|
61
61
|
# @return [Dry::Types::Type]
|
62
62
|
def Value(value)
|
63
|
-
Nominal
|
63
|
+
Nominal(value.class).constrained(eql: value)
|
64
64
|
end
|
65
65
|
|
66
66
|
# Build a type with a single value
|
@@ -70,7 +70,7 @@ module Dry
|
|
70
70
|
#
|
71
71
|
# @return [Dry::Types::Type]
|
72
72
|
def Constant(object)
|
73
|
-
Nominal
|
73
|
+
Nominal(object.class).constrained(is: object)
|
74
74
|
end
|
75
75
|
|
76
76
|
# Build a constructor type
|
@@ -82,7 +82,11 @@ module Dry
|
|
82
82
|
#
|
83
83
|
# @return [Dry::Types::Type]
|
84
84
|
def Constructor(klass, cons = nil, &block)
|
85
|
-
|
85
|
+
if klass.is_a?(Type)
|
86
|
+
klass.constructor(cons || block || klass.method(:new))
|
87
|
+
else
|
88
|
+
Nominal(klass).constructor(cons || block || klass.method(:new))
|
89
|
+
end
|
86
90
|
end
|
87
91
|
|
88
92
|
# Build a nominal type
|
@@ -91,7 +95,13 @@ module Dry
|
|
91
95
|
#
|
92
96
|
# @return [Dry::Types::Type]
|
93
97
|
def Nominal(klass)
|
94
|
-
|
98
|
+
if klass <= ::Array
|
99
|
+
Array.new(klass)
|
100
|
+
elsif klass <= ::Hash
|
101
|
+
Hash.new(klass)
|
102
|
+
else
|
103
|
+
Nominal.new(klass)
|
104
|
+
end
|
95
105
|
end
|
96
106
|
|
97
107
|
# Build a map type
|
@@ -105,7 +115,7 @@ module Dry
|
|
105
115
|
#
|
106
116
|
# @return [Dry::Types::Map]
|
107
117
|
def Map(key_type, value_type)
|
108
|
-
|
118
|
+
Nominal(::Hash).map(key_type, value_type)
|
109
119
|
end
|
110
120
|
|
111
121
|
# Builds a constrained nominal type accepting any value that
|
@@ -12,15 +12,13 @@ module Dry
|
|
12
12
|
class Constructor < Nominal
|
13
13
|
include Dry::Equalizer(:type, :options, inspect: false)
|
14
14
|
|
15
|
-
private :meta
|
16
|
-
|
17
15
|
# @return [#call]
|
18
16
|
attr_reader :fn
|
19
17
|
|
20
18
|
# @return [Type]
|
21
19
|
attr_reader :type
|
22
20
|
|
23
|
-
undef :constrained
|
21
|
+
undef :constrained?, :meta, :optional?, :primitive, :default?, :name
|
24
22
|
|
25
23
|
# @param [Builder, Object] input
|
26
24
|
# @param [Hash] options
|
@@ -46,31 +44,6 @@ module Dry
|
|
46
44
|
super(type, **options, fn: fn)
|
47
45
|
end
|
48
46
|
|
49
|
-
# Return the inner type's primitive
|
50
|
-
#
|
51
|
-
# @return [Class]
|
52
|
-
#
|
53
|
-
# @api public
|
54
|
-
def primitive
|
55
|
-
type.primitive
|
56
|
-
end
|
57
|
-
|
58
|
-
# Return the inner type's name
|
59
|
-
#
|
60
|
-
# @return [String]
|
61
|
-
#
|
62
|
-
# @api public
|
63
|
-
def name
|
64
|
-
type.name
|
65
|
-
end
|
66
|
-
|
67
|
-
# @return [Boolean]
|
68
|
-
#
|
69
|
-
# @api public
|
70
|
-
def default?
|
71
|
-
type.default?
|
72
|
-
end
|
73
|
-
|
74
47
|
# @return [Object]
|
75
48
|
#
|
76
49
|
# @api private
|
@@ -182,7 +155,7 @@ module Dry
|
|
182
155
|
# @api private
|
183
156
|
def method_missing(method, *args, &block)
|
184
157
|
if type.respond_to?(method)
|
185
|
-
response = type.
|
158
|
+
response = type.public_send(method, *args, &block)
|
186
159
|
|
187
160
|
if response.is_a?(Type) && type.class == response.class
|
188
161
|
response.constructor_type.new(response, options)
|
data/lib/dry/types/decorator.rb
CHANGED
data/lib/dry/types/extensions.rb
CHANGED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/monads/result'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Types
|
7
|
+
# Monad extension for Result
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
class Result
|
11
|
+
include Dry::Monads::Result::Mixin
|
12
|
+
|
13
|
+
# Turn result into a monad
|
14
|
+
#
|
15
|
+
# This makes result objects work with dry-monads (or anything with a compatible interface)
|
16
|
+
#
|
17
|
+
# @return [Dry::Monads::Success,Dry::Monads::Failure]
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
def to_monad
|
21
|
+
if success?
|
22
|
+
Success(input)
|
23
|
+
else
|
24
|
+
Failure([error, input])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/dry/types/hash.rb
CHANGED
data/lib/dry/types/lax.rb
CHANGED
data/lib/dry/types/params.rb
CHANGED
@@ -55,5 +55,10 @@ module Dry
|
|
55
55
|
register('params.symbol') do
|
56
56
|
self['nominal.symbol'].constructor(Coercions::Params.method(:to_symbol))
|
57
57
|
end
|
58
|
+
|
59
|
+
COERCIBLE.each_key do |name|
|
60
|
+
next if name.equal?(:string)
|
61
|
+
register("optional.params.#{name}", self['params.nil'] | self["params.#{name}"])
|
62
|
+
end
|
58
63
|
end
|
59
64
|
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/core/cache'
|
4
|
+
require 'dry/types/predicate_registry'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Types
|
8
|
+
# PredicateInferrer returns the list of predicates used by a type.
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class PredicateInferrer
|
12
|
+
extend Core::Cache
|
13
|
+
|
14
|
+
TYPE_TO_PREDICATE = {
|
15
|
+
DateTime => :date_time?,
|
16
|
+
FalseClass => :false?,
|
17
|
+
Integer => :int?,
|
18
|
+
NilClass => :nil?,
|
19
|
+
String => :str?,
|
20
|
+
TrueClass => :true?,
|
21
|
+
BigDecimal => :decimal?
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
REDUCED_TYPES = {
|
25
|
+
[[[:true?], [:false?]]] => %i[bool?]
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
HASH = %i[hash?].freeze
|
29
|
+
|
30
|
+
ARRAY = %i[array?].freeze
|
31
|
+
|
32
|
+
NIL = %i[nil?].freeze
|
33
|
+
|
34
|
+
# Compiler reduces type AST into a list of predicates
|
35
|
+
#
|
36
|
+
# @api private
|
37
|
+
class Compiler
|
38
|
+
# @return [PredicateRegistry]
|
39
|
+
# @api private
|
40
|
+
attr_reader :registry
|
41
|
+
|
42
|
+
# @api private
|
43
|
+
def initialize(registry)
|
44
|
+
@registry = registry
|
45
|
+
end
|
46
|
+
|
47
|
+
# @api private
|
48
|
+
def infer_predicate(type)
|
49
|
+
[TYPE_TO_PREDICATE.fetch(type) { :"#{type.name.split('::').last.downcase}?" }]
|
50
|
+
end
|
51
|
+
|
52
|
+
# @api private
|
53
|
+
def visit(node)
|
54
|
+
meth, rest = node
|
55
|
+
public_send(:"visit_#{meth}", rest)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @api private
|
59
|
+
def visit_nominal(node)
|
60
|
+
type = node[0]
|
61
|
+
predicate = infer_predicate(type)
|
62
|
+
|
63
|
+
if registry.key?(predicate[0])
|
64
|
+
predicate
|
65
|
+
else
|
66
|
+
[type?: type]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# @api private
|
71
|
+
def visit_hash(_)
|
72
|
+
HASH
|
73
|
+
end
|
74
|
+
|
75
|
+
# @api private
|
76
|
+
def visit_array(_)
|
77
|
+
ARRAY
|
78
|
+
end
|
79
|
+
|
80
|
+
# @api private
|
81
|
+
def visit_lax(node)
|
82
|
+
visit(node)
|
83
|
+
end
|
84
|
+
|
85
|
+
# @api private
|
86
|
+
def visit_constructor(node)
|
87
|
+
other, * = node
|
88
|
+
visit(other)
|
89
|
+
end
|
90
|
+
|
91
|
+
# @api private
|
92
|
+
def visit_enum(node)
|
93
|
+
other, * = node
|
94
|
+
visit(other)
|
95
|
+
end
|
96
|
+
|
97
|
+
# @api private
|
98
|
+
def visit_sum(node)
|
99
|
+
left_node, right_node, = node
|
100
|
+
left = visit(left_node)
|
101
|
+
right = visit(right_node)
|
102
|
+
|
103
|
+
if left.eql?(NIL)
|
104
|
+
right
|
105
|
+
else
|
106
|
+
[[left, right]]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# @api private
|
111
|
+
def visit_constrained(node)
|
112
|
+
other, rules = node
|
113
|
+
predicates = visit(rules)
|
114
|
+
|
115
|
+
if predicates.empty?
|
116
|
+
visit(other)
|
117
|
+
else
|
118
|
+
[*visit(other), *merge_predicates(predicates)]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# @api private
|
123
|
+
def visit_any(_)
|
124
|
+
EMPTY_ARRAY
|
125
|
+
end
|
126
|
+
|
127
|
+
# @api private
|
128
|
+
def visit_and(node)
|
129
|
+
left, right = node
|
130
|
+
visit(left) + visit(right)
|
131
|
+
end
|
132
|
+
|
133
|
+
# @api private
|
134
|
+
def visit_predicate(node)
|
135
|
+
pred, args = node
|
136
|
+
|
137
|
+
if pred.equal?(:type?)
|
138
|
+
EMPTY_ARRAY
|
139
|
+
elsif registry.key?(pred)
|
140
|
+
*curried, _ = args
|
141
|
+
values = curried.map { |_, v| v }
|
142
|
+
|
143
|
+
if values.empty?
|
144
|
+
[pred]
|
145
|
+
else
|
146
|
+
[pred => values[0]]
|
147
|
+
end
|
148
|
+
else
|
149
|
+
EMPTY_ARRAY
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
# @api private
|
156
|
+
def merge_predicates(nodes)
|
157
|
+
preds, merged = nodes.each_with_object([[], {}]) do |predicate, (ps, h)|
|
158
|
+
if predicate.is_a?(::Hash)
|
159
|
+
h.update(predicate)
|
160
|
+
else
|
161
|
+
ps << predicate
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
merged.empty? ? preds : [*preds, merged]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# @return [Compiler]
|
170
|
+
# @api private
|
171
|
+
attr_reader :compiler
|
172
|
+
|
173
|
+
# @api private
|
174
|
+
def initialize(registry = PredicateRegistry.new)
|
175
|
+
@compiler = Compiler.new(registry)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Infer predicate identifier from the provided type
|
179
|
+
#
|
180
|
+
# @param [Type] type
|
181
|
+
# @return [Symbol]
|
182
|
+
#
|
183
|
+
# @api private
|
184
|
+
def [](type)
|
185
|
+
self.class.fetch_or_store(type) do
|
186
|
+
predicates = compiler.visit(type.to_ast)
|
187
|
+
|
188
|
+
if predicates.is_a?(::Hash)
|
189
|
+
predicates
|
190
|
+
else
|
191
|
+
REDUCED_TYPES[predicates] || predicates
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|