dry-types 0.14.1 → 0.15.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.
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