dry-schema 1.2.0 → 1.4.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.
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)