dry-schema 1.3.4 → 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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +253 -101
  3. data/LICENSE +1 -1
  4. data/README.md +6 -6
  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 +20 -7
  9. data/lib/dry/schema/compiler.rb +4 -4
  10. data/lib/dry/schema/config.rb +15 -6
  11. data/lib/dry/schema/constants.rb +19 -9
  12. data/lib/dry/schema/dsl.rb +144 -38
  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 +2 -2
  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 +20 -5
  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 +66 -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 +11 -6
  29. data/lib/dry/schema/macros/dsl.rb +53 -21
  30. data/lib/dry/schema/macros/each.rb +4 -4
  31. data/lib/dry/schema/macros/filled.rb +5 -6
  32. data/lib/dry/schema/macros/hash.rb +21 -3
  33. data/lib/dry/schema/macros/key.rb +10 -10
  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 +40 -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 +79 -66
  49. data/lib/dry/schema/messages/i18n.rb +36 -10
  50. data/lib/dry/schema/messages/namespaced.rb +13 -3
  51. data/lib/dry/schema/messages/template.rb +19 -44
  52. data/lib/dry/schema/messages/yaml.rb +72 -13
  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 +2 -2
  56. data/lib/dry/schema/predicate_inferrer.rb +4 -184
  57. data/lib/dry/schema/predicate_registry.rb +3 -24
  58. data/lib/dry/schema/primitive_inferrer.rb +3 -86
  59. data/lib/dry/schema/processor.rb +54 -50
  60. data/lib/dry/schema/processor_steps.rb +139 -0
  61. data/lib/dry/schema/result.rb +52 -5
  62. data/lib/dry/schema/rule_applier.rb +8 -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,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/macros/dsl'
3
+ require "dry/schema/path"
4
+ require "dry/schema/macros/dsl"
4
5
 
5
6
  module Dry
6
7
  module Schema
@@ -13,6 +14,8 @@ module Dry
13
14
  def call(*predicates, **opts, &block)
14
15
  schema = predicates.detect { |predicate| predicate.is_a?(Processor) }
15
16
 
17
+ type_spec = opts[:type_spec]
18
+
16
19
  if schema
17
20
  current_type = schema_dsl.types[name]
18
21
 
@@ -23,17 +26,32 @@ module Dry
23
26
  schema.type_schema
24
27
  end
25
28
 
26
- schema_dsl.set_type(name, updated_type)
29
+ import_steps(schema)
30
+
31
+ type(updated_type) unless custom_type? && !current_type.respond_to?(:of)
27
32
  end
28
33
 
29
- trace.evaluate(*predicates, **opts)
30
- trace.append(new(chain: false).instance_exec(&block)) if block
34
+ trace_opts = opts.reject { |key, _| key == :type_spec || key == :type_rule }
35
+
36
+ if (type_rule = opts[:type_rule])
37
+ trace.append(type_rule).evaluate(*predicates, **trace_opts)
38
+ trace.append(new(chain: false).instance_exec(&block)) if block
39
+ else
40
+ trace.evaluate(*predicates, **trace_opts)
41
+
42
+ if block && type_spec.equal?(:hash)
43
+ hash(&block)
44
+ elsif type_spec.is_a?(::Dry::Types::Type) && hash_type?(type_spec)
45
+ hash(type_spec)
46
+ elsif block
47
+ trace.append(new(chain: false).instance_exec(&block))
48
+ end
49
+ end
31
50
 
32
51
  if trace.captures.empty?
33
- raise ArgumentError, 'wrong number of arguments (given 0, expected at least 1)'
52
+ raise ArgumentError, "wrong number of arguments (given 0, expected at least 1)"
34
53
  end
35
54
 
36
- type_spec = opts[:type_spec]
37
55
  each(type_spec.type.member) if type_spec.respond_to?(:member)
38
56
 
39
57
  self
@@ -44,12 +62,16 @@ module Dry
44
62
  primitive_inferrer[type].eql?([::Array])
45
63
  end
46
64
 
65
+ def hash_type?(type)
66
+ primitive_inferrer[type].eql?([::Hash])
67
+ end
68
+
47
69
  # @api private
48
70
  def build_array_type(array_type, member)
49
71
  if array_type.respond_to?(:of)
50
72
  array_type.of(member)
51
73
  else
52
- raise ArgumentError, <<~ERROR.split("\n").join(' ')
74
+ raise ArgumentError, <<~ERROR.split("\n").join(" ")
53
75
  Cannot define schema for a nominal array type.
54
76
  Array types must be instances of Dry::Types::Array,
55
77
  usually constructed with Types::Constructor(Array) { ... } or
@@ -58,6 +80,11 @@ module Dry
58
80
  end
59
81
  end
60
82
 
83
+ # @api private
84
+ def import_steps(schema)
85
+ schema_dsl.steps.import_callbacks(Path[[*path, name]], schema.steps)
86
+ end
87
+
61
88
  # @api private
62
89
  def respond_to_missing?(meth, include_private = false)
63
90
  super || meth.to_s.end_with?(QUESTION_MARK)
@@ -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
@@ -41,10 +41,10 @@ module Dry
41
41
  attr_reader :default_lookup_options
42
42
 
43
43
  # @api private
44
- def initialize(messages, options = EMPTY_HASH)
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
@@ -55,7 +55,7 @@ module Dry
55
55
 
56
56
  return self if updated_opts.eql?(options)
57
57
 
58
- self.class.new(messages, updated_opts)
58
+ self.class.new(messages, **updated_opts)
59
59
  end
60
60
 
61
61
  # @api private
@@ -100,23 +100,35 @@ 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
117
129
  def visit_namespace(node, opts)
118
130
  ns, rest = node
119
- self.class.new(messages.namespaced(ns), options).visit(rest, opts)
131
+ self.class.new(messages.namespaced(ns), **options).visit(rest, opts)
120
132
  end
121
133
 
122
134
  # @api private
@@ -130,12 +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] || raise(MissingMessageError, path)
145
+ template, meta = messages[predicate, options]
146
+
147
+ unless template
148
+ raise MissingMessageError.new(path, messages.looked_up_paths(predicate, options))
149
+ end
134
150
 
135
151
  text = message_text(template, tokens, options)
136
152
 
137
153
  message_type(options).new(
138
- 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
139
160
  )
140
161
  end
141
162
 
@@ -179,7 +200,7 @@ module Dry
179
200
  def message_text(template, tokens, options)
180
201
  text = template[template.data(tokens)]
181
202
 
182
- return text unless full
203
+ return text if !text || !full
183
204
 
184
205
  rule = options[:path]
185
206
  "#{messages.rule(rule, options) || rule} #{text}"