dry-types 0.14.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +15 -0
  3. data/.rubocop.yml +43 -0
  4. data/.travis.yml +12 -11
  5. data/CHANGELOG.md +115 -4
  6. data/Gemfile +2 -3
  7. data/benchmarks/hash_schemas.rb +5 -5
  8. data/dry-types.gemspec +1 -1
  9. data/lib/dry/types.rb +67 -45
  10. data/lib/dry/types/any.rb +11 -2
  11. data/lib/dry/types/array.rb +1 -4
  12. data/lib/dry/types/array/member.rb +2 -2
  13. data/lib/dry/types/builder.rb +23 -3
  14. data/lib/dry/types/builder_methods.rb +10 -11
  15. data/lib/dry/types/coercions/params.rb +2 -0
  16. data/lib/dry/types/compat.rb +0 -2
  17. data/lib/dry/types/compiler.rb +25 -33
  18. data/lib/dry/types/constrained.rb +5 -4
  19. data/lib/dry/types/constructor.rb +32 -11
  20. data/lib/dry/types/container.rb +2 -0
  21. data/lib/dry/types/core.rb +22 -12
  22. data/lib/dry/types/default.rb +4 -4
  23. data/lib/dry/types/enum.rb +10 -3
  24. data/lib/dry/types/errors.rb +1 -1
  25. data/lib/dry/types/extensions/maybe.rb +11 -1
  26. data/lib/dry/types/hash.rb +70 -63
  27. data/lib/dry/types/hash/constructor.rb +20 -0
  28. data/lib/dry/types/json.rb +7 -7
  29. data/lib/dry/types/map.rb +6 -1
  30. data/lib/dry/types/module.rb +115 -0
  31. data/lib/dry/types/{definition.rb → nominal.rb} +10 -4
  32. data/lib/dry/types/options.rb +2 -2
  33. data/lib/dry/types/params.rb +11 -11
  34. data/lib/dry/types/printable.rb +12 -0
  35. data/lib/dry/types/printer.rb +309 -0
  36. data/lib/dry/types/result.rb +2 -2
  37. data/lib/dry/types/safe.rb +4 -2
  38. data/lib/dry/types/schema.rb +298 -0
  39. data/lib/dry/types/schema/key.rb +130 -0
  40. data/lib/dry/types/spec/types.rb +12 -4
  41. data/lib/dry/types/sum.rb +14 -15
  42. data/lib/dry/types/version.rb +1 -1
  43. metadata +18 -8
  44. data/lib/dry/types/compat/form_types.rb +0 -27
  45. data/lib/dry/types/compat/int.rb +0 -14
  46. data/lib/dry/types/hash/schema.rb +0 -199
  47. data/lib/dry/types/hash/schema_builder.rb +0 -75
@@ -4,9 +4,11 @@ module Dry
4
4
  module Types
5
5
  class Safe
6
6
  include Type
7
- include Dry::Equalizer(:type)
8
7
  include Decorator
9
8
  include Builder
9
+ include Printable
10
+ include Dry::Equalizer(:type, inspect: false)
11
+
10
12
  private :options, :meta
11
13
 
12
14
  # @param [Object] input
@@ -36,7 +38,7 @@ module Dry
36
38
 
37
39
  # @api public
38
40
  #
39
- # @see Definition#to_ast
41
+ # @see Nominal#to_ast
40
42
  def to_ast(meta: true)
41
43
  [:safe, [type.to_ast(meta: meta), EMPTY_HASH]]
42
44
  end
