dry-schema 1.4.3 → 1.5.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +170 -97
  3. data/config/errors.yml +4 -0
  4. data/dry-schema.gemspec +46 -0
  5. data/lib/dry-schema.rb +1 -1
  6. data/lib/dry/schema.rb +19 -6
  7. data/lib/dry/schema/compiler.rb +4 -4
  8. data/lib/dry/schema/config.rb +15 -6
  9. data/lib/dry/schema/constants.rb +16 -7
  10. data/lib/dry/schema/dsl.rb +88 -27
  11. data/lib/dry/schema/extensions.rb +10 -2
  12. data/lib/dry/schema/extensions/hints.rb +15 -8
  13. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +1 -1
  14. data/lib/dry/schema/extensions/hints/message_set_methods.rb +0 -47
  15. data/lib/dry/schema/extensions/info.rb +27 -0
  16. data/lib/dry/schema/extensions/info/schema_compiler.rb +105 -0
  17. data/lib/dry/schema/extensions/monads.rb +1 -1
  18. data/lib/dry/schema/extensions/struct.rb +32 -0
  19. data/lib/dry/schema/json.rb +1 -1
  20. data/lib/dry/schema/key.rb +16 -1
  21. data/lib/dry/schema/key_coercer.rb +4 -4
  22. data/lib/dry/schema/key_map.rb +9 -4
  23. data/lib/dry/schema/key_validator.rb +66 -0
  24. data/lib/dry/schema/macros.rb +8 -8
  25. data/lib/dry/schema/macros/array.rb +17 -4
  26. data/lib/dry/schema/macros/core.rb +9 -4
  27. data/lib/dry/schema/macros/dsl.rb +34 -19
  28. data/lib/dry/schema/macros/each.rb +4 -4
  29. data/lib/dry/schema/macros/filled.rb +5 -5
  30. data/lib/dry/schema/macros/hash.rb +21 -3
  31. data/lib/dry/schema/macros/key.rb +9 -9
  32. data/lib/dry/schema/macros/maybe.rb +3 -3
  33. data/lib/dry/schema/macros/optional.rb +1 -1
  34. data/lib/dry/schema/macros/required.rb +1 -1
  35. data/lib/dry/schema/macros/schema.rb +23 -2
  36. data/lib/dry/schema/macros/value.rb +32 -10
  37. data/lib/dry/schema/message.rb +35 -9
  38. data/lib/dry/schema/message/or.rb +18 -39
  39. data/lib/dry/schema/message/or/abstract.rb +28 -0
  40. data/lib/dry/schema/message/or/multi_path.rb +37 -0
  41. data/lib/dry/schema/message/or/single_path.rb +64 -0
  42. data/lib/dry/schema/message_compiler.rb +37 -17
  43. data/lib/dry/schema/message_compiler/visitor_opts.rb +2 -2
  44. data/lib/dry/schema/message_set.rb +25 -36
  45. data/lib/dry/schema/messages.rb +6 -6
  46. data/lib/dry/schema/messages/abstract.rb +54 -56
  47. data/lib/dry/schema/messages/i18n.rb +29 -27
  48. data/lib/dry/schema/messages/namespaced.rb +12 -2
  49. data/lib/dry/schema/messages/template.rb +19 -44
  50. data/lib/dry/schema/messages/yaml.rb +60 -13
  51. data/lib/dry/schema/params.rb +1 -1
  52. data/lib/dry/schema/path.rb +44 -5
  53. data/lib/dry/schema/predicate.rb +2 -2
  54. data/lib/dry/schema/predicate_inferrer.rb +4 -184
  55. data/lib/dry/schema/predicate_registry.rb +2 -2
  56. data/lib/dry/schema/primitive_inferrer.rb +16 -0
  57. data/lib/dry/schema/processor.rb +49 -28
  58. data/lib/dry/schema/processor_steps.rb +50 -27
  59. data/lib/dry/schema/result.rb +43 -5
  60. data/lib/dry/schema/rule_applier.rb +8 -7
  61. data/lib/dry/schema/step.rb +79 -0
  62. data/lib/dry/schema/trace.rb +5 -4
  63. data/lib/dry/schema/type_container.rb +3 -3
  64. data/lib/dry/schema/type_registry.rb +2 -2
  65. data/lib/dry/schema/types.rb +1 -1
  66. data/lib/dry/schema/value_coercer.rb +2 -2
  67. data/lib/dry/schema/version.rb +1 -1
  68. metadata +22 -8
@@ -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
@@ -44,7 +44,7 @@ module Dry
44
44
  def initialize(messages, **options)
45
45
  super
46
46
  @options = options
47
- @default_lookup_options = options[:locale] ? { locale: locale } : EMPTY_HASH
47
+ @default_lookup_options = options[:locale] ? {locale: locale} : EMPTY_HASH
48
48
  end
