dry-types 0.12.3 → 0.14.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,19 +12,32 @@ module Dry
12
12
 
13
13
  # @param [Dry::Monads::Maybe, Object] input
14
14
  # @return [Dry::Monads::Maybe]
15
- def call(input)
16
- input.is_a?(Dry::Monads::Maybe) ? input : Maybe(type[input])
15
+ def call(input = Undefined)
16
+ case input
17
+ when Dry::Monads::Maybe
18
+ input
19
+ when Undefined
20
+ None()
21
+ else
22
+ Maybe(type[input])
23
+ end
17
24
  end
18
25
  alias_method :[], :call
19
26
 
20
27
  # @param [Object] input
21
28
  # @return [Result::Success]
22
- def try(input)
23
- Result::Success.new(Maybe(type[input]))
29
+ def try(input = Undefined)
30
+ res = if input.equal?(Undefined)
31
+ None()
32
+ else
33
+ Maybe(type[input])
34
+ end
35
+
36
+ Result::Success.new(res)
24
37
  end
25
38
 
26
39
  # @return [true]
27
- def maybe?
40
+ def default?
28
41
  true
29
42
  end
30
43
 
@@ -47,29 +60,6 @@ module Dry
47
60
  end
48
61
  end
49
62
 
50
- class Hash
51
- module MaybeTypes
52
- # @param [Hash] result
53
- # @param [Symbol] key
54
- # @param [Definition] type
55
- def resolve_missing_value(result, key, type)
56
- if type.respond_to?(:maybe?) && type.maybe?
57
- result[key] = type[nil]
58
- else
59
- super
60
- end
61
- end
62
- end
63
-
64
- class StrictWithDefaults < Strict
65
- include MaybeTypes
66
- end
67
-
68
- class Schema < Hash
69
- include MaybeTypes
70
- end
71
- end
72
-
73
63
  # Register non-coercible maybe types
74
64
  NON_NIL.each_key do |name|
75
65
  register("maybe.strict.#{name}", self["strict.#{name}"].maybe)
@@ -9,18 +9,19 @@ module Dry
9
9
  end
10
10
 
11
11
  # @api private
12
- def self.register(function)
13
- register_function_name = register_name(function)
14
- container.register(register_function_name, function) unless container.key?(register_function_name)
15
- register_function_name
12
+ def self.register(function = Dry::Core::Constants::Undefined, &block)
13
+ fn = Dry::Core::Constants::Undefined.default(function, block)
14
+ fn_name = register_name(fn)
15
+ container.register(fn_name, fn) unless container.key?(fn_name)
16
+ fn_name
16
17
  end
17
18
 
18
19
  # @api private
19
- def self.[](function_name)
20
- if container.key?(function_name)
21
- container[function_name]
20
+ def self.[](fn_name)
21
+ if container.key?(fn_name)
22
+ container[fn_name]
22
23
  else
23
- function_name
24
+ fn_name
24
25
  end
25
26
  end
26
27
 
@@ -1,3 +1,5 @@
1
+ require 'dry/types/fn_container'
2
+
1
3
  module Dry
2
4
  module Types
3
5
  class Hash < Definition
@@ -10,14 +12,27 @@ module Dry
10
12
  # @see Dry::Types::Default#evaluate
11
13
  # @see Dry::Types::Default::Callable#evaluate
12
14
  class Schema < Hash
15
+ NO_TRANSFORM = Dry::Types::FnContainer.register { |x| x }
16
+ SYMBOLIZE_KEY = Dry::Types::FnContainer.register(:to_sym.to_proc)
17
+
13
18
  # @return [Hash{Symbol => Definition}]
14
19
  attr_reader :member_types
15
20
 
21
+ # @return [#call]
22
+ attr_reader :transform_key
23
+
16
24
  # @param [Class] _primitive
17
25
  # @param [Hash] options
18
26
  # @option options [Hash{Symbol => Definition}] :member_types
19
- def initialize(_primitive, options)
27
+ # @option options [String] :key_transform_fn
28
+ def initialize(_primitive, **options)
20
29
  @member_types = options.fetch(:member_types)
30
+
31
+ meta = options[:meta] || EMPTY_HASH
32
+ key_fn = meta.fetch(:key_transform_fn, NO_TRANSFORM)
33
+
34
+ @transform_key = Dry::Types::FnContainer[key_fn]
35
+
21
36
  super
22
37
  end
23
38
 
@@ -29,40 +44,46 @@ module Dry
29
44
  alias_method :[], :call
30
45
 
31
46
  # @param [Hash] hash
