dry-schema 1.6.2 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/README.md +4 -3
  4. data/dry-schema.gemspec +16 -14
  5. data/lib/dry/schema/compiler.rb +1 -1
  6. data/lib/dry/schema/config.rb +9 -9
  7. data/lib/dry/schema/dsl.rb +7 -4
  8. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +9 -4
  9. data/lib/dry/schema/extensions/hints.rb +11 -9
  10. data/lib/dry/schema/extensions/info/schema_compiler.rb +10 -1
  11. data/lib/dry/schema/extensions/json_schema/schema_compiler.rb +232 -0
  12. data/lib/dry/schema/extensions/json_schema.rb +29 -0
  13. data/lib/dry/schema/extensions/struct.rb +1 -1
  14. data/lib/dry/schema/extensions.rb +4 -0
  15. data/lib/dry/schema/key.rb +75 -70
  16. data/lib/dry/schema/key_coercer.rb +2 -2
  17. data/lib/dry/schema/key_validator.rb +46 -20
  18. data/lib/dry/schema/macros/array.rb +4 -0
  19. data/lib/dry/schema/macros/core.rb +1 -1
  20. data/lib/dry/schema/macros/dsl.rb +17 -15
  21. data/lib/dry/schema/macros/hash.rb +1 -1
  22. data/lib/dry/schema/macros/key.rb +2 -2
  23. data/lib/dry/schema/macros/schema.rb +2 -0
  24. data/lib/dry/schema/macros/value.rb +13 -1
  25. data/lib/dry/schema/message/or/multi_path.rb +7 -5
  26. data/lib/dry/schema/message_compiler.rb +13 -10
  27. data/lib/dry/schema/messages/abstract.rb +9 -9
  28. data/lib/dry/schema/messages/i18n.rb +98 -96
  29. data/lib/dry/schema/messages/namespaced.rb +1 -0
  30. data/lib/dry/schema/messages/yaml.rb +165 -151
  31. data/lib/dry/schema/path.rb +10 -60
  32. data/lib/dry/schema/predicate.rb +2 -2
  33. data/lib/dry/schema/predicate_inferrer.rb +2 -0
  34. data/lib/dry/schema/primitive_inferrer.rb +2 -0
  35. data/lib/dry/schema/processor.rb +6 -6
  36. data/lib/dry/schema/processor_steps.rb +7 -3
  37. data/lib/dry/schema/result.rb +38 -31
  38. data/lib/dry/schema/step.rb +14 -33
  39. data/lib/dry/schema/trace.rb +5 -1
  40. data/lib/dry/schema/type_registry.rb +1 -2
  41. data/lib/dry/schema/version.rb +1 -1
  42. metadata +11 -8
@@ -85,94 +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] = value.is_a?(::Array) ? value.map { |el| member.write(el) } : value
155
- }
156
- 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
157
161
 
158
- # @api private
159
- def coercible(&coercer)
160
- new(coercer: coercer, member: member.coercible(&coercer))
161
- end
162
+ # @api private
163
+ def coercible(&coercer)
164
+ new(coercer: coercer, member: member.coercible(&coercer))
165
+ end
162
166
 
163
- # @api private
164
- def stringified
165
- new(name: name.to_s, member: member.stringified)
166
- end
167
+ # @api private
168
+ def stringified
169
+ new(name: name.to_s, member: member.stringified)
170
+ end
167
171
 
168
- # @api private
169
- def to_dot_notation
170
- [:"#{name}[]"].product(member.to_dot_notation).map { |el| el.join(DOT) }
171
- end
172
+ # @api private
173
+ def to_dot_notation
174
+ [:"#{name}[]"].product(member.to_dot_notation).map { |el| el.join(DOT) }
175
+ end
172
176
 
173
- # @api private
174
- def dump
175
- [name, member.dump]
177
+ # @api private
178
+ def dump
179
+ [name, member.dump]
180
+ end
176
181
  end
177
182
  end
178
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
 
@@ -46,22 +36,58 @@ module Dry
46
36
 
47
37
  private
48
38
 
39
+ # @api private
40
+ def validate_path(key_paths, path)
41
+ if path[INDEX_REGEX]
42
+ key = path.gsub(INDEX_REGEX, BRACKETS)
43
+
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
52
+
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
58
+
49
59
  # @api private
50
60
  def key_paths(hash)