49
49
 
50
50
  # @api private
@@ -100,17 +100,29 @@ module Dry
100
100
  end
101
101
  end
102
102
 
103
+ # @api private
104
+ def visit_unexpected_key(node, _opts)
105
+ path, input = node
106
+
107
+ msg = messages.translate("errors.unexpected_key")
108
+
109
+ Message.new(
110
+ path: path,
111
+ text: msg[:text],
112
+ predicate: nil,
113
+ input: input
114
+ )
115
+ end
116
+
103
117
  # @api private
104
118
  def visit_or(node, opts)
105
119
  left, right = node.map { |n| visit(n, opts) }
120
+ Message::Or[left, right, or_translator]
121
+ end
106
122
 
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
123
+ # @api private
124
+ def or_translator
125
+ @or_translator ||= proc { |k| messages.translate(k, **default_lookup_options) }
114
126
  end
115
127
 
116
128
  # @api private
@@ -130,13 +142,21 @@ module Dry
130
142
  path: path.last, **tokens, **lookup_options(arg_vals: arg_vals, input: input)
131
143
  ).to_h
132
144
 
133
- template, meta = messages[predicate, options] ||
134
- raise(MissingMessageError.new(path, messages.looked_up_paths(predicate, options)))
145
+ template, meta = messages[predicate, options]
146
+
147
+ unless template
148
+ raise MissingMessageError.new(path, messages.looked_up_paths(predicate, options))
149
+ end
135
150
 
136
151
  text = message_text(template, tokens, options)
137
152
 
138
153
  message_type(options).new(
139
- text: text, path: path, predicate: predicate, args: arg_vals, input: input, meta: meta
154
+ text: text,
155
+ meta: meta,
156
+ path: path,
157
+ predicate: predicate,
158
+ args: arg_vals,
159
+ input: input
140
160
  )
141
161
  end
142
162
 
@@ -180,7 +200,7 @@ module Dry
180
200
  def message_text(template, tokens, options)
181
201
  text = template[template.data(tokens)]
182
202
 
183
- return text unless full
203
+ return text if !text || !full
184
204
 
185
205
  rule = options[:path]
186
206
  "#{messages.rule(rule, options) || rule} #{text}"
@@ -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
@@ -18,12 +18,6 @@ module Dry
18
18
  # @return [Array<Message>]
19
19
  attr_reader :messages
20
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
-
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
@@ -7,8 +7,8 @@ module Dry
7
7
  # @api private
8
8
  module Messages
9
9
  BACKENDS = {
10
- i18n: 'I18n',
11
- yaml: 'YAML'
10
+ i18n: "I18n",
11
+ yaml: "YAML"
12
12
  }.freeze
13
13
 
14
14
  module_function
@@ -31,7 +31,7 @@ module Dry
31
31
  end
32
32
  end
33
33
 
34
- require 'dry/schema/messages/abstract'
35
- require 'dry/schema/messages/namespaced'
36
- require 'dry/schema/messages/yaml'
37
- require 'dry/schema/messages/i18n' if defined?(I18n)
34
+ require "dry/schema/messages/abstract"
35
+ require "dry/schema/messages/namespaced"
36
+ require "dry/schema/messages/yaml"
37
+ require "dry/schema/messages/i18n" if defined?(I18n)
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
- require 'concurrent/map'
5
- require 'dry/equalizer'
6
- require 'dry/configurable'
3
+ require "set"
4
+ require "concurrent/map"
5
+ require "dry/equalizer"
6
+ require "dry/configurable"
7
7
 
8
- require 'dry/schema/constants'
9
- require 'dry/schema/messages/template'
8
+ require "dry/schema/constants"
9
+ require "dry/schema/messages/template"
10
10
 
11
11
  module Dry
12
12
  module Schema
@@ -21,36 +21,31 @@ module Dry
21
21
  setting :default_locale, nil
22
22
  setting :load_paths, Set[DEFAULT_MESSAGES_PATH]
23
23
  setting :top_namespace, DEFAULT_MESSAGES_ROOT
24
- setting :root, 'errors'
24
+ setting :root, "errors"
25
25
  setting :lookup_options, %i[root predicate path val_type arg_type].freeze
26
26
 