32
- # @param [#call,nil] block
33
47
  # @yieldparam [Failure] failure
34
48
  # @yieldreturn [Result]
35
49
  # @return [Logic::Result]
36
50
  # @return [Object] if coercion fails and a block is given
37
- def try(hash, &block)
38
- success = true
39
- output = {}
51
+ def try(hash)
52
+ if hash.is_a?(::Hash)
53
+ success = true
54
+ output = {}
40
55
 
41
- begin
42
- result = try_coerce(hash) do |key, member_result|
43
- success &&= member_result.success?
44
- output[key] = member_result.input
56
+ begin
57
+ result = try_coerce(hash) do |key, member_result|
58
+ success &&= member_result.success?
59
+ output[key] = member_result.input
45
60
 
46
- member_result
61
+ member_result
62
+ end
63
+ rescue ConstraintError, UnknownKeysError, SchemaError, MissingKeyError => e
64
+ success = false
65
+ result = e
47
66
  end
48
- rescue ConstraintError, UnknownKeysError, SchemaError => e
67
+ else
49
68
  success = false
50
- result = e
69
+ output = hash
70
+ result = "#{hash} must be a hash"
51
71
  end
52
72
 
53
73
  if success
54
74
  success(output)
55
75
  else
56
76
  failure = failure(output, result)
57
- block ? yield(failure) : failure
77
+ block_given? ? yield(failure) : failure
58
78
  end
59
79
  end
60
80
 
81
+ # @param meta [Boolean] Whether to dump the meta to the AST
82
+ # @return [Array] An AST representation
61
83
  def to_ast(meta: true)
