dry-schema 1.4.2 → 1.5.3

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +217 -78
  3. data/LICENSE +1 -1
  4. data/README.md +4 -4
  5. data/config/errors.yml +4 -0
  6. data/dry-schema.gemspec +46 -0
  7. data/lib/dry-schema.rb +1 -1
  8. data/lib/dry/schema.rb +19 -6
  9. data/lib/dry/schema/compiler.rb +5 -5
  10. data/lib/dry/schema/config.rb +15 -6
  11. data/lib/dry/schema/constants.rb +16 -7
  12. data/lib/dry/schema/dsl.rb +87 -27
  13. data/lib/dry/schema/extensions.rb +10 -2
  14. data/lib/dry/schema/extensions/hints.rb +15 -8
  15. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +1 -1
  16. data/lib/dry/schema/extensions/hints/message_set_methods.rb +0 -47
  17. data/lib/dry/schema/extensions/info.rb +27 -0
  18. data/lib/dry/schema/extensions/info/schema_compiler.rb +105 -0
  19. data/lib/dry/schema/extensions/monads.rb +1 -1
  20. data/lib/dry/schema/extensions/struct.rb +32 -0
  21. data/lib/dry/schema/json.rb +1 -1
  22. data/lib/dry/schema/key.rb +16 -1
  23. data/lib/dry/schema/key_coercer.rb +4 -4
  24. data/lib/dry/schema/key_map.rb +9 -4
  25. data/lib/dry/schema/key_validator.rb +67 -0
  26. data/lib/dry/schema/macros.rb +8 -8
  27. data/lib/dry/schema/macros/array.rb +17 -4
  28. data/lib/dry/schema/macros/core.rb +9 -4
  29. data/lib/dry/schema/macros/dsl.rb +34 -19
  30. data/lib/dry/schema/macros/each.rb +4 -4
  31. data/lib/dry/schema/macros/filled.rb +5 -5
  32. data/lib/dry/schema/macros/hash.rb +21 -3
  33. data/lib/dry/schema/macros/key.rb +9 -9
  34. data/lib/dry/schema/macros/maybe.rb +4 -5
  35. data/lib/dry/schema/macros/optional.rb +1 -1
  36. data/lib/dry/schema/macros/required.rb +1 -1
  37. data/lib/dry/schema/macros/schema.rb +23 -2
  38. data/lib/dry/schema/macros/value.rb +34 -7
  39. data/lib/dry/schema/message.rb +35 -9
  40. data/lib/dry/schema/message/or.rb +18 -39
  41. data/lib/dry/schema/message/or/abstract.rb +28 -0
  42. data/lib/dry/schema/message/or/multi_path.rb +37 -0
  43. data/lib/dry/schema/message/or/single_path.rb +64 -0
  44. data/lib/dry/schema/message_compiler.rb +55 -19
  45. data/lib/dry/schema/message_compiler/visitor_opts.rb +2 -2
  46. data/lib/dry/schema/message_set.rb +26 -37
  47. data/lib/dry/schema/messages.rb +6 -6
  48. data/lib/dry/schema/messages/abstract.rb +54 -56
  49. data/lib/dry/schema/messages/i18n.rb +29 -27
  50. data/lib/dry/schema/messages/namespaced.rb +12 -2
  51. data/lib/dry/schema/messages/template.rb +19 -44
  52. data/lib/dry/schema/messages/yaml.rb +61 -14
  53. data/lib/dry/schema/params.rb +1 -1
  54. data/lib/dry/schema/path.rb +44 -5
  55. data/lib/dry/schema/predicate.rb +4 -2
  56. data/lib/dry/schema/predicate_inferrer.rb +4 -184
  57. data/lib/dry/schema/predicate_registry.rb +2 -2
  58. data/lib/dry/schema/primitive_inferrer.rb +16 -0
  59. data/lib/dry/schema/processor.rb +49 -28
  60. data/lib/dry/schema/processor_steps.rb +50 -27
  61. data/lib/dry/schema/result.rb +52 -5
  62. data/lib/dry/schema/rule_applier.rb +7 -7
  63. data/lib/dry/schema/step.rb +79 -0
  64. data/lib/dry/schema/trace.rb +5 -4
  65. data/lib/dry/schema/type_container.rb +3 -3
  66. data/lib/dry/schema/type_registry.rb +2 -2
  67. data/lib/dry/schema/types.rb +1 -1
  68. data/lib/dry/schema/value_coercer.rb +2 -2
  69. data/lib/dry/schema/version.rb +1 -1
  70. metadata +21 -7
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/initializer'
4
- require 'dry/equalizer'
3
+ require "dry/initializer"
4
+ require "dry/equalizer"
5
5
 
6
- require 'dry/schema/path'
7
- require 'dry/schema/message/or'
6
+ require "dry/schema/path"
7
+ require "dry/schema/message/or"
8
8
 