27
27
  setting :lookup_paths, [
28
- '%<root>s.rules.%<path>s.%<predicate>s.arg.%<arg_type>s',
29
- '%<root>s.rules.%<path>s.%<predicate>s',
30
- '%<root>s.%<predicate>s.%<message_type>s',
31
- '%<root>s.%<predicate>s.value.%<path>s',
32
- '%<root>s.%<predicate>s.value.%<val_type>s.arg.%<arg_type>s',
33
- '%<root>s.%<predicate>s.value.%<val_type>s',
34
- '%<root>s.%<predicate>s.arg.%<arg_type>s',
35
- '%<root>s.%<predicate>s'
28
+ "%<root>s.rules.%<path>s.%<predicate>s.arg.%<arg_type>s",
29
+ "%<root>s.rules.%<path>s.%<predicate>s",
30
+ "%<root>s.%<predicate>s.%<message_type>s",
31
+ "%<root>s.%<predicate>s.value.%<path>s",
32
+ "%<root>s.%<predicate>s.value.%<val_type>s.arg.%<arg_type>s",
33
+ "%<root>s.%<predicate>s.value.%<val_type>s",
34
+ "%<root>s.%<predicate>s.arg.%<arg_type>s",
35
+ "%<root>s.%<predicate>s"
36
36
  ].freeze
37
37
 
38
- setting :rule_lookup_paths, ['rules.%<name>s'].freeze
38
+ setting :rule_lookup_paths, ["rules.%<name>s"].freeze
39
39
 
40
- setting :arg_types, Hash.new { |*| 'default' }.update(
41
- Range => 'range'
40
+ setting :arg_types, Hash.new { |*| "default" }.update(
41
+ Range => "range"
42
42
  )
43
43
 
44
- setting :val_types, Hash.new { |*| 'default' }.update(
45
- Range => 'range',
46
- String => 'string'
44
+ setting :val_types, Hash.new { |*| "default" }.update(
45
+ Range => "range",
46
+ String => "string"
47
47
  )
48
48
 
49
- # @api private
50
- def self.cache
51
- @cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
52
- end
53
-
54
49
  # @api private
55
50
  def self.build(options = EMPTY_HASH)
56
51
  messages = new
@@ -79,7 +74,7 @@ module Dry
79
74
 
80
75
  # @api private
81
76
  def rule(name, options = {})
82
- tokens = { name: name, locale: options.fetch(:locale, default_locale) }
77
+ tokens = {name: name, locale: options.fetch(:locale, default_locale)}
83
78
  path = rule_lookup_paths(tokens).detect { |key| key?(key, options) }
84
79
 
85
80
  rule = get(path, options) if path
@@ -92,13 +87,35 @@ module Dry
92
87
  #
93
88
  # @api public
94
89
  def call(predicate, options)
95
- cache.fetch_or_store(cache_key(predicate, options)) do
96
- text, meta = lookup(predicate, options)
97
- [Template[text], meta] if text
98
- end
90
+ options = {locale: default_locale, **options}
91
+ opts = options.reject { |k,| config.lookup_options.include?(k) }
92
+ path = lookup_paths(predicate, options).detect { |key| key?(key, opts) }
93
+
94
+ return unless path
95
+
96
+ result = get(path, opts)
97
+
98
+ [
99
+ Template.new(
100
+ messages: self,
101
+ key: path,
102
+ options: opts
103
+ ),
104
+ result[:meta]
105
+ ]
99
106
  end
107
+
100
108
  alias_method :[], :call
101
109
 
110
+ # Check if given key is defined
111
+ #
112
+ # @return [Boolean]
113
+ #
114
+ # @api public
115
+ def key?(_key, _options = EMPTY_HASH)
116
+ raise NotImplementedError
117
+ end
118
+
102
119
  # Retrieve an array of looked up paths
103
120
  #
104
121
  # @param [Symbol] predicate
@@ -112,21 +129,6 @@ module Dry
112
129
  filled_lookup_paths(tokens)
113
130
  end
114
131
 
115
- # Try to find a message for the given predicate and its options
116
- #
117
- # @api private
118
- #
119
- # rubocop:disable Metrics/AbcSize
120
- def lookup(predicate, options)
121
- opts = options.reject { |k, _| config.lookup_options.include?(k) }
122
- path = lookup_paths(predicate, options).detect { |key| key?(key, opts) }
123
-
124
- return unless path
125
-
126
- get(path, opts).values_at(:text, :meta)
127
- end
128
- # rubocop:enable Metrics/AbcSize
129
-
130
132
  # @api private
131
133
  def lookup_paths(predicate, options)
132
134
  tokens = lookup_tokens(predicate, options)
@@ -162,22 +164,18 @@ module Dry
162
164
  end
163
165
 
164
166
  # @api private
165
- def cache
166
- @cache ||= self.class.cache[self]
167
+ def default_locale
168
+ config.default_locale
167
169
  end
168
170
 
169
171
  # @api private
170
- def default_locale
171
- config.default_locale
172
+ def interpolatable_data(_key, _options, **_data)
173
+ raise NotImplementedError
172
174
  end
173
175
 
174
176
  # @api private
175
- def cache_key(predicate, options)
176
- if options.key?(:input)
177
- [predicate, options.reject { |k,| k.equal?(:input) }]
178
- else
179
- [predicate, options]
180
- end
177
+ def interpolate(_key, _options, **_data)
178
+ raise NotImplementedError
181
179
  end
182
180
 
183
181
  private