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,9 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Dry::Schema.register_extension(:monads) do
4
- require 'dry/schema/extensions/monads'
4
+ require "dry/schema/extensions/monads"
5
5
  end
6
6
 
7
7
  Dry::Schema.register_extension(:hints) do
8
- require 'dry/schema/extensions/hints'
8
+ require "dry/schema/extensions/hints"
9
+ end
10
+
11
+ Dry::Schema.register_extension(:struct) do
12
+ require "dry/schema/extensions/struct"
13
+ end
14
+
15
+ Dry::Schema.register_extension(:info) do
16
+ require "dry/schema/extensions/info"
9
17
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/compiler'
4
- require 'dry/schema/message'
5
- require 'dry/schema/message_compiler'
3
+ require "dry/schema/compiler"
4
+ require "dry/schema/message"
5
+ require "dry/schema/message_compiler"
6
6
 
7
- require 'dry/schema/extensions/hints/compiler_methods'
8
- require 'dry/schema/extensions/hints/message_compiler_methods'
9
- require 'dry/schema/extensions/hints/message_set_methods'
10
- require 'dry/schema/extensions/hints/result_methods'
7
+ require "dry/schema/extensions/hints/compiler_methods"
8
+ require "dry/schema/extensions/hints/message_compiler_methods"
9
+ require "dry/schema/extensions/hints/message_set_methods"
10
+ require "dry/schema/extensions/hints/result_methods"
11
11
 
12
12
  module Dry
13
13
  module Schema
@@ -22,7 +22,14 @@ module Dry
22
22
  # @see Message::Or
23
23
  #
24
24
  # @api public
25
- class Or
25
+ class Or::SinglePath
26
+ # @api private
27
+ def hint?
28
+ false
29
+ end
30
+ end
31
+
32
+ class Or::MultiPath
26
33
  # @api private
27
34
  def hint?
28
35
  false
@@ -19,7 +19,7 @@ module Dry
19
19
  attr_reader :hints
20
20
 
21
21
  # @api private
22
- def initialize(*args)
22
+ def initialize(*, **)
23
23
  super
24
24
  @hints = @options.fetch(:hints, true)
25
25
  end
@@ -73,7 +73,7 @@ module Dry
73
73
  end
74
74
 
75
75
  # @api private
76
- def visit_each(node, opts)
76
+ def visit_each(_node, _opts)
77
77
  # TODO: we can still generate a hint for elements here!
78
78
  []
79
79
  end
@@ -37,53 +37,6 @@ module Dry
37
37
  @to_h ||= failures ? messages_map : messages_map(hints)
38
38
  end
39
39
  alias_method :to_hash, :to_h
40
-
41
- private
42
-
43
- # @api private
44
- def unique_paths
45
- messages.uniq(&:path).map(&:path)
46
- end
47
-
48
- # @api private
49
- def messages_map(messages = self.messages)
50
- return EMPTY_HASH if empty?
51
-
52
- messages.reduce(placeholders) { |hash, msg|
53
- node = msg.path.reduce(hash) { |a, e| a.is_a?(Hash) ? a[e] : a.last[e] }
54
- (node[0].is_a?(::Array) ? node[0] : node) << msg.dump
55
- hash
56
- }
57
- end
58
-
59
- # @api private
60
- #
61
- # rubocop:disable Metrics/AbcSize
62
- # rubocop:disable Metrics/PerceivedComplexity
63
- def initialize_placeholders!
64
- @placeholders = unique_paths.each_with_object(EMPTY_HASH.dup) { |path, hash|
65
- curr_idx = 0
66
- last_idx = path.size - 1
67
- node = hash
68
-
69
- while curr_idx <= last_idx
70
- key = path[curr_idx]
71
-
72
- next_node =
73
- if node.is_a?(Array) && key.is_a?(Symbol)
74
- node_hash = (node << [] << {}).last
75
- node_hash[key] || (node_hash[key] = curr_idx < last_idx ? {} : [])
76
- else
77
- node[key] || (node[key] = curr_idx < last_idx ? {} : [])
78
- end
79
-
80
- node = next_node
81
- curr_idx += 1
82
- end
83
- }
84
- end
85
- # rubocop:enable Metrics/AbcSize
86
- # rubocop:enable Metrics/PerceivedComplexity
87
40
  end
88
41
  end
