dry-types 0.12.3 → 0.13.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.
@@ -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