dry-types 0.12.3 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
27
+ # @option options [String] :key_transform_fn
19
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 => 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
@@ -0,0 +1,7 @@
1
+ require 'dry/inflector'
2
+
3
+ module Dry
4
+ module Types
5
+ Inflector = Dry::Inflector.new
6
+ end
7
+ end
@@ -0,0 +1,93 @@
1
+ module Dry
2
+ module Types
3
+ class Map < Definition
4
+ def initialize(_primitive, key_type: Types['any'], value_type: Types['any'], meta: EMPTY_HASH)
5
+ super(_primitive, key_type: key_type, value_type: value_type, meta: meta)
6
+ validate_options!
7
+ end
8
+
9
+ # @return [Type]
10
+ def key_type
11
+ options[:key_type]
12
+ end
13
+
14
+ # @return [Type]
15
+ def value_type
16
+ options[:value_type]
17
+ end
18
+
19
+ # @return [String]
20
+ def name
21
+ "Map"
22
+ end
23
+
24
+ # @param [Hash] hash
25
+ # @return [Hash]
26
+ def call(hash)
27
+ try(hash) do |failure|
28
+ raise MapError, failure.error.join("\n")
29
+ end.input
30
+ end
31
+ alias_method :[], :call
32
+
33
+ # @param [Hash] hash
34
+ # @return [Boolean]
35
+ def valid?(hash)
36
+ coerce(hash).success?
37
+ end
38
+ alias_method :===, :valid?
39
+
40
+ # @param [Hash] hash
41
+ # @return [Result]
42
+ def try(hash)
43
+ result = coerce(hash)
44
+ return result if result.success? || !block_given?
45
+ yield(result)
46
+ end
47
+
48
+ # @param meta [Boolean] Whether to dump the meta to the AST
49
+ # @return [Array] An AST representation
50
+ def to_ast(meta: true)
51
+ [:map,
52
+ [key_type.to_ast(meta: true), value_type.to_ast(meta: true),
53
+ meta ? self.meta : EMPTY_HASH]]
54
+ end
55
+
56
+ private
57
+
58
+ def coerce(input)
59
+ return failure(
60
+ input, "#{input.inspect} must be an instance of #{primitive}"
61
+ ) unless primitive?(input)
62
+
63
+ output, failures = {}, []
64
+
65
+ input.each do |k,v|
66
+ res_k = options[:key_type].try(k)
67
+ res_v = options[:value_type].try(v)
68
+ if res_k.failure?
69
+ failures << "input key #{k.inspect} is invalid: #{res_k.error}"
70
+ elsif output.key?(res_k.input)
71
+ failures << "duplicate coerced hash key #{res_k.input.inspect}"
72
+ elsif res_v.failure?
73
+ failures << "input value #{v.inspect} for key #{k.inspect} is invalid: #{res_v.error}"
74
+ else
75
+ output[res_k.input] = res_v.input
76
+ end
77
+ end
78
+
79
+ return success(output) if failures.empty?
80
+
81
+ failure(input, failures)
82
+ end
83
+
84
+ def validate_options!
85
+ %i(key_type value_type).each do |opt|
86
+ type = send(opt)
87
+ next if type.is_a?(Type)
88
+ raise MapError, ":#{opt} must be a #{Type}, got: #{type.inspect}"
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end