89
42
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/extensions/info/schema_compiler"
4
+
5
+ module Dry
6
+ module Schema
7
+ # Info extension
8
+ #
9
+ # @api public
10
+ module Info
11
+ module SchemaMethods
12
+ # Return information about keys and types
13
+ #
14
+ # @return [Hash<Symbol=>Hash>]
15
+ #
16
+ # @api public
17
+ def info
18
+ compiler = SchemaCompiler.new
19
+ compiler.call(to_ast)
20
+ compiler.to_h
21
+ end
22
+ end
23
+ end
24
+
25
+ Processor.include(Info::SchemaMethods)
26
+ end
27
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/constants"
4
+
5
+ module Dry
6
+ module Schema
7
+ # @api private
8
+ module Info
9
+ # @api private
10
+ class SchemaCompiler
11
+ PREDICATE_TO_TYPE = {
12
+ array?: "array",
13
+ bool?: "bool",
14
+ date?: "date",
15
+ date_time?: "date_time",
16
+ decimal?: "float",
17
+ float?: "float",
18
+ hash?: "hash",
19
+ int?: "integer",
20
+ nil?: "nil",
21
+ str?: "string",
22
+ time?: "time"
23
+ }.freeze
24
+
25
+ # @api private
26
+ attr_reader :keys
27
+
28
+ # @api private
29
+ def initialize
30
+ @keys = EMPTY_HASH.dup
31
+ end
32
+
33
+ # @api private
34
+ def to_h
35
+ {keys: keys}
36
+ end
37
+
38
+ # @api private
39
+ def call(ast)
40
+ visit(ast)
41
+ end
42
+
43
+ # @api private
44
+ def visit(node, opts = EMPTY_HASH)
45
+ meth, rest = node
46
+ public_send(:"visit_#{meth}", rest, opts)
47
+ end
48
+
49
+ # @api private
50
+ def visit_set(node, opts = EMPTY_HASH)
51
+ target = (key = opts[:key]) ? self.class.new : self
52
+
53
+ node.map { |child| target.visit(child, opts) }
54
+
55
+ return unless key
56
+
57
+ target_info = opts[:member] ? {member: target.to_h} : target.to_h
58
+ type = opts[:member] ? "array" : "hash"
59
+
60
+ keys.update(key => {**keys[key], type: type, **target_info})
61
+ end
62
+
63
+ # @api private
64
+ def visit_and(node, opts = EMPTY_HASH)
65
+ left, right = node
66
+
67
+ visit(left, opts)
68
+ visit(right, opts)
69
+ end
70
+
71
+ # @api private
72
+ def visit_implication(node, opts = EMPTY_HASH)
73
+ node.each do |el|
74
+ visit(el, opts.merge(required: false))
75
+ end
76
+ end
77
+
78
+ # @api private
79
+ def visit_each(node, opts = EMPTY_HASH)
80
+ visit(node, opts.merge(member: true))
81
+ end
82
+
83
+ # @api private
84
+ def visit_key(node, opts = EMPTY_HASH)
85
+ name, rest = node
86
+ visit(rest, opts.merge(key: name, required: true))
87
+ end
88
+
89
+ # @api private
90
+ def visit_predicate(node, opts = EMPTY_HASH)
91
+ name, rest = node
92
+
93
+ key = opts[:key]
94
+
95
+ if name.equal?(:key?)
96
+ keys[rest[0][1]] = {required: opts.fetch(:required, true)}
97
+ else
98
+ type = PREDICATE_TO_TYPE[name]
99
+ keys[key][:type] = type if type
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/monads/result'
3
+ require "dry/monads/result"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/struct"
4
+ require "dry/schema/predicate_inferrer"
5
+ require "dry/schema/primitive_inferrer"
6
+ require "dry/schema/macros/dsl"
7
+ require "dry/schema/macros/hash"
8
+
9
+ module Dry
10
+ module Schema
11
+ module Macros
12
+ Hash.prepend(::Module.new {
13
+ def call(*args)
14
+ if args.size >= 1 && args[0].is_a?(::Class) && args[0] <= ::Dry::Struct
15
+ if block_given?
16
+ raise ArgumentError, "blocks are not supported when using "\
17
+ "a struct class (#{name.inspect} => #{args[0]})"
18
+ end
19
+
20
+ super(args[0].schema, *args.drop(1))
21
+ type(schema_dsl.types[name].constructor(args[0]))
22
+ else
23
+ super
24
+ end
25
+ end
26
+ })
27
+ end
28
+
29
+ PredicateInferrer::Compiler.send(:alias_method, :visit_struct, :visit_hash)
30
+ PrimitiveInferrer::Compiler.send(:alias_method, :visit_struct, :visit_hash)
31
+ end
32
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/processor'
3
+ require "dry/schema/processor"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -27,8 +27,8 @@ module Dry
27
27
  end
28
28
 
29
29
  # @api private
30
- def self.new(*args)
31
- fetch_or_store(*args) { super }
30
+ def self.new(*args, **kwargs)
31
+ fetch_or_store(args, kwargs) { super }
32
32
  end
33
33
 
