dry-schema 1.8.0 → 1.9.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -1
  3. data/README.md +4 -4
  4. data/dry-schema.gemspec +2 -2
  5. data/lib/dry/schema/compiler.rb +1 -1
  6. data/lib/dry/schema/dsl.rb +7 -4
  7. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +9 -4
  8. data/lib/dry/schema/extensions/hints.rb +11 -9
  9. data/lib/dry/schema/extensions/info/schema_compiler.rb +10 -1
  10. data/lib/dry/schema/extensions/json_schema/schema_compiler.rb +244 -0
  11. data/lib/dry/schema/extensions/json_schema.rb +29 -0
  12. data/lib/dry/schema/extensions/struct.rb +1 -1
  13. data/lib/dry/schema/extensions.rb +4 -0
  14. data/lib/dry/schema/key.rb +75 -74
  15. data/lib/dry/schema/key_coercer.rb +2 -2
  16. data/lib/dry/schema/key_validator.rb +44 -23
  17. data/lib/dry/schema/macros/array.rb +4 -0
  18. data/lib/dry/schema/macros/core.rb +1 -1
  19. data/lib/dry/schema/macros/dsl.rb +17 -15
  20. data/lib/dry/schema/macros/hash.rb +1 -1
  21. data/lib/dry/schema/macros/key.rb +2 -2
  22. data/lib/dry/schema/macros/schema.rb +2 -0
  23. data/lib/dry/schema/macros/value.rb +7 -0
  24. data/lib/dry/schema/message/or/multi_path.rb +73 -11
  25. data/lib/dry/schema/message/or.rb +2 -2
  26. data/lib/dry/schema/message_compiler.rb +13 -10
  27. data/lib/dry/schema/messages/i18n.rb +98 -96
  28. data/lib/dry/schema/messages/namespaced.rb +6 -0
  29. data/lib/dry/schema/messages/yaml.rb +165 -158
  30. data/lib/dry/schema/predicate.rb +2 -2
  31. data/lib/dry/schema/predicate_inferrer.rb +2 -0
  32. data/lib/dry/schema/primitive_inferrer.rb +2 -0
  33. data/lib/dry/schema/processor.rb +2 -2
  34. data/lib/dry/schema/result.rb +5 -7
  35. data/lib/dry/schema/trace.rb +5 -1
  36. data/lib/dry/schema/type_registry.rb +1 -2
  37. data/lib/dry/schema/version.rb +1 -1
  38. metadata +6 -4
@@ -85,98 +85,99 @@ module Dry
85
85
  def coerced_name
86
86
  @__coerced_name__ ||= coercer[name]
87
87
  end
88
- end
89
-
90
- # A specialized key type which handles nested hashes
91
- #
92
- # @api private
93
- class Key::Hash < Key
94
- include Dry.Equalizer(:name, :members, :coercer)
95
88
 
89
+ # A specialized key type which handles nested hashes
90
+ #
96
91
  # @api private
97
- attr_reader :members
92
+ class Hash < self
93
+ include Dry.Equalizer(:name, :members, :coercer)
98
94
 
99
- # @api private
100
- def initialize(id, members:, **opts)
101
- super(id, **opts)
102
- @members = members
103
- end
95
+ # @api private
96
+ attr_reader :members
104
97
 
105
- # @api private
106
- def read(source)
107
- super if source.is_a?(::Hash)
108
- end
98
+ # @api private
99
+ def initialize(id, members:, **opts)
100
+ super(id, **opts)
101
+ @members = members
102
+ end
109
103
 
110
- def write(source, target)
111
- read(source) { |value|
112
- target[coerced_name] = value.is_a?(::Hash) ? members.write(value) : value
113
- }
114
- end
104
+ # @api private
105
+ def read(source)
106
+ super if source.is_a?(::Hash)
107
+ end
115
108
 
116
- # @api private
117
- def coercible(&coercer)
118
- new(coercer: coercer, members: members.coercible(&coercer))
119
- end
109
+ def write(source, target)
110
+ read(source) { |value|
111
+ target[coerced_name] = value.is_a?(::Hash) ? members.write(value) : value
112
+ }
113
+ end
120
114
 