62
84
  [
63
- :hash,
85
+ :hash_schema,
64
86
  [
65
- hash_type,
66
87
  member_types.map { |name, member| [:member, [name, member.to_ast(meta: meta)]] },
67
88
  meta ? self.meta : EMPTY_HASH
68
89
  ]
@@ -77,204 +98,102 @@ module Dry
77
98
  end
78
99
  alias_method :===, :valid?
79
100
 
80
- private
81
-
82
- def hash_type
83
- :schema
101
+ # Whether the schema rejects unknown keys
102
+ # @return [Boolean]
103
+ def strict?
104
+ meta.fetch(:strict, false)
84
105
  end
85
106
 
86
- # @param [Hash] hash
87
- # @return [Hash{Symbol => Object}]
88
- def try_coerce(hash)
89
- resolve(hash) do |type, key, value|
90
- yield(key, type.try(value))
91
- end
107
+ # Make the schema intolerant to unknown keys
108
+ # @return [Schema]
109
+ def strict
110
+ meta(strict: true)
92
111
  end
93
112
 
94
- # @param [Hash] hash
95
- # @return [Hash{Symbol => Object}]
96
- def coerce(hash)
97
- resolve(hash) do |type, key, value|
98
- begin
99
- type.call(value)
100
- rescue ConstraintError => e
101
- raise SchemaError.new(key, value, e.result)
102
- end
103
- end
104
- end
105
-
106
- # @param [Hash] hash
107
- # @return [Hash{Symbol => Object}]
108
- def resolve(hash)
109
- result = {}
110
- member_types.each do |key, type|
111
- if hash.key?(key)
112
- result[key] = yield(type, key, hash[key])
113
- else
114
- resolve_missing_value(result, key, type)
115
- end
116
- end
117
- result
118
- end
113
+ # Injects a key transformation function
114
+ # @param [#call,nil] proc
115
+ # @param [#call,nil] block
116
+ # @return [Schema]
117
+ def with_key_transform(proc = nil, &block)
118
+ fn = proc || block
119
119
 
120
- # @param [Hash] result
121
- # @param [Symbol] key
122
- # @param [Type] type
123
- # @return [Object]
124
- # @see Dry::Types::Default#evaluate
125
- # @see Dry::Types::Default::Callable#evaluate
126
- def resolve_missing_value(result, key, type)
127
- if type.default?
128
- result[key] = type.evaluate
129
- else
130
- super
120
+ if fn.nil?
121
+ raise ArgumentError, "a block or callable argument is required"
131
122
  end
132
- end
133
- end
134
-
135
- # Permissive schema raises a {MissingKeyError} if the given key is missing
136
- # in provided hash.
137
- class Permissive < Schema
138
- private
139
123
 
140
- def hash_type
141
- :permissive
124
+ handle = Dry::Types::FnContainer.register(fn)
125
+ meta(key_transform_fn: handle)
142
126
  end
143
127
 
144
- # @param [Symbol] key
145
- # @raise [MissingKeyError] when key is missing in given input
146
- def resolve_missing_value(_, key, _)
147
- raise MissingKeyError, key
128
+ # @param [{Symbol => Definition}] type_map
129
+ # @return [Schema]
130
+ def schema(type_map)
131
+ member_types = self.member_types.merge(transform_types(type_map))
132
+ Schema.new(primitive, **options, member_types: member_types, meta: meta)
148
133
  end
149
- end
150
134
 
151
- # Strict hash will raise errors when keys are missing or value types are incorrect.
152
- # Strict schema raises a {UnknownKeysError} if there are any unexpected
153
- # keys in given hash, and raises a {MissingKeyError} if any key is missing
154
- # in it.
155
- # @example
156
- # hash = Types::Hash.strict(name: Types::String, age: Types::Coercible::Int)
157
- # hash[email: 'jane@doe.org', name: 'Jane', age: 21]
158
- # # => Dry::Types::SchemaKeyError: :email is missing in Hash input
159
- class Strict < Permissive
160
135
  private
161
136
 
162
- def hash_type
163
- :strict
164
- end
165
-
166
- # @param [Hash] hash
167
- # @return [Hash{Symbol => Object}]
168
- # @raise [UnknownKeysError]
169
- # if there any unexpected key in given hash
170
- # @raise [MissingKeyError]
171
- # if a required key is not present
172
- # @raise [SchemaError]
173
- # if a value is the wrong type
174
137
  def resolve(hash)
175
- unexpected = hash.keys - member_types.keys
176
- raise UnknownKeysError.new(*unexpected) unless unexpected.empty?
138
+ result = {}
177
139
 
178
- super do |member_type, key, value|
179
- type = member_type.default? ? member_type.type : member_type
140
+ hash.each do |key, value|
141
+ k = transform_key.(key)
180
142
 
181
- yield(type, key, value)
143
+ if member_types.key?(k)
144
+ result[k] = yield(member_types[k], k, value)
145
+ elsif strict?
146
+ raise UnknownKeysError.new(*unexpected_keys(hash.keys))
147
+ end
182
148
  end
183
- end
184
- end
185
-
186
- # {StrictWithDefaults} checks that there are no extra keys
187
- # (raises {UnknownKeysError} otherwise) and there a no missing keys
188
- # without default values given (raises {MissingKeyError} otherwise).
189
- # @see Default#evaluate
190
- # @see Default::Callable#evaluate
191
- class StrictWithDefaults < Strict
192
- private
193
149
 
194
- def hash_type
195
- :strict_with_defaults
196
- end
197
-
198
- # @param [Hash] result
199
- # @param [Symbol] key
200
- # @param [Type] type
201
- # @return [Object]
202
- # @see Dry::Types::Default#evaluate
203
- # @see Dry::Types::Default::Callable#evaluate
204
- def resolve_missing_value(result, key, type)
205
- if type.default?
206
- result[key] = type.evaluate
207
- else
208
- super
150
+ if result.size < member_types.size
151
+ resolve_missing_keys(result, &Proc.new)
209
152
  end
210
- end
211
- end
212
-
213
- # Weak schema provides safe types for every type given in schema hash
214
- # @see Safe
215
- class Weak < Schema
216
- # @param [Class] primitive
217
- # @param [Hash] options
218
- # @see #initialize
219
- def self.new(primitive, options)
220
- member_types = options.
221
- fetch(:member_types).
222
- each_with_object({}) { |(k, t), res| res[k] = t.safe }
223
153
 
224
- super(primitive, options.merge(member_types: member_types))
154
+ result
225
155
  end
226
156
 
227
- # @param [Object] value
228
- # @param [#call, nil] block
229
- # @yieldparam [Failure] failure
230
- # @yieldreturn [Result]
231
- # @return [Object] if block given
232
- # @return [Result,Logic::Result] otherwise
233
- def try(value, &block)
234
- if value.is_a?(::Hash)
235
- super
236
- else
237
- result = failure(value, "#{value} must be a hash")
238
- block ? yield(result) : result
157
+ def resolve_missing_keys(result)
158
+ member_types.each do |k, type|
159
+ next if result.key?(k)
160
+
161
+ if type.default?
162
+ result[k] = yield(type, k, Undefined)
163
+ elsif !type.meta[:omittable]
164
+ raise MissingKeyError, k
165
+ end
239
166
  end
240
167
  end
241
168
 
242
- private
243
-
244
- def hash_type
245
- :weak
169
+ # @param keys [Array<Symbol>]
170
+ # @return [Array<Symbol>]
171
+ def unexpected_keys(keys)
172
+ keys.map(&transform_key) - member_types.keys
246
173
  end
247
- end
248
-
249
- # {Symbolized} hash will turn string key names into symbols.
250
- class Symbolized < Weak
251
- private
252
174
 
253
- def hash_type
254
- :symbolized
175
+ # @param [Hash] hash
176
+ # @return [Hash{Symbol => Object}]
177
+ def try_coerce(hash)
178
+ resolve(hash) do |type, key, value|
179
+ yield(key, type.try(value))
180
+ end
255
181
  end
256
182
 
257
- def resolve(hash)
258
- result = {}
259
- member_types.each do |key, type|
260
- keyname =
261
- if hash.key?(key)
262
- key
263
- elsif hash.key?(string_key = key.to_s)
264
- string_key
265
- end
266
-
267
- if keyname
268
- result[key] = yield(type, key, hash[keyname])
269
- else
270
- resolve_missing_value(result, key, type)
183
+ # @param [Hash] hash
184
+ # @return [Hash{Symbol => Object}]
185
+ def coerce(hash)
186
+ resolve(hash) do |type, key, value|
187
+ begin
188
+ type.call(value)
189
+ rescue ConstraintError => e
190
+ raise SchemaError.new(key, value, e.result)
191
+ rescue TypeError, ArgumentError => e
192
+ raise SchemaError.new(key, value, e.message)
271
193
  end
272
194
  end
273
- result
274
195
  end
275
196
  end
276
-
277
- private_constant(*constants(false))
278
197
  end
279
198
  end
280
199
  end
@@ -0,0 +1,75 @@
1
+ require 'dry/types/hash/schema'
2
+ require 'dry/types/fn_container'
3
+
4
+ module Dry
5
+ module Types
6
+ class Hash < Definition
7
+ # A bulder for legacy schemas
8
+ # @api private
9
+ class SchemaBuilder
10
+ NIL_TO_UNDEFINED = -> v { v.nil? ? Undefined : v }
11
+ OMITTABLE_KEYS = %i(schema weak symbolized).freeze
12
+ STRICT = %i(strict strict_with_defaults).freeze
13
+
14
+ # @param primitive [Type]
15
+ # @option options [Hash{Symbol => Definition}] :member_types
16
+ # @option options [Symbol] :hash_type
17
+ def call(primitive, **options)
18
+ hash_type = options.fetch(:hash_type)
19
+ member_types = {}
20
+
21
+ options.fetch(:member_types).each do |k, t|
22
+ member_types[k] = build_type(hash_type, t)
23
+ end
24
+
25
+ instantiate(primitive, **options, member_types: member_types)
26
+ end
27
+
28
+ def instantiate(primitive, hash_type: :base, meta: EMPTY_HASH, **options)
29
+ meta = meta.dup
30
+
31
+ meta[:strict] = true if strict?(hash_type)
32
+ meta[:key_transform_fn] = Schema::SYMBOLIZE_KEY if hash_type == :symbolized
33
+
34
+ Schema.new(primitive, **options, meta: meta)
35
+ end
36
+
37
+ private
38
+
39
+ def omittable?(constructor)
40
+ OMITTABLE_KEYS.include?(constructor)
41
+ end
42
+
43
+ def strict?(constructor)
44
+ STRICT.include?(constructor)
45
+ end
46
+
47
+ def build_type(constructor, type)
48
+ type = safe(constructor, type)
49
+ type = default(constructor, type) if type.default?
50
+ type = type.meta(omittable: true) if omittable?(constructor)
51
+ type
52
+ end
53
+
54
+ def safe(constructor, type)
55
+ if constructor == :weak || constructor == :symbolized
56
+ type.safe
57
+ else
58
+ type
59
+ end
60
+ end
61
+
62
+ def default(constructor, type)
63
+ case constructor
64
+ when :strict_with_defaults
65
+ type
66
+ when :strict
67
+ type.type
68
+ else
69
+ type.constructor(NIL_TO_UNDEFINED)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,62 +1,117 @@
1
- require 'dry/types/hash/schema'
1
+ require 'dry/types/hash/schema_builder'
2
2
 
3
3
  module Dry
4
4
  module Types
5
5
  class Hash < Definition
6
+ SCHEMA_BUILDER = SchemaBuilder.new.freeze
7
+
6
8
  # @param [{Symbol => Definition}] type_map
7
- # @param [Class] klass
8
- # {Schema} or one of its subclasses ({Weak}, {Permissive}, {Strict},
9
- # {StrictWithDefaults}, {Symbolized})
9
+ # @param [Symbol] constructor
10
10
  # @return [Schema]
11
- def schema(type_map, klass = Schema)
12
- member_types = type_map.each_with_object({}) { |(name, type), result|
13
- result[name] =
14
- case type
15
- when String, Class then Types[type]
16
- else type
17
- end
18
- }
11
+ def schema(type_map, constructor = nil)
12
+ member_types = transform_types(type_map)
19
13
 
20
- klass.new(primitive, options.merge(member_types: member_types, meta: meta))
14
+ if constructor.nil?
15
+ Schema.new(primitive, member_types: member_types, **options, meta: meta)
16
+ else
17
+ SCHEMA_BUILDER.(
18
+ primitive,
19
+ **options,
20
+ member_types: member_types,
21
+ meta: meta,
22
+ hash_type: constructor
23
+ )
24
+ end
25
+ end
26
+
27
+ # Build a map type
28
+ #
29
+ # @param [Type] key_type
30
+ # @param [Type] value_type
31
+ # @return [Map]
32
+ def map(key_type, value_type)
33
+ Map.new(
34
+ primitive,
35
+ key_type: resolve_type(key_type),
36
+ value_type: resolve_type(value_type),
37
+ meta: meta
38
+ )
21
39
  end
22
40
 
23
41
  # @param [{Symbol => Definition}] type_map
24
- # @return [Weak]
42
+ # @return [Schema]
25
43
  def weak(type_map)
26
- schema(type_map, Weak)
44
+ schema(type_map, :weak)
27
45
  end
28
46
 
29
47
  # @param [{Symbol => Definition}] type_map
30
- # @return [Permissive]
48
+ # @return [Schema]
31
49
  def permissive(type_map)
32
- schema(type_map, Permissive)
50
+ schema(type_map, :permissive)
33
51
  end
34
52
 
35
53
  # @param [{Symbol => Definition}] type_map
36
- # @return [Strict]
54
+ # @return [Schema]
37
55
  def strict(type_map)
38
- schema(type_map, Strict)
56
+ schema(type_map, :strict)
39
57
  end
40
58
 
41
59
  # @param [{Symbol => Definition}] type_map
42
- # @return [StrictWithDefaults]
60
+ # @return [Schema]
43
61
  def strict_with_defaults(type_map)
44
- schema(type_map, StrictWithDefaults)
62
+ schema(type_map, :strict_with_defaults)
45
63
  end
46
64
 
47
65
  # @param [{Symbol => Definition}] type_map
48
- # @return [Symbolized]
66
+ # @return [Schema]
49
67
  def symbolized(type_map)
50
- schema(type_map, Symbolized)
68
+ schema(type_map, :symbolized)
69
+ end
70
+
71
+ # Build a schema from an AST
72
+ # @api private
73
+ # @param [{Symbol => Definition}] member_types
74
+ # @return [Schema]
75
+ def instantiate(member_types)
76
+ SCHEMA_BUILDER.instantiate(primitive, **options, member_types: member_types)
77
+ end
78
+
79
+ # Injects a type transformation function for building schemas
80
+ # @param [#call,nil] proc
81
+ # @param [#call,nil] block
82
+ # @return [Hash]
83
+ def with_type_transform(proc = nil, &block)
84
+ fn = proc || block
85
+
86
+ if fn.nil?
87
+ raise ArgumentError, "a block or callable argument is required"
88
+ end
89
+
90
+ handle = Dry::Types::FnContainer.register(fn)
91
+ meta(type_transform_fn: handle)
51
92
  end
52
93
 
53
94
  private
54
95
 
55
- # @param [Hash] _result
56
- # @param [Symbol] _key
57
- # @param [Type] _type
58
- def resolve_missing_value(_result, _key, _type)
59
- # noop
96
+ # @api private
97
+ def transform_types(type_map)
98
+ type_fn = meta.fetch(:type_transform_fn, Schema::NO_TRANSFORM)
99
+ type_transform = Dry::Types::FnContainer[type_fn]
100
+
101
+ type_map.each_with_object({}) { |(name, type), result|
102
+ result[name] = type_transform.(
103
+ resolve_type(type),
104
+ name
105
+ )
106
+ }
107
+ end
108
+
109
+ # @api private
110
+ def resolve_type(type)
111
+ case type
112
+ when String, Class then Types[type]
113
+ else type
114
+ end
60
115
  end
61
116
  end
62
117
  end
@@ -0,0 +1,7 @@
1
+ require 'dry/inflector'
2
+
3
+ module Dry
4
+ module Types
5
+ Inflector = Dry::Inflector.new
6
+ end
7
+ end