dry-schema 1.6.2 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/README.md +4 -3
  4. data/dry-schema.gemspec +16 -14
  5. data/lib/dry/schema/compiler.rb +1 -1
  6. data/lib/dry/schema/config.rb +9 -9
  7. data/lib/dry/schema/dsl.rb +7 -4
  8. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +9 -4
  9. data/lib/dry/schema/extensions/hints.rb +11 -9
  10. data/lib/dry/schema/extensions/info/schema_compiler.rb +10 -1
  11. data/lib/dry/schema/extensions/json_schema/schema_compiler.rb +232 -0
  12. data/lib/dry/schema/extensions/json_schema.rb +29 -0
  13. data/lib/dry/schema/extensions/struct.rb +1 -1
  14. data/lib/dry/schema/extensions.rb +4 -0
  15. data/lib/dry/schema/key.rb +75 -70
  16. data/lib/dry/schema/key_coercer.rb +2 -2
  17. data/lib/dry/schema/key_validator.rb +46 -20
  18. data/lib/dry/schema/macros/array.rb +4 -0
  19. data/lib/dry/schema/macros/core.rb +1 -1
  20. data/lib/dry/schema/macros/dsl.rb +17 -15
  21. data/lib/dry/schema/macros/hash.rb +1 -1
  22. data/lib/dry/schema/macros/key.rb +2 -2
  23. data/lib/dry/schema/macros/schema.rb +2 -0
  24. data/lib/dry/schema/macros/value.rb +13 -1
  25. data/lib/dry/schema/message/or/multi_path.rb +7 -5
  26. data/lib/dry/schema/message_compiler.rb +13 -10
  27. data/lib/dry/schema/messages/abstract.rb +9 -9
  28. data/lib/dry/schema/messages/i18n.rb +98 -96
  29. data/lib/dry/schema/messages/namespaced.rb +1 -0
  30. data/lib/dry/schema/messages/yaml.rb +165 -151
  31. data/lib/dry/schema/path.rb +10 -60
  32. data/lib/dry/schema/predicate.rb +2 -2
  33. data/lib/dry/schema/predicate_inferrer.rb +2 -0
  34. data/lib/dry/schema/primitive_inferrer.rb +2 -0
  35. data/lib/dry/schema/processor.rb +6 -6
  36. data/lib/dry/schema/processor_steps.rb +7 -3
  37. data/lib/dry/schema/result.rb +38 -31
  38. data/lib/dry/schema/step.rb +14 -33
  39. data/lib/dry/schema/trace.rb +5 -1
  40. data/lib/dry/schema/type_registry.rb +1 -2
  41. data/lib/dry/schema/version.rb +1 -1
  42. metadata +11 -8
@@ -32,10 +32,10 @@ module Dry
32
32
  new(spec.split(DOT).map(&:to_sym))
33
33
  when Hash
34
34
  new(keys_from_hash(spec))
35
- when Path
35
+ when self
36
36
  spec
37
37
  else
38
- raise ArgumentError, "+spec+ must be either a Symbol, Array, Hash or a Path"
38
+ raise ArgumentError, "+spec+ must be either a Symbol, Array, Hash or a #{name}"
39
39
  end
40
40
  end
41
41
 
@@ -60,24 +60,9 @@ module Dry
60
60
 
61
61
  # @api private
62
62
  def to_h(value = EMPTY_ARRAY.dup)
63
- curr_idx = 0
64
- last_idx = keys.size - 1
65
- hash = EMPTY_HASH.dup
66
- node = hash
67
-
68
- while curr_idx <= last_idx
69
- node =
70
- node[keys[curr_idx]] =
71
- if curr_idx == last_idx
72
- value.is_a?(Array) ? value : [value]
73
- else
74
- EMPTY_HASH.dup
75
- end
76
-
77
- curr_idx += 1
78
- end
63
+ value = [value] unless value.is_a?(Array)
79
64
 
80
- hash
65
+ keys.reverse_each.reduce(value) { |result, key| {key => result} }
81
66
  end
82
67
 
83
68
  # @api private
@@ -85,59 +70,27 @@ module Dry
85
70
  keys.each(&block)