121
- # @api private
122
- def stringified
123
- new(name: name.to_s, members: members.stringified)
124
- end
115
+ # @api private
116
+ def coercible(&coercer)
117
+ new(coercer: coercer, members: members.coercible(&coercer))
118
+ end
125
119
 
126
- # @api private
127
- def to_dot_notation
128
- [name].product(members.flat_map(&:to_dot_notation)).map { |e| e.join(DOT) }
129
- end
120
+ # @api private
121
+ def stringified
122
+ new(name: name.to_s, members: members.stringified)
123
+ end
130
124
 
131
- # @api private
132
- def dump
133
- {name => members.map(&:dump)}
125
+ # @api private
126
+ def to_dot_notation
127
+ [name].product(members.flat_map(&:to_dot_notation)).map { |e| e.join(DOT) }
128
+ end
129
+
130
+ # @api private
131
+ def dump
132
+ {name => members.map(&:dump)}
133
+ end
134
134
  end
135
- end
136
135
 
137
- # A specialized key type which handles nested arrays
138
- #
139
- # @api private
140
- class Key::Array < Key
141
- include Dry.Equalizer(:name, :member, :coercer)
136
+ # A specialized key type which handles nested arrays
137
+ #
138
+ # @api private
139
+ class Array < self
140
+ include Dry.Equalizer(:name, :member, :coercer)
142
141
 
143
- attr_reader :member
142
+ attr_reader :member
144
143
 
145
- # @api private
146
- def initialize(id, member:, **opts)
147
- super(id, **opts)
148
- @member = member
149
- end
144
+ # @api private
145
+ def initialize(id, member:, **opts)
146
+ super(id, **opts)
147
+ @member = member
148
+ end
150
149
 
151
- # @api private
152
- def write(source, target)
153
- read(source) { |value|
154
- target[coerced_name] = if value.is_a?(::Array)
155
- value.map { |el| el.is_a?(::Hash) ? member.write(el) : el }
156
- else
157
- value
158
- end
159
- }
160
- end
150
+ # @api private
151
+ def write(source, target)
152
+ read(source) { |value|
153
+ target[coerced_name] =
154
+ if value.is_a?(::Array)
155
+ value.map { |el| el.is_a?(::Hash) ? member.write(el) : el }
156
+ else
157
+ value
158
+ end
159
+ }
160
+ end
161
161
 
162
- # @api private
163
- def coercible(&coercer)
164
- new(coercer: coercer, member: member.coercible(&coercer))
165
- end
162
+ # @api private
163
+ def coercible(&coercer)
164
+ new(coercer: coercer, member: member.coercible(&coercer))
165
+ end
166
166
 
167
- # @api private
168
- def stringified
169
- new(name: name.to_s, member: member.stringified)
170
- end
167
+ # @api private
168
+ def stringified
169
+ new(name: name.to_s, member: member.stringified)
170
+ end
171
171
 
172
- # @api private
173
- def to_dot_notation
174
- [:"#{name}[]"].product(member.to_dot_notation).map { |el| el.join(DOT) }
175
- end
172
+ # @api private
173
+ def to_dot_notation
174
+ [:"#{name}[]"].product(member.to_dot_notation).map { |el| el.join(DOT) }
175
+ end
176
176
 
177
- # @api private
178
- def dump
179
- [name, member.dump]
177
+ # @api private
178
+ def dump
179
+ [name, member.dump]
180
+ end
180
181
  end
181
182
  end
182
183
  end
@@ -17,8 +17,8 @@ module Dry
17
17
  attr_reader :key_map, :coercer
18
18
 
19
19
  # @api private
20
- def self.new(*args, &coercer)
21
- fetch_or_store(*args, coercer) { super(*args, &coercer) }
20
+ def self.new(*args)
21
+ fetch_or_store(*args) { super }
22
22
  end
23
23
 
24
24
  # @api private
