dry-schema 1.2.0 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  [gem]: https://rubygems.org/gems/dry-schema
2
- [travis]: https://travis-ci.org/dry-rb/dry-schema
2
+ [travis]: https://travis-ci.com/dry-rb/dry-schema
3
+ [actions]: https://github.com/dry-rb/dry-schema/actions
3
4
  [codeclimate]: https://codeclimate.com/github/dry-rb/dry-schema
4
5
  [chat]: https://dry-rb.zulipchat.com
5
6
  [inchpages]: http://inch-ci.org/github/dry-rb/dry-schema
@@ -7,7 +8,8 @@
7
8
  # dry-schema [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
8
9
 
9
10
  [![Gem Version](https://badge.fury.io/rb/dry-schema.svg)][gem]
10
- [![Build Status](https://travis-ci.org/dry-rb/dry-schema.svg?branch=master)][travis]
11
+ [![Travis Status](https://travis-ci.com/dry-rb/dry-schema.svg?branch=master)][travis]
12
+ [![CI Status](https://github.com/dry-rb/dry-schema/workflows/ci/badge.svg)][actions]
11
13
  [![Code Climate](https://codeclimate.com/github/dry-rb/dry-schema/badges/gpa.svg)][codeclimate]
12
14
  [![Test Coverage](https://codeclimate.com/github/dry-rb/dry-schema/badges/coverage.svg)][codeclimate]
13
15
  [![Inline docs](http://inch-ci.org/github/dry-rb/dry-schema.svg?branch=master)][inchpages]
data/config/errors.yml CHANGED
@@ -73,8 +73,12 @@ en:
73
73
 
74
74
  max_size?: "size cannot be greater than %{num}"
75
75
 
76
+ max_bytesize?: "bytesize cannot be greater than %{num}"
77
+
76
78
  min_size?: "size cannot be less than %{num}"
77
79
 
80
+ min_bytesize?: "bytesize cannot be less than %{num}"
81
+
78
82
  nil?: "cannot be defined"
79
83
 
80
84
  str?: "must be a string"
@@ -92,5 +96,10 @@ en:
92
96
  default: "length must be %{size}"
93
97
  range: "length must be within %{size_left} - %{size_right}"
94
98
 
99
+ bytesize?:
100
+ arg:
101
+ default: "must be %{size} bytes long"
102
+ range: "must be within %{size_left} - %{size_right} bytes long"
103
+
95
104
  not:
96
105
  empty?: "cannot be empty"
@@ -26,10 +26,11 @@ module Dry
26
26
  # An error raised when a localized message cannot be found
27
27
  MissingMessageError = Class.new(StandardError) do
28
28
  # @api private
29
- def initialize(path)
29
+ def initialize(path, paths = [])
30
30
  *rest, rule = path
31
31
  super(<<~STR)
32
- Message template for #{rule.inspect} under #{rest.join(DOT).inspect} was not found
32
+ Message template for #{rule.inspect} under #{rest.join(DOT).inspect} was not found. Searched in:
33
+ #{paths.map { |string| "\"#{string}\"" }.join("\n")}
33
34
  STR
34
35
  end
35
36
  end
@@ -9,6 +9,7 @@ require 'dry/schema/types'
9
9
  require 'dry/schema/macros'
10
10
 
11
11
  require 'dry/schema/processor'
12
+ require 'dry/schema/processor_steps'
12
13
  require 'dry/schema/key_map'
13
14
  require 'dry/schema/key_coercer'
14
15
  require 'dry/schema/value_coercer'
@@ -64,18 +65,21 @@ module Dry
64
65
  # @return [Compiler] A key=>type map defined within the DSL
65
66
  option :types, default: -> { EMPTY_HASH.dup }
66
67
 
67
- # @return [DSL] An optional parent DSL object that will be used to merge keys and rules
68
- option :parent, optional: true
68
+ # @return [Array] Optional parent DSL objects, that will be used to merge keys and rules
69
+ option :parent, Types::Coercible::Array, default: -> { EMPTY_ARRAY.dup }, as: :parents
69
70
 
70
71
  # @return [Config] Configuration object exposed via `#configure` method
71
72
  option :config, optional: true, default: proc { parent ? parent.config.dup : Config.new }
72
73
 
74
+ # @return [ProcessorSteps] Steps for the processor
75
+ option :steps, default: proc { parent ? parent.steps.dup : ProcessorSteps.new }
76
+
73
77
  # Build a new DSL object and evaluate provided block
74
78
  #
75
79
  # @param [Hash] options
76
80
  # @option options [Class] :processor The processor type (`Params`, `JSON` or a custom sub-class)
77
81
  # @option options [Compiler] :compiler An instance of a rule compiler (must be compatible with `Schema::Compiler`) (optional)
78
- # @option options [DSL] :parent An instance of the parent DSL (optional)
82
+ # @option options [Array[DSL]] :parent One or more instances of the parent DSL (optional)
79
83
  # @option options [Config] :config A configuration object (optional)
80
84
  #
81
85
  # @see Schema.define
@@ -189,9 +193,10 @@ module Dry
189
193
  #
190
194
  # @api private
191
195
  def call
192
- steps = [key_coercer]
193
- steps << filter_schema.rule_applier if filter_rules?
194
- steps << value_coercer << rule_applier
196
+ steps[:key_coercer] = key_coercer
197
+ steps[:value_coercer] = value_coercer
198
+ steps[:rule_applier] = rule_applier
199
+ steps[:filter_schema] = filter_schema.rule_applier if filter_rules?
195
200
 
196
201
  processor_type.new(schema_dsl: self, steps: steps)
197
202
  end
@@ -215,14 +220,54 @@ module Dry
215
220
  -> member_type { type_registry['array'].of(resolve_type(member_type)) }
216
221
  end
217
222
 
223
+ # Method allows steps injection to the processor
224
+ #
225
+ # @example
226
+ # before(:rule_applier) do |input|
227
+ # input.compact
228
+ # end
229
+ #
230
+ # @return [DSL]
231
+ #
232
+ # @api public
233
+ def before(key, &block)
234
+ steps.before(key, &block)
235
+ self
236
+ end
237
+
238
+ # Method allows steps injection to the processor
239
+ #
240
+ # @example
241
+ # after(:rule_applier) do |input|
242
+ # input.compact
243
+ # end
244
+ #
245
+ # @return [DSL]
246
+ #
247
+ # @api public
248
+ def after(key, &block)
249
+ steps.after(key, &block)
250
+ self
251
+ end
252
+
253
+ # The parent (last from parents) which is used for copying non mergeable configuration
254
+ #
255
+ # @return DSL
256
+ #
257
+ # @api public
258
+ def parent
259
+ @parent ||= parents.last
260
+ end
261
+
218
262
  # Return type schema used by the value coercer
219
263
  #
220
264
  # @return [Dry::Types::Safe]
221
265
  #
222
266
  # @api private
223
267
  def type_schema
224
- schema = type_registry['hash'].schema(types).lax
225
- parent ? parent.type_schema.schema(schema.to_a) : schema
268
+ our_schema = type_registry['hash'].schema(types).lax
269
+ schemas = [*parents.map(&:type_schema), our_schema]
270
+ schemas.inject { |result, schema| result.schema(schema.to_a) }
226
271
  end
227
272
 
228
273
  # Return a new DSL instance using the same processor type
@@ -231,7 +276,7 @@ module Dry
231
276
  #
232
277
  # @api private
233
278
  def new(options = EMPTY_HASH, &block)
234
- self.class.new(options.merge(processor_type: processor_type), &block)
279
+ self.class.new(options.merge(processor_type: processor_type, config: config), &block)
235
280
  end
236
281
 
237
282
  # Set a type for the given key name
@@ -276,14 +321,18 @@ module Dry
276
321
  #
277
322
  # @api private
278
323
  def filter_schema_dsl
279
- @filter_schema_dsl ||= new(parent: parent_filter_schema)
324
+ @filter_schema_dsl ||= new(parent: parent_filter_schemas)
280
325
  end
281
326
 
282
327
  # Check if any filter rules were defined
283
328
  #
284
329
  # @api private
285
330
  def filter_rules?
286
- (instance_variable_defined?('@filter_schema_dsl') && !filter_schema_dsl.macros.empty?) || parent&.filter_rules?
331
+ if instance_variable_defined?('@filter_schema_dsl') && !filter_schema_dsl.macros.empty?
332
+ return true
333
+ end
334
+
335
+ parents.any?(&:filter_rules?)
287
336
  end
288
337
 
289
338
  protected
@@ -323,10 +372,8 @@ module Dry
323
372
  private
324
373
 
325
374
  # @api private
326
- def parent_filter_schema
327
- return unless parent
328
-
329
- parent.filter_schema if parent.filter_rules?
375
+ def parent_filter_schemas
376
+ parents.select(&:filter_rules?).map(&:filter_schema)
330
377
  end
331
378
 
332
379
  # Build a key coercer
@@ -380,12 +427,12 @@ module Dry
380
427
 
381
428
  # @api private
382
429
  def parent_rules
383
- parent&.rules || EMPTY_HASH
430
+ parents.reduce({}) { |rules, parent| rules.merge(parent.rules) }
384
431
  end
385
432
 
386
433
  # @api private
387
434
  def parent_key_map
388
- parent&.key_map || EMPTY_ARRAY
435
+ parents.reduce([]) { |key_map, parent| parent.key_map + key_map }
389
436
  end
390
437
  end
391
438
  end
@@ -12,7 +12,7 @@ module Dry
12
12
  #
13
13
  # @return [Array<Message::Hint>]
14
14
  attr_reader :hints
15
-
15
+
16
16
  # Configuration option to enable/disable showing errors
17
17
  #
18
18
  # @return [Boolean]
@@ -37,6 +37,53 @@ module Dry
37
37
  @to_h ||= failures ? messages_map : messages_map(hints)
38
38
  end
39
39
  alias_method :to_hash, :to_h
40
+
41
+ private
42
+
43
+ # @api private
44
+ def unique_paths
45
+ messages.uniq(&:path).map(&:path)
46
+ end
47
+
48
+ # @api private
49
+ def messages_map(messages = self.messages)
50
+ return EMPTY_HASH if empty?
51
+
52
+ messages.reduce(placeholders) { |hash, msg|
53
+ node = msg.path.reduce(hash) { |a, e| a.is_a?(Hash) ? a[e] : a.last[e] }
54
+ (node[0].is_a?(::Array) ? node[0] : node) << msg.dump
55
+ hash
56
+ }
57
+ end
58
+
59
+ # @api private
60
+ #
61
+ # rubocop:disable Metrics/AbcSize
62
+ # rubocop:disable Metrics/PerceivedComplexity
63
+ def initialize_placeholders!
64
+ @placeholders = unique_paths.each_with_object(EMPTY_HASH.dup) { |path, hash|
65
+ curr_idx = 0
66
+ last_idx = path.size - 1
67
+ node = hash
68
+
69
+ while curr_idx <= last_idx
70
+ key = path[curr_idx]
71
+
72
+ next_node =
73
+ if node.is_a?(Array) && key.is_a?(Symbol)
74
+ node_hash = (node << [] << {}).last
75
+ node_hash[key] || (node_hash[key] = curr_idx < last_idx ? {} : [])
76
+ else
77
+ node[key] || (node[key] = curr_idx < last_idx ? {} : [])
78
+ end
79
+
80
+ node = next_node
81
+ curr_idx += 1
82
+ end
83
+ }
84
+ end
85
+ # rubocop:enable Metrics/AbcSize
86
+ # rubocop:enable Metrics/PerceivedComplexity
40
87
  end
41
88
  end
42
89
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'dry/logic/operators'
4
+ require 'dry/types/predicate_inferrer'
5
+ require 'dry/types/primitive_inferrer'
4
6
 
5
7
  require 'dry/schema/macros/core'
6
8
 
@@ -26,7 +28,13 @@ module Dry
26
28
  # PredicateInferrer is used to infer predicate type-check from a type spec
27
29
  # @return [PredicateInferrer]
28
30
  # @api private
29
- option :predicate_inferrer, default: proc { PredicateInferrer.new(compiler.predicates) }
31
+ option :predicate_inferrer, default: proc { ::Dry::Types::PredicateInferrer.new(compiler.predicates) }
32
+
33
+ # @!attribute [r] primitive_inferrer
34
+ # PrimitiveInferrer used to get a list of primitive classes from configured type
35
+ # @return [PrimitiveInferrer]
36
+ # @api private
37
+ option :primitive_inferrer, default: proc { ::Dry::Types::PrimitiveInferrer.new }
30
38
 
31
39
  # @overload value(*predicates, **predicate_opts)
32
40
  # Set predicates without and with arguments
@@ -192,27 +200,30 @@ module Dry
192
200
  predicates = Array(type_spec ? args[1..-1] : args)
193
201
 
194
202
  if type_spec
195
- resolved_type = schema_dsl.resolve_type(
196
- nullable && !type_spec.is_a?(::Array) ? [:nil, type_spec] : type_spec
197
- )
203
+ resolved_type = resolve_type(type_spec, nullable)
198
204
 
199
205
  type(resolved_type) if set_type
200
206
 
201
207
  type_predicates = predicate_inferrer[resolved_type]
202
208
 
203
- unless type_predicates.empty? || predicates.include?(type_predicates)
204
- if type_predicates.is_a?(::Array) && type_predicates.size.equal?(1)
205
- predicates.unshift(type_predicates[0])
206
- else
207
- predicates.unshift(type_predicates)
208
- end
209
- end
209
+ predicates.replace(type_predicates + predicates) unless type_predicates.empty?
210
210
 
211
211
  return self if predicates.empty?
212
212
  end
213
213
 
214
214
  yield(*predicates, type_spec: type_spec)
215
215
  end
216
+
217
+ # @api private
218
+ def resolve_type(type_spec, nullable)
219
+ resolved = schema_dsl.resolve_type(type_spec)
220
+
221
+ if type_spec.is_a?(::Array) || !nullable || resolved.optional?
222
+ resolved
223
+ else
224
+ schema_dsl.resolve_type([:nil, resolved])
225
+ end
226
+ end
216
227
  end
217
228
  end
218
229
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/primitive_inferrer'
4
3
  require 'dry/schema/macros/value'
5
4
 
6
5
  module Dry
@@ -10,12 +9,6 @@ module Dry
10
9
  #
11
10
  # @api private
12
11
  class Filled < Value
13
- # @!attribute [r] primitive_inferrer
14
- # PrimitiveInferrer used to get a list of primitive classes from configured type
15
- # @return [PrimitiveInferrer]
16
- # @api private
17
- option :primitive_inferrer, default: proc { PrimitiveInferrer.new }
18
-
19
12
  # @api private
20
13
  def call(*predicates, **opts, &block)
21
14
  ensure_valid_predicates(predicates)
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/predicate_inferrer'
4
3
  require 'dry/schema/processor'
5
4
  require 'dry/schema/macros/dsl'
6
5
  require 'dry/schema/constants'
@@ -11,10 +11,10 @@ module Dry
11
11
  class Schema < Value
12
12
  # @api private
13
13
  def call(*args, &block)
14
- super(*args) unless args.empty?
14
+ super(*args, &nil) unless args.empty?
15
15
 
16
16
  if block
17
- schema = define(&block)
17
+ schema = define(*args, &block)
18
18
  trace << schema.to_rule
19
19
  end
20
20
 
@@ -24,11 +24,17 @@ module Dry
24
24
  private
25
25
 
26
26
  # @api private
27
- def define(&block)
27
+ def define(*args, &block)
28
28
  definition = schema_dsl.new(&block)
29
29
  schema = definition.call
30
-
31
- type_schema = array? ? parent_type.of(definition.type_schema) : definition.type_schema
30
+ type_schema =
31
+ if array_type?(parent_type)
32
+ build_array_type(parent_type, definition.type_schema)
33
+ elsif redefined_schema?(args)
34
+ parent_type.schema(definition.types)
35
+ else
36
+ definition.type_schema
37
+ end
32
38
  final_type = optional? ? type_schema.optional : type_schema
33
39
 
34
40
  type(final_type)
@@ -51,8 +57,13 @@ module Dry
51
57
  end
52
58
 
53
59
  # @api private
54
- def array?
55
- parent_type.respond_to?(:of)
60
+ def schema?
61
+ parent_type.respond_to?(:schema)
62
+ end
63
+
64
+ # @api private
65
+ def redefined_schema?(args)
66
+ schema? && args.first.is_a?(Processor)
56
67
  end
57
68
  end
58
69
  end
@@ -17,8 +17,8 @@ module Dry
17
17
  current_type = schema_dsl.types[name]
18
18
 
19
19
  updated_type =
20
- if current_type.respond_to?(:of)
21
- current_type.of(schema.type_schema)
20
+ if array_type?(current_type)
21
+ build_array_type(current_type, schema.type_schema)
22
22
  else
23
23
  schema.type_schema
24
24
  end
@@ -39,6 +39,25 @@ module Dry
39
39
  self
40
40
  end
41
41
 
42
+ # @api private
43
+ def array_type?(type)
44
+ primitive_inferrer[type].eql?([::Array])
45
+ end
46
+
47
+ # @api private
48
+ def build_array_type(array_type, member)
49
+ if array_type.respond_to?(:of)
50
+ array_type.of(member)
51
+ else
52
+ raise ArgumentError, <<~ERROR.split("\n").join(' ')
53
+ Cannot define schema for a nominal array type.
54
+ Array types must be instances of Dry::Types::Array,
55
+ usually constructed with Types::Constructor(Array) { ... } or
56
+ Dry::Types['array'].constructor { ... }
57
+ ERROR
58
+ end
59
+ end
60
+
42
61
  # @api private
43
62
  def respond_to_missing?(meth, include_private = false)
44
63
  super || meth.to_s.end_with?(QUESTION_MARK)