86
71
  end
87
72
 
88
- # @api private
89
- def index(key)
90
- keys.index(key)
91
- end
92
-
93
- def without_index
94
- self.class.new(to_a[0..-2])
95
- end
96
-
97
73
  # @api private
98
74
  def include?(other)
99
- if !same_root?(other)
100
- false
101
- elsif index?
102
- if other.index?
103
- last.equal?(other.last)
104
- else
105
- without_index.include?(other)
106
- end
107
- elsif other.index? && key_matches(other, :select).size < 2
108
- false
109
- else
110
- self >= other && !other.key_matches(self).include?(nil)
111
- end
75
+ keys[0, other.keys.length].eql?(other.keys)
112
76
  end
113
77
 
114
78
  # @api private
115
79
  def <=>(other)
116
- raise ArgumentError, "Can't compare paths from different branches" unless same_root?(other)
80
+ return keys.length <=> other.keys.length if include?(other) || other.include?(self)
117
81
 
118
- return 0 if keys.eql?(other.keys)
82
+ first_uncommon_index = (self & other).keys.length
119
83
 
120
- res = key_matches(other).compact.reject { |value| value.equal?(false) }
121
-
122
- res.size < count ? 1 : -1
84
+ keys[first_uncommon_index] <=> other.keys[first_uncommon_index]
123
85
  end
124
86
 
125
87
  # @api private
126
88
  def &(other)
127
- unless same_root?(other)
128
- raise ArgumentError, "#{other.inspect} doesn't have the same root #{inspect}"
129
- end
130
-
131
89
  self.class.new(
132
- key_matches(other, :select).compact.reject { |value| value.equal?(false) }
90
+ keys.take_while.with_index { |key, index| other.keys[index].eql?(key) }
133
91
  )
134
92
  end
135
93
 
136
- # @api private
137
- def key_matches(other, meth = :map)
138
- public_send(meth) { |key| (idx = other.index(key)) && keys[idx].equal?(key) }
139
- end
140
-
141
94
  # @api private
142
95
  def last
143
96
  keys.last
@@ -148,10 +101,7 @@ module Dry
148
101
  root.equal?(other.root)
149
102
  end
150
103
 
151
- # @api private
152
- def index?
153
- last.is_a?(Integer)
154
- end
104
+ EMPTY = new(EMPTY_ARRAY).freeze
155
105
  end
156
106
  end
157
107
  end
@@ -28,8 +28,8 @@ module Dry
28
28
  # @return [Array]
29
29
  #
30
30
  # @api private
31
- def to_ast(*args)
32
- [:not, predicate.to_ast(*args)]
31
+ def to_ast(...)
32
+ [:not, predicate.to_ast(...)]
33
33
  end
34
34
  alias_method :ast, :to_ast
35
35
  end
@@ -9,6 +9,8 @@ module Dry
9
9
  Compiler = ::Class.new(superclass::Compiler)
10
10
 
11
11
  def initialize(registry = PredicateRegistry.new)
12
+ super
13
+
12
14
  @compiler = Compiler.new(registry)
13
15
  end
14
16
  end
@@ -9,6 +9,8 @@ module Dry
9
9
  Compiler = ::Class.new(superclass::Compiler)
10
10
 
11
11
  def initialize
12
+ super
13
+
12
14
  @compiler = Compiler.new
13
15
  end
14
16
  end
@@ -28,8 +28,8 @@ module Dry
28
28
  include Dry::Logic::Operators
29
29
 
30
30
  setting :key_map_type
31
- setting :type_registry_namespace, :strict
32
- setting :filter_empty_string, false
31
+ setting :type_registry_namespace, default: :strict
32
+ setting :filter_empty_string, default: false
33
33
 
34
34
  option :steps, default: -> { ProcessorSteps.new }
35
35
 
@@ -53,7 +53,7 @@ module Dry
53
53
  # @api public
54
54
  def define(&block)
55
55
  @definition ||= DSL.new(
56
- processor_type: self, parent: superclass.definition, **config, &block
56
+ processor_type: self, parent: superclass.definition, **config.to_h, &block
57
57
  )
