dry-schema 1.4.2 → 1.5.3

Sign up to get free protection for your applications and to get access to all the features.
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