51
- hash.flat_map { |key, _|
52
- case (value = hash[key])
53
- when Hash
54
- [key].product(key_paths(hash[key])).map { |keys| keys.join(DOT) }
55
- when Array
56
- hashes_or_arrays = value.select { |e| e.is_a?(Array) || e.is_a?(Hash) }
57
- hashes_or_arrays.flat_map.with_index { |el, idx|
58
- key_paths(el).map { |path| ["#{key}[#{idx}]", *path].join(DOT) }
59
- }
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
60
79
  else
61
80
  key.to_s
62
81
  end
63
82
  }
64
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
65
91
  end
66
92
  end
67
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,9 +11,18 @@ module Dry
11
11
  # @api private
12
12
  class Value < DSL
13
13
  # @api private
14
- def call(*predicates, **opts, &block)
14
+ #
15
+ # rubocop:disable Metrics/AbcSize
16
+ # rubocop:disable Metrics/CyclomaticComplexity
17
+ # rubocop:disable Metrics/PerceivedComplexity
18
+ def call(*args, **opts, &block)
19
+ types, predicates = args.partition { |arg| arg.is_a?(Dry::Types::Type) }
20
+
21
+ constructor = types.select { |type| type.is_a?(Dry::Types::Constructor) }.reduce(:>>)
15
22
  schema = predicates.detect { |predicate| predicate.is_a?(Processor) }
16
23
 
24
+ schema_dsl.set_type(name, constructor) if constructor
25
+
17
26
  type_spec = opts[:type_spec]
18
27
 
19
28
  if schema
@@ -60,6 +69,9 @@ module Dry
60
69
 
61
70
  self
62
71
  end
72
+ # rubocop:enable Metrics/AbcSize
73
+ # rubocop:enable Metrics/CyclomaticComplexity
74
+ # rubocop:enable Metrics/PerceivedComplexity
63
75
 
64
76
  # @api private
65
77
  def array_type?(type)
@@ -17,17 +17,19 @@ module Dry
17
17
  attr_reader :root
18
18
 
19
19
  # @api private
20
- def initialize(*args)
20
+ def initialize(...)
21
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) }
22
+ flat_left = left.flatten
23
+ flat_right = right.flatten
24
+ @root = [*flat_left, *flat_right].map(&:_path).reduce(:&)
25
+ @left = flat_left.map { _1.to_or(root) }
26
+ @right = flat_right.map { _1.to_or(root) }
25
27
  end
26
28
 
27
29
  # @api public
28
30
  def to_h
29
31
  @to_h ||= Path[[*root, :or]].to_h(
30
- [left.map(&:to_h).reduce(:merge), right.map(&:to_h).reduce(:merge)]
32
+ [MessageSet.new(left).to_h, MessageSet.new(right).to_h]
31
33
  )
32
34
  end
33
35
  end
@@ -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
@@ -18,13 +18,13 @@ module Dry
18
18
  include Dry::Configurable
19
19
  include Dry::Equalizer(:config)
20
20
 
21
- setting :default_locale, nil
22
- setting :load_paths, Set[DEFAULT_MESSAGES_PATH]
23
- setting :top_namespace, DEFAULT_MESSAGES_ROOT
24
- setting :root, "errors"
25
- setting :lookup_options, %i[root predicate path val_type arg_type].freeze
21
+ setting :default_locale
22
+ setting :load_paths, default: Set[DEFAULT_MESSAGES_PATH]
23
+ setting :top_namespace, default: DEFAULT_MESSAGES_ROOT
24
+ setting :root, default: "errors"
25
+ setting :lookup_options, default: %i[root predicate path val_type arg_type].freeze
26
26
 
27
- setting :lookup_paths, [
27
+ setting :lookup_paths, default: [
28
28
  "%<root>s.rules.%<path>s.%<predicate>s.arg.%<arg_type>s",
29
29
  "%<root>s.rules.%<path>s.%<predicate>s",
30
30
  "%<root>s.%<predicate>s.%<message_type>s",
@@ -35,13 +35,13 @@ module Dry
35
35
  "%<root>s.%<predicate>s"
36
36
  ].freeze
37
37
 
38
- setting :rule_lookup_paths, ["rules.%<name>s"].freeze
38
+ setting :rule_lookup_paths, default: ["rules.%<name>s"].freeze
39
39
 
40
- setting :arg_types, Hash.new { |*| "default" }.update(
40
+ setting :arg_types, default: Hash.new { |*| "default" }.update(
41
41
  Range => "range"
42
42
  )
43
43
 
44
- setting :val_types, Hash.new { |*| "default" }.update(
44
+ setting :val_types, default: Hash.new { |*| "default" }.update(
45
45
  Range => "range",
46
46
  String => "string"
47
47
  )