@@ -0,0 +1,298 @@
1
+ require 'dry/types/fn_container'
2
+
3
+ module Dry
4
+ module Types
5
+ # The built-in Hash type can be defined in terms of keys and associated types
6
+ # its values can contain. Such definitions are named {Schema}s and defined
7
+ # as lists of {Key} types.
8
+ #
9
+ # @see Dry::Types::Schema::Key
10
+ #
11
+ # {Schema} evaluates default values for keys missing in input hash
12
+ #
13
+ # @see Dry::Types::Default#evaluate
14
+ # @see Dry::Types::Default::Callable#evaluate
15
+ #
16
+ # {Schema} implements Enumerable using its keys as collection.
17
+ class Schema < Hash
18
+ NO_TRANSFORM = Dry::Types::FnContainer.register { |x| x }
19
+ SYMBOLIZE_KEY = Dry::Types::FnContainer.register(:to_sym.to_proc)
20
+
21
+ include ::Enumerable
22
+
23
+ # @return [Array[Dry::Types::Schema::Key]]
24
+ attr_reader :keys
25
+
26
+ # @return [Hash[Symbol, Dry::Types::Schema::Key]]
27
+ attr_reader :name_key_map
28
+
29
+ # @return [#call]
30
+ attr_reader :transform_key
31
+
32
+ # @param [Class] _primitive
33
+ # @param [Hash] options
34
+ # @option options [Array[Dry::Types::Schema::Key]] :keys
35
+ # @option options [String] :key_transform_fn
36
+ def initialize(_primitive, **options)
37
+ @keys = options.fetch(:keys)
38
+ @name_key_map = keys.each_with_object({}) do |key, idx|
39
+ idx[key.name] = key
40
+ end
41
+
42
+ key_fn = options.fetch(:key_transform_fn, NO_TRANSFORM)
43
+
44
+ @transform_key = Dry::Types::FnContainer[key_fn]
45
+
46
+ super
47
+ end
48
+
49
+ # @param [Hash] hash
50
+ # @return [Hash{Symbol => Object}]
51
+ def call(hash)
52
+ coerce(hash)
53
+ end
54
+ alias_method :[], :call
55
+
56
+ # @param [Hash] hash
57
+ # @option options [Boolean] :skip_missing If true don't raise error if on missing keys
58
+ # @option options [Boolean] :resolve_defaults If false default value
59
+ # won't be evaluated for missing key
60
+ # @return [Hash{Symbol => Object}]
61
+ def apply(hash, options = EMPTY_HASH)
62
+ coerce(hash, options)
63
+ end
64
+
65
+ # @param [Hash] hash
66
+ # @yieldparam [Failure] failure
67
+ # @yieldreturn [Result]
68
+ # @return [Logic::Result]
69
+ # @return [Object] if coercion fails and a block is given
70
+ def try(hash)
71
+ if hash.is_a?(::Hash)
72
+ success = true
73
+ output = {}
74
+
75
+ begin
76
+ result = try_coerce(hash) do |key, key_result|
77
+ success &&= key_result.success?
78
+ output[key.name] = key_result.input
79
+
80
+ key_result
81
+ end
82
+ rescue ConstraintError, UnknownKeysError, SchemaError, MissingKeyError => e
83
+ success = false
84
+ result = e
85
+ end
86
+ else
87
+ success = false
88
+ output = hash
89
+ result = "#{hash} must be a hash"
90
+ end
91
+
92
+ if success
93
+ success(output)
94
+ else
95
+ failure = failure(output, result)
96
+ block_given? ? yield(failure) : failure
97
+ end
98
+ end
99
+
100
+ # @param meta [Boolean] Whether to dump the meta to the AST
101
+ # @return [Array] An AST representation
102
+ def to_ast(meta: true)
103
+ if RUBY_VERSION >= "2.5"
104
+ opts = options.slice(:key_transform_fn, :type_transform_fn, :strict)
105
+ else
106
+ opts = options.select { |k, _|
107
+ k == :key_transform_fn || k == :type_transform_fn || k == :strict
108
+ }
109
+ end
110
+
111
+ [
112
+ :schema,
113
+ [
114
+ keys.map { |key| key.to_ast(meta: meta) },
115
+ opts,
116
+ meta ? self.meta : EMPTY_HASH
117
+ ]
118
+ ]
119
+ end
120
+
121
+ # @param [Hash] hash
122
+ # @return [Boolean]
123
+ def valid?(hash)
124
+ result = try(hash)
125
+ result.success?
126
+ end
127
+ alias_method :===, :valid?
128
+
129
+ # Whether the schema rejects unknown keys
130
+ # @return [Boolean]
131
+ def strict?
132
+ options.fetch(:strict, false)
133
+ end
134
+
135
+ # Make the schema intolerant to unknown keys
136
+ # @return [Schema]
137
+ def strict
138
+ with(strict: true)
139
+ end
140
+
141
+ # Injects a key transformation function
142
+ # @param [#call,nil] proc
143
+ # @param [#call,nil] block
144
+ # @return [Schema]
145
+ def with_key_transform(proc = nil, &block)
146
+ fn = proc || block
147
+
148
+ if fn.nil?
149
+ raise ArgumentError, "a block or callable argument is required"
150
+ end
151
+
152
+ handle = Dry::Types::FnContainer.register(fn)
153
+ with(key_transform_fn: handle)
154
+ end
155
+
156
+ # Whether the schema transforms input keys
157
+ # @return [Boolean]
158
+ # @api public
159
+ def trasform_keys?
160
+ !options[:key_transform_fn].nil?
161
+ end
162
+
163
+ # @overload schema(type_map, meta = EMPTY_HASH)
164
+ # @param [{Symbol => Dry::Types::Nominal}] type_map
165
+ # @param [Hash] meta
166
+ # @return [Dry::Types::Schema]
167
+ # @overload schema(keys)
168
+ # @param [Array<Dry::Types::Schema::Key>] key List of schema keys
169
+ # @param [Hash] meta
170
+ # @return [Dry::Types::Schema]
171
+ def schema(keys_or_map)
172
+ if keys_or_map.is_a?(::Array)
173
+ new_keys = keys_or_map
174
+ else
175
+ new_keys = build_keys(keys_or_map)
176
+ end
177
+
178
+ keys = merge_keys(self.keys, new_keys)
179
+ Schema.new(primitive, **options, keys: keys, meta: meta)
180
+ end
181
+
182
+ # Iterate over each key type
183
+ #
184
+ # @return [Array<Dry::Types::Schema::Key>,Enumerator]
185
+ def each(&block)
186
+ keys.each(&block)
187
+ end
188
+
189
+ # Whether the schema has the given key
190
+ #
191
+ # @param [Symbol] name Key name
192
+ # @return [Boolean]
193
+ def key?(name)
194
+ name_key_map.key?(name)
195
+ end
196
+
197
+ # Fetch key type by a key name.
198
+ # Behaves as ::Hash#fetch
199
+ #
200
+ # @overload key(name, fallback = Undefined)
201
+ # @param [Symbol] name Key name
202
+ # @param [Object] fallback Optional fallback, returned if key is missing
203
+ # @return [Dry::Types::Schema::Key,Object] key type or fallback if key is not in schema
204
+ #
205
+ # @overload key(name, &block)
206
+ # @param [Symbol] name Key name
207
+ # @param [Proc] block Fallback block, runs if key is missing
208
+ # @return [Dry::Types::Schema::Key,Object] key type or block value if key is not in schema
209
+ def key(name, fallback = Undefined, &block)
210
+ if Undefined.equal?(fallback)
211
+ name_key_map.fetch(name, &block)
212
+ else
213
+ name_key_map.fetch(name, fallback)
214
+ end
215
+ end
216
+
217
+ # @return [Boolean]
218
+ def constrained?
219
+ true
220
+ end
221
+
222
+ private
223
+
224
+ # @param [Array<Dry::Types::Schema::Keys>] keys
225
+ # @return [Dry::Types::Schema]
226
+ # @api private
227
+ def merge_keys(*keys)
228
+ keys.
229
+ flatten(1).
230
+ each_with_object({}) { |key, merged| merged[key.name] = key }.
231
+ values
232
+ end
233
+
234
+ def resolve(hash, options = EMPTY_HASH, &block)
235
+ result = {}
236
+
237
+ hash.each do |key, value|
238
+ k = transform_key.(key)
239
+
240
+ if name_key_map.key?(k)
241
+ result[k] = yield(name_key_map[k], value)
242
+ elsif strict?
243
+ raise UnknownKeysError.new(*unexpected_keys(hash.keys))
244
+ end
245
+ end
246
+
247
+ if result.size < keys.size
248
+ resolve_missing_keys(result, options, &block)
249
+ end
250
+
251
+ result
252
+ end
253
+
254
+ def resolve_missing_keys(result, options)
255
+ skip_missing = options.fetch(:skip_missing, false)
256
+ resolve_defaults = options.fetch(:resolve_defaults, true)
257
+
258
+ keys.each do |key|
259
+ next if result.key?(key.name)
260
+
261
+ if key.default? && resolve_defaults
262
+ result[key.name] = yield(key, Undefined)
263
+ elsif key.required? && !skip_missing
264
+ raise MissingKeyError, key.name
265
+ end
266
+ end
267
+ end
268
+
269
+ # @param keys [Array<Symbol>]
270
+ # @return [Array<Symbol>]
271
+ def unexpected_keys(keys)
272
+ keys.map(&transform_key) - name_key_map.keys
273
+ end
274
+
275
+ # @param [Hash] hash
276
+ # @return [Hash{Symbol => Object}]
277
+ def try_coerce(hash)
278
+ resolve(hash) do |key, value|
279
+ yield(key, key.try(value))
280
+ end
281
+ end
282
+
283
+ # @param [Hash] hash
284
+ # @return [Hash{Symbol => Object}]
285
+ def coerce(hash, options = EMPTY_HASH)
286
+ resolve(hash, options) do |key, value|
287
+ begin
288
+ key.(value)
289
+ rescue ConstraintError => e
290
+ raise SchemaError.new(key.name, value, e.result)
291
+ rescue TypeError, ArgumentError => e
292
+ raise SchemaError.new(key.name, value, e.message)
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,130 @@
1
+ require 'dry/equalizer'
2
+
3
+ module Dry
4
+ module Types
5
+ class Schema < Hash
6
+ # Proxy type for schema keys. Contains only key name and
7
+ # whether it's required or not. All other calls deletaged
8
+ # to the wrapped type.
9
+ #
10
+ # @see Dry::Types::Schema
11
+ class Key
12
+ include Type
13
+ include Dry::Equalizer(:name, :type, :options, inspect: false)
14
+ include Decorator
15
+ include Builder
16
+ include Printable
17
+
18
+ # @return [Symbol]
19
+ attr_reader :name
20
+
21
+ # @api private
22
+ def initialize(type, name, required: Undefined, **options)
23
+ required = Undefined.default(required) do
24
+ type.meta.fetch(:required) { !type.meta.fetch(:omittable, false) }
25
+ end
26
+
27
+ super(type, name, required: required, **options)
28
+ @name = name
29
+ end
30
+
31
+ # @see Dry::Types::Nominal#call
32
+ def call(input, &block)
33
+ type.(input, &block)
34
+ end
35
+
36
+ # @see Dry::Types::Nominal#try
37
+ def try(input, &block)
38
+ type.try(input, &block)
39
+ end
40
+
41
+ # Whether the key is required in schema input
42
+ #
43
+ # @return [Boolean]
44
+ def required?
45
+ options.fetch(:required)
46
+ end
47
+
48
+ # Control whether the key is required
49
+ #
50
+ # @overload required
51
+ # @return [Boolean]
52
+ #
53
+ # @overload required(required)
54
+ # Change key's "requireness"
55
+ #
56
+ # @param [Boolean] required New value
57
+ # @return [Dry::Types::Schema::Key]
58
+ def required(required = Undefined)
59
+ if Undefined.equal?(required)
60
+ options.fetch(:required)
61
+ else
62
+ with(required: required)
63
+ end
64
+ end
65
+
66
+ # Make key not required
67
+ #
68
+ # @return [Dry::Types::Schema::Key]
69
+ def omittable
70
+ required(false)
71
+ end
72
+
73
+ # Construct a default type. Default values are
74
+ # evaluated/applied when key is absent in schema
75
+ # input.
76
+ #
77
+ # @see Dry::Types::Default
78
+ # @return [Dry::Types::Schema::Key]
79
+ def default(input = Undefined, &block)
80
+ new(type.default(input, &block))
81
+ end
82
+
83
+ # Replace the underlying type
84
+ # @param [Dry::Types::Type] type
85
+ # @return [Dry::Types::Schema::Key]
86
+ def new(type)
87
+ self.class.new(type, name, options)
88
+ end
89
+
90
+ # @see Dry::Types::Safe
91
+ # @return [Dry::Types::Schema::Key]
92
+ def safe
93
+ new(type.safe)
94
+ end
95
+
96
+ # Dump to internal AST representation
97
+ #
98
+ # @return [Array]
99
+ def to_ast(meta: true)
100
+ [
101
+ :key,
102
+ [
103
+ name,
104
+ required,
105
+ type.to_ast(meta: meta)
106
+ ]
107
+ ]
108
+ end
109
+
110
+ # Get/set type metadata. The Key type doesn't have
111
+ # its out meta, it delegates these calls to the underlying
112
+ # type.
113
+ #
114
+ # @overload meta
115
+ # @return [Hash] metadata associated with type
116
+ #
117
+ # @overload meta(data)
118
+ # @param [Hash] new metadata to merge into existing metadata
119
+ # @return [Type] new type with added metadata
120
+ def meta(data = nil)
121
+ if data.nil?
122
+ type.meta
123
+ else
124
+ new(type.meta(data))
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end