9
9
  module Dry
10
10
  module Schema
@@ -52,9 +52,22 @@ module Dry
52
52
  #
53
53
  # @api public
54
54
  def dump
55
- @dump ||= meta.empty? ? text : { text: text, **meta }
55
+ @dump ||= meta.empty? ? text : {text: text, **meta}
56
+ end
57
+ alias_method :to_s, :dump
58
+
59
+ # Dump the message into a hash
60
+ #
61
+ # The hash will be deeply nested if the path's size is greater than 1
62
+ #
63
+ # @see Message#to_h
64
+ #
65
+ # @return [Hash]
66
+ #
67
+ # @api public
68
+ def to_h
69
+ @to_h ||= _path.to_h(dump)
56
70
  end
57
- alias to_s dump
58
71
 
59
72
  # See if another message is the same
60
73
  #
@@ -69,19 +82,32 @@ module Dry
69
82
  other.is_a?(String) ? text == other : super
70
83
  end
71
84
 
85
+ # @api private
86
+ def to_or(root)
87
+ clone = dup
88
+ clone.instance_variable_set("@path", path - root.to_a)
89
+ clone.instance_variable_set("@_path", nil)
90
+ clone
91
+ end
92
+
72
93
  # See which message is higher in the hierarchy
73
94
  #
74
95
  # @api private
75
96
  def <=>(other)
76
- l_path = Path[path]
77
- r_path = Path[other.path]
97
+ l_path = _path
98
+ r_path = other._path
78
99
 
79
100
  unless l_path.same_root?(r_path)
80
- raise ArgumentError, 'Cannot compare messages from different root paths'
101
+ raise ArgumentError, "Cannot compare messages from different root paths"
81
102
  end
82
103
 
83
104
  l_path <=> r_path
84
105
  end
106
+
107
+ # @api private
108
+ def _path
109
+ @_path ||= Path[path]
110
+ end
85
111
  end
86
112
  end
87
113
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/equalizer'
3
+ require "dry/schema/message/or/single_path"
4
+ require "dry/schema/message/or/multi_path"
4
5
 
5
6
  module Dry
6
7
  module Schema
@@ -8,45 +9,23 @@ module Dry
8
9
  #
9
10
  # @api public
10
11
  class Message
11
- # A message sub-type used by OR operations
12
- #
13
- # @api public
14
- class Or
12
+ module Or
15
13
  # @api private
16
- attr_reader :left
17
-
18
- # @api private
19
- attr_reader :right
20
-
21
- # @api private
22
- attr_reader :path
23
-
24
- # @api private
25
- attr_reader :messages
26
-
27
- # @api private
28
- def initialize(left, right, messages)
29
- @left = left
30
- @right = right
31
- @messages = messages
32
- @path = left.path
33
- end
34
-
35
- # Dump a message into a string
36
- #
37
- # @see Message#dump
38
- #
39
- # @return [String]
40
- #
41
- # @api public
42
- def dump
43
- to_a.map(&:dump).join(" #{messages[:or][:text]} ")
44
- end
45
- alias to_s dump
46
-
47
- # @api private
48
- def to_a
49
- [left, right]
14
+ def self.[](left, right, messages)
15
+ msgs = [left, right].flatten
16
+ paths = msgs.map(&:path)
17
+
18
+ if paths.uniq.size == 1
19
+ SinglePath.new(left, right, messages)
20
+ elsif right.is_a?(Array)
21
+ if left.is_a?(Array) && paths.uniq.size > 1
22
+ MultiPath.new(left, right)
23
+ else
24
+ right
25
+ end
26
+ else
27
+ msgs.max
28
+ end
50
29
  end
51
30
  end