34
34
  # @api private
@@ -65,8 +65,13 @@ module Dry
65
65
  end
66
66
 
67
67
  # @api private
68
- def new(new_opts = EMPTY_HASH)
69
- self.class.new(id, { name: name, coercer: coercer }.merge(new_opts))
68
+ def to_dot_notation
69
+ [name.to_s]
70
+ end
71
+
72
+ # @api private
73
+ def new(**new_opts)
74
+ self.class.new(id, name: name, coercer: coercer, **new_opts)
70
75
  end
71
76
 
72
77
  # @api private
@@ -118,9 +123,14 @@ module Dry
118
123
  new(name: name.to_s, members: members.stringified)
119
124
  end
120
125
 
126
+ # @api private
127
+ def to_dot_notation
128
+ [name].product(members.flat_map(&:to_dot_notation)).map { |e| e.join(DOT) }
129
+ end
130
+
121
131
  # @api private
122
132
  def dump
123
- { name => members.map(&:dump) }
133
+ {name => members.map(&:dump)}
124
134
  end
125
135
  end
126
136
 
@@ -155,6 +165,11 @@ module Dry
155
165
  new(name: name.to_s, member: member.stringified)
156
166
  end
157
167
 
168
+ # @api private
169
+ def to_dot_notation
170
+ [:"#{name}[]"].product(member.to_dot_notation).map { |el| el.join(DOT) }
171
+ end
172
+
158
173
  # @api private
159
174
  def dump
160
175
  [name, member.dump]
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/cache'
4
- require 'dry/equalizer'
3
+ require "dry/core/cache"
4
+ require "dry/equalizer"
5
5
 
6
6
  module Dry
7
7
  module Schema
@@ -32,8 +32,8 @@ module Dry
32
32
  end
33
33
 
34
34
  # @api private
35
- def call(source)
36
- key_map.write(source.to_h)
35
+ def call(result)
36
+ key_map.write(result.to_h)
37
37
  end
38
38
  alias_method :[], :call
39
39
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/equalizer'
4
- require 'dry/core/cache'
5
- require 'dry/schema/constants'
6
- require 'dry/schema/key'
3
+ require "dry/equalizer"
4
+ require "dry/core/cache"
5
+ require "dry/schema/constants"
6
+ require "dry/schema/key"
7
7
 
8
8
  module Dry
9
9
  module Schema
@@ -100,6 +100,11 @@ module Dry
100
100
  self.class.new(map(&:stringified))
101
101
  end
102
102
 
103
+ # @api private
104
+ def to_dot_notation
105
+ @to_dot_notation ||= map(&:to_dot_notation).flatten
106
+ end
107
+
103
108
  # Iterate over keys
104
109
  #
105
110
  # @api public
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/initializer"
4
+ require "dry/schema/constants"
5
+
6
+ module Dry
7
+ module Schema
8
+ # @api private
9
+ class KeyValidator
10
+ extend Dry::Initializer
11
+
12
+ INDEX_REGEX = /\[\d+\]/.freeze
13
+ DIGIT_REGEX = /\A\d+\z/.freeze
14
+ BRACKETS = "[]"
15
+
16
+ # @api private
17
+ option :key_map
18
+
19
+ # @api private
20
+ def call(result)
21
+ input = result.to_h
22
+
23
+ input_paths = key_paths(input)
24
+ key_paths = key_map.to_dot_notation
25
+
26
+ input_paths.each do |path|
27
+ error_path =
28
+ if path[INDEX_REGEX]
29
+ key = path.gsub(INDEX_REGEX, BRACKETS)
30
+
31
+ unless key_paths.include?(key)
32
+ arr = path.gsub(INDEX_REGEX) { |m| ".#{m[1]}" }
33
+ arr.split(DOT).map { |s| DIGIT_REGEX.match?(s) ? s.to_i : s.to_sym }
34
+ end
35
+ elsif !key_paths.include?(path)
36
+ path
37
+ end
38
+
39
+ next unless error_path
40
+
41
+ result.add_error([:unexpected_key, [error_path, input]])
42
+ end
43
+
44
+ result
45
+ end
46
+
47
+ private
48
+
49
+ # @api private
50
+ def key_paths(hash)
51
+ hash.flat_map { |key, _|
52
+ case (value = hash[key])
53
+ when Hash
54
+ [key].product(key_paths(hash[key])).map { |keys| keys.join(DOT) }
55
+ when Array
56
+ value.flat_map.with_index { |el, idx|
57
+ key_paths(el).map { |path| ["#{key}[#{idx}]", *path].join(DOT) }
58
+ }
59
+ else
60
+ key.to_s
61
+ end
62
+ }
63
+ end
64
+ end
65
+ end
66
+ end