dry-schema 1.3.4 → 1.5.0

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 +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}"