@@ -24,17 +24,7 @@ module Dry
24
24
  key_paths = key_map.to_dot_notation
25
25
 
26
26
  input_paths.each do |path|
27
- error_path =
28
- if path[INDEX_REGEX]
29
- key = path.gsub(INDEX_REGEX, BRACKETS)
30
-
31
- if key_paths.none? { |key_path| key_path.include?(key) }
32
- arr = path.gsub(INDEX_REGEX) { |m| ".#{m[1]}" }
33
- arr.split(DOT).map { |s| DIGIT_REGEX.match?(s) ? s.to_i : s.to_sym }
34
- end
35
- elsif key_paths.none? { |key_path| key_path.include?(path) }
36
- path
37
- end
27
+ error_path = validate_path(key_paths, path)
38
28
 
39
29
  next unless error_path
40
30
 
@@ -47,26 +37,57 @@ module Dry
47
37
  private
48
38
 
49
39
  # @api private
50
- def key_paths(hash)
51
- hash.flat_map { |key, _|
52
- case (value = hash[key])
53
- when Hash
54
- next key.to_s if value.empty?
40
+ def validate_path(key_paths, path)
41
+ if path[INDEX_REGEX]
42
+ key = path.gsub(INDEX_REGEX, BRACKETS)
55
43
 
56
- [key].product(key_paths(hash[key])).map { |keys| keys.join(DOT) }
57
- when Array
58
- hashes_or_arrays = value.select { |e| (e.is_a?(Array) || e.is_a?(Hash)) && !e.empty? }
44
+ if key_paths.none? { paths_match?(key, _1) }
45
+ arr = path.gsub(INDEX_REGEX) { ".#{_1[1]}" }
46
+ arr.split(DOT).map { DIGIT_REGEX.match?(_1) ? Integer(_1, 10) : _1.to_sym }
47
+ end
48
+ elsif key_paths.none? { paths_match?(path, _1) }
49
+ path
50
+ end
51
+ end
59
52
 
60
- next key.to_s if hashes_or_arrays.empty?
53
+ # @api private
54
+ def paths_match?(input_path, key_path)
55
+ residue = key_path.sub(input_path, "")
56
+ residue.empty? || residue.start_with?(DOT, BRACKETS)
57
+ end
61
58
 
62
- hashes_or_arrays.flat_map.with_index { |el, idx|
63
- key_paths(el).map { |path| ["#{key}[#{idx}]", *path].join(DOT) }
64
- }
59
+ # @api private
60
+ def key_paths(hash)
61
+ hash.flat_map { |key, value|
62
+ case value
63
+ when ::Hash
64
+ if value.empty?
65
+ [key.to_s]
66
+ else
67
+ [key].product(key_paths(hash[key])).map { _1.join(DOT) }
68
+ end
69
+ when ::Array
70
+ hashes_or_arrays = hashes_or_arrays(value)
71
+
72
+ if hashes_or_arrays.empty?
73
+ [key.to_s]
74
+ else
75
+ hashes_or_arrays.flat_map.with_index { |el, idx|
76
+ key_paths(el).map { ["#{key}[#{idx}]", *_1].join(DOT) }
77
+ }
78
+ end
65
79
  else
66
80
  key.to_s
67
81
  end
68
82
  }
69
83
  end
84
+
85
+ # @api private
86
+ def hashes_or_arrays(xs)
87
+ xs.select { |x|
88
+ (x.is_a?(::Array) || x.is_a?(::Hash)) && !x.empty?
89
+ }
90
+ end
70
91
  end
71
92
  end
72
93
  end
@@ -10,6 +10,8 @@ module Dry
10
10
  # @api private
11
11
  class Array < DSL
12
12
  # @api private
13
+ # rubocop: disable Metrics/PerceivedComplexity
14
+ # rubocop: disable Metrics/AbcSize
13
15
  def value(*args, **opts, &block)
14
16
  type(:array)
15
17
 
@@ -40,6 +42,8 @@ module Dry
40
42
 
41
43
  self
42
44
  end
45
+ # rubocop: enable Metrics/AbcSize
46
+ # rubocop: enable Metrics/PerceivedComplexity
43
47
 
44
48
  # @api private
45
49
  def to_ast(*)
@@ -16,7 +16,7 @@ module Dry
16
16
  extend Dry::Initializer
17
17
 
18
18
  # @api private
19
- option :name, default: proc { nil }, optional: true
19
+ option :name, default: proc {}, optional: true
20
20
 
21
21
  # @api private
22
22
  option :compiler, default: proc { Compiler.new }
@@ -58,9 +58,9 @@ module Dry
58
58
  # @return [Macros::Core]
59
59
  #
60
60
  # @api public
61
- def value(*predicates, &block)
61
+ def value(...)
62
62
  append_macro(Macros::Value) do |macro|
63
- macro.call(*predicates, &block)
63
+ macro.call(...)
64
64
  end
65
65
  end
66
66
  ruby2_keywords :value if respond_to?(:ruby2_keywords, true)
@@ -76,9 +76,9 @@ module Dry
76
76
  # @return [Macros::Core]
77
77
  #
78
78
  # @api public
79
- def filled(*args, &block)
79
+ def filled(...)
80
80
  append_macro(Macros::Filled) do |macro|
81
- macro.call(*args, &block)
81
+ macro.call(...)
82
82
  end
83
83
  end
84
84
  ruby2_keywords :filled if respond_to?(:ruby2_keywords, true)
@@ -97,9 +97,9 @@ module Dry
97
97
  # @return [Macros::Core]
98
98
  #
99
99
  # @api public
100
- def schema(*args, &block)
100
+ def schema(...)
101
101
  append_macro(Macros::Schema) do |macro|
102
- macro.call(*args, &block)
102
+ macro.call(...)
103
103
  end
104
104
  end
105
105
  ruby2_keywords :schema if respond_to?(:ruby2_keywords, true)
@@ -112,9 +112,9 @@ module Dry
112
112
  # end
113
113
  #
114
114
  # @api public
115
- def hash(*args, &block)
115
+ def hash(...)
116
116
  append_macro(Macros::Hash) do |macro|
117
- macro.call(*args, &block)
117
+ macro.call(...)
118
118
  end
119
119
  end
120
120
  ruby2_keywords :hash if respond_to?(:ruby2_keywords, true)
@@ -136,9 +136,9 @@ module Dry
136
136
  # @return [Macros::Core]
137
137
  #
138
138
  # @api public
139
- def each(*args, &block)
139
+ def each(...)
140
140
  append_macro(Macros::Each) do |macro|
141
- macro.value(*args, &block)
141
+ macro.value(...)
142
142
  end
143
143
  end
144
144
  ruby2_keywords :each if respond_to?(:ruby2_keywords, true)
@@ -156,9 +156,9 @@ module Dry
156
156
  # @return [Macros::Core]
157
157
  #
158
158
  # @api public
159
- def array(*args, &block)
159
+ def array(...)
160
160
  append_macro(Macros::Array) do |macro|
161
- macro.value(*args, &block)
161
+ macro.value(...)
162
162
  end
163
163
  end
164
164
  ruby2_keywords :array if respond_to?(:ruby2_keywords, true)
@@ -200,10 +200,11 @@ module Dry
200
200
  end
201
201
 
202
202
  # @api private
203
+ # rubocop: disable Metrics/PerceivedComplexity
203
204
  def extract_type_spec(*args, nullable: false, set_type: true)
204
205
  type_spec = args[0] unless schema_or_predicate?(args[0])
205
206
 
206
- predicates = Array(type_spec ? args[1..-1] : args)
207
+ predicates = Array(type_spec ? args[1..] : args)
207
208
  type_rule = nil
208
209
 
209
210
  if type_spec
@@ -228,6 +229,7 @@ module Dry
228
229
  yield(*predicates, type_spec: type_spec, type_rule: nil)
229
230
  end
230
231
  end
232
+ # rubocop: enable Metrics/PerceivedComplexity
231
233
 
232
234
  # @api private
233
235
  def resolve_type(type_spec, nullable)
@@ -243,8 +245,8 @@ module Dry
243
245
  # @api private
244
246
  def schema_or_predicate?(arg)
245
247
  arg.is_a?(Dry::Schema::Processor) ||
246
- arg.is_a?(Symbol) &&
247
- arg.to_s.end_with?(QUESTION_MARK)
248
+ (arg.is_a?(Symbol) &&
249
+ arg.to_s.end_with?(QUESTION_MARK))
248
250
  end
249
251
  end
250
252
  end
@@ -29,7 +29,7 @@ module Dry
29
29
  else
30
30
  trace << hash?
31
31
 
32
- super(*args, &block)
32
+ super
33
33
  end
34
34
  end
35
35
  end
@@ -26,8 +26,8 @@ module Dry
26
26
  # @return [Macros::Key]
27
27
  #
28
28
  # @api public
29
- def filter(*args, &block)
30
- (filter_schema_dsl[name] || filter_schema_dsl.optional(name)).value(*args, &block)
29
+ def filter(...)
30
+ (filter_schema_dsl[name] || filter_schema_dsl.optional(name)).value(...)
31
31
  self
32
32
  end
33
33
  ruby2_keywords(:filter) if respond_to?(:ruby2_keywords, true)
@@ -45,6 +45,7 @@ module Dry
45
45
  end
46
46
 
47
47
  # @api private
48
+ # rubocop: disable Metrics/AbcSize
48
49
  def define(*args, &block)
49
50
  definition = schema_dsl.new(path: schema_dsl.path, &block)
50
51
  schema = definition.call
@@ -66,6 +67,7 @@ module Dry
66
67
 
67
68
  schema
68
69
  end
70
+ # rubocop: enable Metrics/AbcSize
69
71
 
70
72
  # @api private
71
73
  def parent_type
@@ -11,6 +11,10 @@ module Dry
11
11
  # @api private
12
12
  class Value < DSL
13
13
  # @api private
14
+ #
15
+ # rubocop:disable Metrics/AbcSize
16
+ # rubocop:disable Metrics/CyclomaticComplexity
17
+ # rubocop:disable Metrics/PerceivedComplexity
14
18
  def call(*args, **opts, &block)
15
19
  types, predicates = args.partition { |arg| arg.is_a?(Dry::Types::Type) }
16
20
 
@@ -65,6 +69,9 @@ module Dry
65
69
 
66
70
  self
67
71
  end
72
+ # rubocop:enable Metrics/AbcSize
73
+ # rubocop:enable Metrics/CyclomaticComplexity
74
+ # rubocop:enable Metrics/PerceivedComplexity
68
75
 
69
76
  # @api private
70
77
  def array_type?(type)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/core/equalizer"
4
-
5
3
  require "dry/schema/message/or/abstract"
6
4
  require "dry/schema/path"
7
5
 
@@ -14,21 +12,85 @@ module Dry
14
12
  # @api public
15
13
  class MultiPath < Abstract
16
14
  # @api private
17
- attr_reader :root
15
+ class MessageArray
16
+ # @api private
17
+ def initialize(messages)
18
+ @messages = messages.flatten
19
+ end
20
+
21
+ # @api private
22
+ def _paths
23
+ @messages.map(&:_path)
24
+ end
25
+
26
+ # @api private
27
+ def to_or(root)
28
+ self.class.new(@messages.map { _1.to_or(root) })
29
+ end
30
+
31
+ # @api private
32
+ def to_h
33
+ MessageSet.new(@messages).to_h
34
+ end
35
+ end
36
+
37
+ # @api private
38
+ def self.handler(message)
39
+ handlers.find { |k,| message.is_a?(k) }&.last
40
+ end
18
41
 
19
42
  # @api private
20
- def initialize(*args)
21
- super
22
- @root = [left, right].flatten.map(&:_path).reduce(:&)
23
- @left = left.map { |msg| msg.to_or(root) }
24
- @right = right.map { |msg| msg.to_or(root) }
43
+ private_class_method def self.handlers
44
+ @handlers ||= {
45
+ self => -> { _1 },
46
+ Array => -> { MessageArray.new(_1) }
47
+ }.freeze
25
48
  end
26
49
 
27
50
  # @api public
28
51
  def to_h
29
- @to_h ||= Path[[*root, :or]].to_h(
30
- [left.map(&:to_h).reduce(:merge), right.map(&:to_h).reduce(:merge)]
31
- )
52
+ @to_h ||= Path[[*root, :or]].to_h(messages.map(&:to_h))
53
+ end
54
+
55
+ # @api private
56
+ def messages
57
+ @messages ||= _messages.flat_map { _1.to_or(root) }
58
+ end
59
+
60
+ # @api private
61
+ def root
62
+ @root ||= _messages.flat_map(&:_paths).reduce(:&)
63
+ end
64
+
65
+ # @api private
66
+ def path
67
+ root
68
+ end
69
+
70
+ # @api private
71
+ def _paths
72
+ @paths ||= [Path[root]]
73
+ end
74
+
75
+ # @api private
76
+ def to_or(root)
77
+ self.root == root ? messages : [self]
78
+ end
79
+
80
+ private
81
+
82
+ # @api private
83
+ def _messages
84
+ @_messages ||= [left, right].map do |message|
85
+ handler = self.class.handler(message)
86
+
87
+ unless handler
88
+ raise ArgumentError,
89
+ "#{message.inspect} is of unknown type #{message.class.inspect}"
90
+ end
91
+
92
+ handler.(message)
93
+ end
32
94
  end
33
95
  end
34
96
  end
@@ -17,8 +17,8 @@ module Dry
17
17
 
18
18
  if paths.uniq.size == 1
19
19
  SinglePath.new(left, right, messages)
20
- elsif right.is_a?(Array)
21
- if left.is_a?(Array) && paths.uniq.size > 1
20
+ elsif MultiPath.handler(right)
21
+ if MultiPath.handler(left) && paths.uniq.size > 1
22
22
  MultiPath.new(left, right)
23
23
  else
24
24
  right
@@ -30,14 +30,14 @@ module Dry
30
30
 
31
31
  EMPTY_OPTS = VisitorOpts.new
32
32
  EMPTY_MESSAGE_SET = MessageSet.new(EMPTY_ARRAY).freeze
33
- FULL_MESSAGE_WHITESPACE = Hash.new(' ').merge(
34
- ja: '',
35
- zh: '',
36
- bn: '',
37
- th: '',
38
- lo: '',
39
- my: '',
40
- )
33
+ FULL_MESSAGE_WHITESPACE = Hash.new(" ").merge(
34
+ ja: "",
35
+ zh: "",
36
+ bn: "",
37
+ th: "",
38
+ lo: "",
39
+ my: ""
40
+ )
41
41
 
42
42
  param :messages
43
43
 
@@ -206,7 +206,8 @@ module Dry
206
206
  return text if !text || !full
207
207
 
208
208
  rule = options[:path]
209
- [messages.rule(rule, options) || rule, text].join(FULL_MESSAGE_WHITESPACE[template.options[:locale]])
209
+ [messages.rule(rule, options) || rule,
210
+ text].join(FULL_MESSAGE_WHITESPACE[template.options[:locale]])
210
211
  end
211
212
 
212
213
  # @api private
@@ -228,7 +229,9 @@ module Dry
228
229
  # @api private
229
230
  def append_mapped_size_tokens(tokens)
230
231
  # this is a temporary fix for the inconsistency in the "size" errors arguments
231
- mapped_hash = tokens.each_with_object({}) { |(k, v), h| h[k.to_s.gsub("size", "num").to_sym] = v }
232
+ mapped_hash = tokens.each_with_object({}) { |(k, v), h|
233
+ h[k.to_s.gsub("size", "num").to_sym] = v
234
+ }
232
235
  tokens.merge(mapped_hash)
233
236
  end
234
237
  end