58
58
  self
59
59
  end
@@ -84,14 +84,14 @@ module Dry
84
84
  #
85
85
  # @api public
86
86
  def call(input)
87
- Result.new(input, input: input, message_compiler: message_compiler) do |result|
87
+ Result.new(input.dup, message_compiler: message_compiler) do |result|
88
88
  steps.call(result)
89
89
  end
90
90
  end
91
91
  alias_method :[], :call
92
92
 
93
93
  # @api public
94
- def xor(other)
94
+ def xor(_other)
95
95
  raise NotImplementedError, "composing schemas using `xor` operator is not supported yet"
96
96
  end
97
97
  alias_method :^, :xor
@@ -123,7 +123,7 @@ module Dry
123
123
  # @api public
124
124
  def inspect
125
125
  <<~STR.strip
126
- #<#{self.class.name} keys=#{key_map.map(&:dump)} rules=#{rules.map { |k, v| [k, v.to_s] }.to_h}>
126
+ #<#{self.class.name} keys=#{key_map.map(&:dump)} rules=#{rules.transform_values(&:to_s)}>
127
127
  STR
128
128
  end
129
129
 
@@ -87,6 +87,7 @@ module Dry
87
87
  def after(name, &block)
88
88
  after_steps[name] ||= EMPTY_ARRAY.dup
89
89
  after_steps[name] << Step.new(type: :after, name: name, executor: block)
90
+ after_steps[name].sort_by!(&:path)
90
91
  self
91
92
  end
92
93
 
@@ -100,6 +101,7 @@ module Dry
100
101
  def before(name, &block)
101
102
  before_steps[name] ||= EMPTY_ARRAY.dup
102
103
  before_steps[name] << Step.new(type: :before, name: name, executor: block)
104
+ before_steps[name].sort_by!(&:path)
103
105
  self
104
106
  end
105
107
 
@@ -120,18 +122,20 @@ module Dry
120
122
  # @api private
121
123
  def merge_callbacks(left, right)
122
124
  left.merge(right) do |_key, oldval, newval|
123
- oldval + newval
125
+ (oldval + newval).sort_by(&:path)
124
126
  end
125
127
  end
126
128
 
127
129
  # @api private
128
130
  def import_callbacks(path, other)
129
131
  other.before_steps.each do |name, steps|
130
- (before_steps[name] ||= []).concat(steps.map { |step| step.scoped(path) })
132
+ before_steps[name] ||= []
133
+ before_steps[name].concat(steps.map { |step| step.scoped(path) }).sort_by!(&:path)
131
134
  end
132
135
 
133
136
  other.after_steps.each do |name, steps|
134
- (after_steps[name] ||= []).concat(steps.map { |step| step.scoped(path) })
137
+ after_steps[name] ||= []
138
+ after_steps[name].concat(steps.map { |step| step.scoped(path) }).sort_by!(&:path)
135
139
  end
136
140
  end
137
141
  end
@@ -15,24 +15,21 @@ module Dry
15
15
  class Result
16
16
  include Dry::Equalizer(:output, :errors)
17
17
 
18
- extend Dry::Initializer
18
+ extend Dry::Initializer[undefined: false]
19
19
 
20
20
  # @api private
21
- param :output
21
+ param :output, reader: false
22
22
 
23
- # Dump result to a hash returning processed and validated data
23
+ # A list of failure ASTs produced by rule result objects
24
24
  #
25
- # @return [Hash]
26
- alias_method :to_h, :output
27
-
28
25
  # @api private
29
- param :results, default: -> { EMPTY_ARRAY.dup }
26
+ option :result_ast, default: -> { EMPTY_ARRAY.dup }
30
27
 
31
28
  # @api private
32
29
  option :message_compiler
33
30
 
34
31
  # @api private
35
- option :parent, default: -> { nil }
32
+ option :path, optional: true, reader: false
36
33
 
37
34
  # @api private
38
35
  def self.new(*, **)
@@ -48,8 +45,8 @@ module Dry
48
45
  # @return [Result]
49
46
  #
50
47
  # @api private