52
31
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Schema
5
+ class Message
6
+ module Or
7
+ # A message type used by OR operations
8
+ #
9
+ # @abstract
10
+ #
11
+ # @api private
12
+ class Abstract
13
+ # @api private
14
+ attr_reader :left
15
+
16
+ # @api private
17
+ attr_reader :right
18
+
19
+ # @api private
20
+ def initialize(left, right)
21
+ @left = left
22
+ @right = right
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/equalizer"
4
+
5
+ require "dry/schema/message/or/abstract"
6
+ require "dry/schema/path"
7
+
8
+ module Dry
9
+ module Schema
10
+ class Message
11
+ module Or
12
+ # A message type used by OR operations with different paths
13
+ #
14
+ # @api public
15
+ class MultiPath < Abstract
16
+ # @api private
17
+ attr_reader :root
18
+
19
+ # @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) }
25
+ end
26
+
27
+ # @api public
28
+ 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
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/message/or/abstract"
4
+
5
+ module Dry
6
+ module Schema
7
+ class Message
8
+ module Or
9
+ # A message type used by OR operations with the same path
10
+ #
11
+ # @api public
12
+ class SinglePath < Abstract
13
+ # @api private
14
+ attr_reader :path
15
+
16
+ # @api private
17
+ attr_reader :_path
18
+
19
+ # @api private
20
+ attr_reader :messages
21
+
22
+ # @api private
23
+ def initialize(*args, messages)
24
+ super(*args)
25
+ @messages = messages
26
+ @path = left.path
27
+ @_path = left._path
28
+ end
29
+
30
+ # Dump a message into a string
31
+ #
32
+ # Both sides of the message will be joined using translated
33
+ # value under `dry_schema.or` message key
34
+ #
35
+ # @see Message#dump
36
+ #
37
+ # @return [String]
38
+ #
39
+ # @api public
40
+ def dump
41
+ @dump ||= "#{left.dump} #{messages[:or][:text]} #{right.dump}"
42
+ end
43
+ alias_method :to_s, :dump
44
+
45
+ # Dump an `or` message into a hash
46
+ #
47
+ # @see Message#to_h
48
+ #
49
+ # @return [String]
50
+ #
51
+ # @api public
52
+ def to_h
53
+ @to_h ||= _path.to_h(dump)
54
+ end
55
+
56
+ # @api private
57
+ def to_a
58
+ @to_a ||= [left, right]
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/initializer'
3
+ require "dry/initializer"
4
4
 
5
- require 'dry/schema/constants'
6
- require 'dry/schema/message'
7
- require 'dry/schema/message_set'
8
- require 'dry/schema/message_compiler/visitor_opts'
5
+ require "dry/schema/constants"
6
+ require "dry/schema/message"
7
+ require "dry/schema/message_set"
8
+ require "dry/schema/message_compiler/visitor_opts"
9
9
 
10
10
  module Dry
11
11
  module Schema
@@ -29,6 +29,14 @@ module Dry
29
29
 
30
30
  EMPTY_OPTS = VisitorOpts.new
31
31
  EMPTY_MESSAGE_SET = MessageSet.new(EMPTY_ARRAY).freeze
32
+ FULL_MESSAGE_WHITESPACE = Hash.new(' ').merge(
33
+ ja: '',
34
+ zh: '',
35
+ bn: '',
36
+ th: '',
37
+ lo: '',
38
+ my: '',
39
+ )
32
40
 
33
41
  param :messages
34
42
 
@@ -44,7 +52,7 @@ module Dry
44
52
  def initialize(messages, **options)
45
53
  super
46
54
  @options = options
47
- @default_lookup_options = options[:locale] ? { locale: locale } : EMPTY_HASH
55
+ @default_lookup_options = options[:locale] ? {locale: locale} : EMPTY_HASH
48
56
  end
49
57
 
50
58
  # @api private
@@ -100,17 +108,29 @@ module Dry
100
108
  end
101
109
  end
102
110
 
111
+ # @api private
112
+ def visit_unexpected_key(node, _opts)
113
+ path, input = node
114
+
115
+ msg = messages.translate("errors.unexpected_key")
116
+
117
+ Message.new(
118
+ path: path,
119
+ text: msg[:text],
120
+ predicate: nil,
121
+ input: input
122
+ )
123
+ end
124
+
103
125
  # @api private
104
126
  def visit_or(node, opts)
105
127
  left, right = node.map { |n| visit(n, opts) }
128
+ Message::Or[left, right, or_translator]
129
+ end
106
130
 
107
- if [left, right].flatten.map(&:path).uniq.size == 1
108
- Message::Or.new(left, right, proc { |k| messages.translate(k, **default_lookup_options) })
109
- elsif right.is_a?(Array)
110
- right
111
- else
112
- [left, right].flatten.max
113
- end
131
+ # @api private
132
+ def or_translator
133
+ @or_translator ||= proc { |k| messages.translate(k, **default_lookup_options) }
114
134
  end
115
135
 
116
136
  # @api private
@@ -130,13 +150,21 @@ module Dry
130
150
  path: path.last, **tokens, **lookup_options(arg_vals: arg_vals, input: input)
131
151
  ).to_h
132
152
 
133
- template, meta = messages[predicate, options] ||
134
- raise(MissingMessageError.new(path, messages.looked_up_paths(predicate, options)))
153
+ template, meta = messages[predicate, options]
154
+
155
+ unless template
156
+ raise MissingMessageError.new(path, messages.looked_up_paths(predicate, options))
157
+ end
135
158
 
136
159
  text = message_text(template, tokens, options)
137
160
 
138
161
  message_type(options).new(
139
- text: text, path: path, predicate: predicate, args: arg_vals, input: input, meta: meta
162
+ text: text,
163
+ meta: meta,
164
+ path: path,
165
+ predicate: predicate,
166
+ args: arg_vals,
167
+ input: input
140
168
  )
