dry-schema 1.6.2 → 1.9.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 (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
  )