51
- def at(path, &block)
52
- new(Path[path].reduce(output) { |a, e| a[e] }, parent: self, &block)
48
+ def at(at_path, &block)
49
+ new(@output, path: Path.new([*path, *Path[at_path]]), &block)
53
50
  end
54
51
 
55
52
  # @api private
@@ -57,7 +54,7 @@ module Dry
57
54
  self.class.new(
58
55
  output,
59
56
  message_compiler: message_compiler,
60
- results: results,
57
+ result_ast: result_ast,
61
58
  **opts,
62
59
  &block
63
60
  )
@@ -70,14 +67,35 @@ module Dry
70
67
  end
71
68
 
72
69
  # @api private
73
- def replace(hash)
74
- @output = hash
70
+ def path
71
+ @path || Path::EMPTY
72
+ end
73
+
74
+ # Dump result to a hash returning processed and validated data
75
+ #
76
+ # @return [Hash]
77
+ def output
78
+ path.equal?(Path::EMPTY) ? @output : @output.dig(*path)
79
+ end
80
+ alias_method :to_h, :output
81
+
82
+ # @api private
83
+ def replace(value)
84
+ if value.is_a?(output.class)
85
+ output.replace(value)
86
+ elsif path.equal?(Path::EMPTY)
87
+ @output = value
88
+ else
89
+ value_holder = path.keys.length > 1 ? @output.dig(*path.to_a[0..-2]) : @output
90
+
91
+ value_holder[path.last] = value
92
+ end
93
+
75
94
  self
76
95
  end
77
96
 
78
97
  # @api private
79
98
  def concat(other)
80
- results.concat(other)
81
99
  result_ast.concat(other.map(&:to_ast))
82
100
  self
83
101
  end
@@ -164,16 +182,14 @@ module Dry
164
182
  #
165
183
  # @api public
166
184
  def inspect
167
- "#<#{self.class}#{to_h.inspect} errors=#{errors.to_h.inspect}>"
185
+ "#<#{self.class}#{to_h.inspect} errors=#{errors.to_h.inspect} path=#{path.keys.inspect}>"
168
186
  end
169
187
 
170
- if RUBY_VERSION >= "2.7"
171
- # Pattern matching support
172
- #
173
- # @api private
174
- def deconstruct_keys(_)
175
- output
176
- end
188
+ # Pattern matching support
189
+ #
190
+ # @api private
191
+ def deconstruct_keys(_)
192
+ output
177
193
  end
178
194
 
179
195
  # Add a new error AST node
@@ -182,15 +198,6 @@ module Dry
182
198
  def add_error(node)
183
199
  result_ast << node
184
200
  end
185
-
186
- private
187
-
188
- # A list of failure ASTs produced by rule result objects
189
- #
190
- # @api private
191
- def result_ast
192
- @result_ast ||= results.map(&:to_ast)
193
- end
194
201
  end
195
202
  end
196
203
  end
@@ -17,53 +17,34 @@ module Dry
17
17
  attr_reader :executor
18
18
 
19
19
  # @api private
20
- class Scoped
21
- # @api private
22
- attr_reader :path
23
-
24
- # @api private
25
- attr_reader :step
26
-
27
- # @api private
28
- def initialize(path, step)
29
- @path = Path[path]
30
- @step = step
31
- end
32
-
33
- # @api private
34
- def scoped(new_path)
35
- self.class.new(Path[[*new_path, *path]], step)
36
- end
37
-
38
- # @api private
39
- def call(result)
40
- result.at(path) do |scoped_result|
41
- output = step.(scoped_result).to_h
42
- target = Array(path)[0..-2].reduce(result) { |a, e| a[e] }
43
-
44
- target.update(path.last => output)
45
- end
46
- end
47
- end
20
+ attr_reader :path
48
21
 
49
22
  # @api private
50
- def initialize(type:, name:, executor:)
23
+ def initialize(type:, name:, executor:, path: Path::EMPTY)
51
24
  @type = type
52
25
  @name = name
53
26
  @executor = executor
27
+ @path = path
54
28
  validate_name(name)
55
29
  end
56
30
 
57
31
  # @api private