141
169
  end
142
170
 
@@ -180,15 +208,15 @@ module Dry
180
208
  def message_text(template, tokens, options)
181
209
  text = template[template.data(tokens)]
182
210
 
183
- return text unless full
211
+ return text if !text || !full
184
212
 
185
213
  rule = options[:path]
186
- "#{messages.rule(rule, options) || rule} #{text}"
214
+ [messages.rule(rule, options) || rule, text].join(FULL_MESSAGE_WHITESPACE[template.options[:locale]])
187
215
  end
188
216
 
189
217
  # @api private
190
218
  def message_tokens(args)
191
- args.each_with_object({}) do |arg, hash|
219
+ tokens = args.each_with_object({}) do |arg, hash|
192
220
  case arg[1]
193
221
  when Array
194
222
  hash[arg[0]] = arg[1].join(LIST_SEPARATOR)
@@ -199,6 +227,14 @@ module Dry
199
227
  hash[arg[0]] = arg[1]
200
228
  end
201
229
  end
230
+ args.any? { |e| e.first == :size } ? append_mapped_size_tokens(tokens) : tokens
231
+ end
232
+
233
+ # @api private
234
+ def append_mapped_size_tokens(tokens)
235
+ # this is a temporary fix for the inconsistency in the "size" errors arguments
236
+ mapped_hash = tokens.each_with_object({}) { |(k, v), h| h[k.to_s.gsub("size", "num").to_sym] = v }
237
+ tokens.merge(mapped_hash)
202
238
  end
203
239
  end
204
240
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/constants'
4
- require 'dry/schema/message'
3
+ require "dry/schema/constants"
4
+ require "dry/schema/message"
5
5
 
6
6
  module Dry
7
7
  module Schema
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/equalizer'
3
+ require "dry/equalizer"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -17,13 +17,7 @@ module Dry
17
17
  #
18
18
  # @return [Array<Message>]
19
19
  attr_reader :messages
20
-
21
- # An internal hash that is filled in with dumped messages
22
- # when a message set is coerced to a hash
23
- #
24
- # @return [Hash<Symbol=>[Array,Hash]>]
25
- attr_reader :placeholders
26
-
20
+
27
21
  # Options hash
28
22
  #
29
23
  # @return [Hash]
@@ -38,7 +32,6 @@ module Dry
38
32
  def initialize(messages, options = EMPTY_HASH)
39
33
  @messages = messages
40
34
  @options = options
41
- initialize_placeholders!
42
35
  end
43
36
 
44
37
  # Iterate over messages
@@ -112,43 +105,39 @@ module Dry
112
105
 
113
106
  # @api private
114
107
  def messages_map(messages = self.messages)
115
- return EMPTY_HASH if empty?
116
-
117
- messages.group_by(&:path).reduce(placeholders) do |hash, (path, msgs)|
118
- node = path.reduce(hash) { |a, e| a[e] }
108
+ combine_message_hashes(messages.map(&:to_h))
109
+ end
119
110
 
120
- msgs.each do |msg|
121
- node << msg
111
+ # @api private
112
+ def combine_message_hashes(hashes)
113
+ hashes.reduce(EMPTY_HASH.dup) do |a, e|
114
+ a.merge(e) do |_, *values|
115
+ combine_message_values(values)
122
116
  end
123
-
124
- node.map!(&:dump)
125
-
126
- hash
127
117
  end
128
118
  end
129
119
 
130
120
  # @api private
131
- def paths
132
- @paths ||= messages.map(&:path).uniq
121
+ def combine_message_values(values)
122
+ hashes, other = partition_message_values(values)
123
+ combined = combine_message_hashes(hashes)
124
+ flattened = other.flatten
125
+
126
+ if flattened.empty?
127
+ combined
128
+ elsif combined.empty?
129
+ flattened
130
+ else
131
+ [flattened, combined]
132
+ end
133
133
  end
134
134
 
135
135
  # @api private
136
- def initialize_placeholders!
137
- return @placeholders = EMPTY_HASH if empty?
138
-
139
- @placeholders = paths.reduce(EMPTY_HASH.dup) do |hash, path|
140
- curr_idx = 0
141
- last_idx = path.size - 1
142
- node = hash
143
-
144
- while curr_idx <= last_idx
145
- key = path[curr_idx]
146
- node = (node[key] || node[key] = curr_idx < last_idx ? {} : [])
147
- curr_idx += 1
148
- end
149
-
150
- hash
151
- end
136
+ def partition_message_values(values)
137
+ values
138
+ .map { |value| value.is_a?(Array) ? value : [value] }
139
+ .reduce(EMPTY_ARRAY.dup, :+)
140
+ .partition { |value| value.is_a?(Hash) && !value[:text].is_a?(String) }
152
141
  end
153
142
  end
154
143
  end