dry-types 0.12.3 → 0.14.1

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.
@@ -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