58
32
  def call(result)
59
- output = executor.(result)
60
- result.replace(output) if output.is_a?(Hash)
33
+ scoped_result = path.equal?(Path::EMPTY) ? result : result.at(path)
34
+
35
+ output = executor.(scoped_result)
36
+ scoped_result.replace(output) if output.is_a?(Hash)
61
37
  output
62
38
  end
63
39
 
64
40
  # @api private
65
- def scoped(path)
66
- Scoped.new(path, self)
41
+ def scoped(parent_path)
42
+ self.class.new(
43
+ type: type,
44
+ name: name,
45
+ executor: executor,
46
+ path: Path.new([*parent_path, *path])
47
+ )
67
48
  end
68
49
 
69
50
  private
@@ -89,6 +89,10 @@ module Dry
89
89
  captures.map(&:to_ast).map(&compiler.method(:visit)).reduce(:and)
90
90
  end
91
91
 
92
+ def respond_to_missing?(meth, include_private = false)
93
+ super || (meth.to_s.end_with?(QUESTION_MARK) && compuiler.support?(meth))
94
+ end
95
+
92
96
  # @api private
93
97
  def method_missing(meth, *args, &block)
94
98
  if meth.to_s.end_with?(QUESTION_MARK)
@@ -96,7 +100,7 @@ module Dry
96
100
  ::Kernel.raise InvalidSchemaError, "#{meth} predicate cannot be used in this context"
97
101
  end
98
102
 
99
- unless compiler.supports?(meth)
103
+ unless compiler.support?(meth)
100
104
  ::Kernel.raise ::ArgumentError, "#{meth} predicate is not defined"
101
105
  end
102
106
 
@@ -37,8 +37,7 @@ module Dry
37
37
  def [](name)
38
38
  key = [namespace, name].compact.join(DOT)
39
39
 
40
- type = types.registered?(key) ? types[key] : types[name.to_s]
41
- type
40
+ types.registered?(key) ? types[key] : types[name.to_s]
42
41
  end
43
42
  end
44
43
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module Schema
5
- VERSION = "1.6.2"
5
+ VERSION = "1.9.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.2
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-15 00:00:00.000000000 Z
11
+ date: 2022-02-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -30,20 +30,20 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.8'
33
+ version: '0.13'
34
34
  - - ">="
35
35
  - !ruby/object:Gem::Version
36
- version: 0.8.3
36
+ version: 0.13.0
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
40
40
  requirements:
41
41
  - - "~>"
42
42
  - !ruby/object:Gem::Version
43
- version: '0.8'
43
+ version: '0.13'
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 0.8.3
46
+ version: 0.13.0
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: dry-core
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -148,10 +148,11 @@ dependencies:
148
148
  - - ">="
149
149
  - !ruby/object:Gem::Version
150
150
  version: '0'
151
- description: |
151
+ description: |+
152
152
  dry-schema provides a DSL for defining schemas with keys and rules that should be applied to
153
153
  values. It supports coercion, input sanitization, custom types and localized error messages
154
154
  (with or without I18n gem). It's also used as the schema engine in dry-validation.
155
+
155
156
  email:
156
157
  - piotr.solnica@gmail.com
157
158
  executables: []
@@ -177,6 +178,8 @@ files:
177
178
  - lib/dry/schema/extensions/hints/result_methods.rb
178
179
  - lib/dry/schema/extensions/info.rb
179
180
  - lib/dry/schema/extensions/info/schema_compiler.rb
181
+ - lib/dry/schema/extensions/json_schema.rb
182
+ - lib/dry/schema/extensions/json_schema/schema_compiler.rb
180
183
  - lib/dry/schema/extensions/monads.rb
181
184
  - lib/dry/schema/extensions/struct.rb
182
185
  - lib/dry/schema/json.rb
@@ -245,7 +248,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
245
248
  requirements:
246
249
  - - ">="
247
250
  - !ruby/object:Gem::Version
248
- version: 2.5.0
251
+ version: 2.7.0
249
252
  required_rubygems_version: !ruby/object:Gem::Requirement
250
253
  requirements:
251
254
  - - ">="