dry-schema 1.4.3 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -64,6 +64,11 @@ module Dry
64
64
  new(name: name.to_s)
65
65
  end
66
66
 
67
+ # @api private
68
+ def to_dot_notation
69
+ [name.to_s]
70
+ end
71
+
67
72
  # @api private
68
73
  def new(**new_opts)
69
74
  self.class.new(id, name: name, coercer: coercer, **new_opts)
@@ -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
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/macros/array'
4
- require 'dry/schema/macros/each'
5
- require 'dry/schema/macros/filled'
6
- require 'dry/schema/macros/schema'
7
- require 'dry/schema/macros/hash'
8
- require 'dry/schema/macros/maybe'
9
- require 'dry/schema/macros/optional'
10
- require 'dry/schema/macros/required'
3
+ require "dry/schema/macros/array"
4
+ require "dry/schema/macros/each"
5
+ require "dry/schema/macros/filled"
6
+ require "dry/schema/macros/schema"
7
+ require "dry/schema/macros/hash"
8
+ require "dry/schema/macros/maybe"
9
+ require "dry/schema/macros/optional"
10
+ require "dry/schema/macros/required"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/macros/dsl'
3
+ require "dry/schema/macros/dsl"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -13,16 +13,29 @@ module Dry
13
13
  def value(*args, **opts, &block)
14
14
  type(:array)
15
15
 
16
- extract_type_spec(*args, set_type: false) do |*predicates, type_spec:|
16
+ extract_type_spec(*args, set_type: false) do |*predicates, type_spec:, type_rule:|
17
17
  type(schema_dsl.array[type_spec]) if type_spec
18
18
 
19
19
  is_hash_block = type_spec.equal?(:hash)
20
20
 
21
21
  if predicates.any? || opts.any? || !is_hash_block
22
- super(*predicates, type_spec: type_spec, **opts, &(is_hash_block ? nil : block))
22
+ super(
23
+ *predicates, type_spec: type_spec, type_rule: type_rule, **opts,
24
+ &(is_hash_block ? nil : block)
25
+ )
23
26
  end
24
27
 
25
- hash(&block) if is_hash_block
28
+ is_op = args.size.equal?(2) && args[1].is_a?(Logic::Operations::Abstract)
29
+
30
+ if is_hash_block && !is_op
31
+ hash(&block)
32
+ elsif is_op
33
+ hash = Value.new(schema_dsl: schema_dsl.new, name: name).hash(args[1])
34
+
35
+ trace.captures.concat(hash.trace.captures)
36
+
37
+ type(schema_dsl.types[name].of(hash.schema_dsl.types[name]))
38
+ end
26
39
  end
27
40
 
28
41
  self