dry-schema 1.6.2 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad4d59e0bdc2cc46cc165f50e1ab741c07d1c00dca7467c4a6bfd647460a6d3b
4
- data.tar.gz: 295466e7e905103612280d5fdb549f116212c99c2c96b54f3561b1264174a499
3
+ metadata.gz: 305c1a8710478195cc35c3de75a1f50834c2cc044b0a6d62bb7c95845ff48db8
4
+ data.tar.gz: 62254023ae1e6ee82ecf7023e3900906a75ebda86676c59dfa24269d337af7f8
5
5
  SHA512:
6
- metadata.gz: 635185546a80867d4fc34db6d30153e43533865c3003e64bd7d7fc638cbe0cb97a7770598d3902ccf8fbd53b3b8e15253a0bb2ce4dc36c9e553fe2486491d9f1
7
- data.tar.gz: 465f1b34aab9ff0c74f690aedcfef27536fea6372df401f5193b90a2347752c58d5e2a7ed020a674ac8f0c468f809b7448cda6f303eb5dd9a48575c7cea43cde
6
+ metadata.gz: '08c894bf7682d81654ecd9034ae71e14c10e314997196cf11ee39ddaadf799aabeba33e7398aeb54961ba4b19a67e0d68cb3142ebccc182df18dd64ebca3c725'
7
+ data.tar.gz: 947264e7d824feededff8608e6e090592287503cb699d11ab31183955c963a01c1ceaeaa977c19d8add8e719a329ed1afbe72d8dd51f86603750c2502fd41a98
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  <!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
2
2
 
3
+ ## 1.7.0 2021-06-29
4
+
5
+ This release ships with a bunch of internal refactorings that should improve performance but if you see any unexpected behavior please do report issues.
6
+
7
+ ### Fixed
8
+
9
+ - Handle arrays of hashes where Array constructor coerces non-Hash input (#351 fixed via #354) (@ojab)
10
+ - Run outer schema processor steps before inner ones (issue #350 fixed via #361) (@ojab)
11
+ - Fix key validator false negatives on empty collections (see #363) (@Drenmi)
12
+ - Prevent error message YAML files from being parsed multiple times (issue #352 via #364) (@alassek)
13
+ - Using constructor types should work fine now ie `required(:foo).filled(Types::Params::Integer.constructor(&:succ))` (issue #280 fixed via #365) (@solnic)
14
+ - Handle non-Hash to Hash transformation in `before(:key_coercer)` (issue #350 fixed via #362) (@ojab)
15
+
16
+ ### Changed
17
+
18
+ - [internal] `Dry::Schema::Path` clean up and performance improvements (via #358) (@ojab)
19
+ - [internal] simplify and speed up handling of steps in nested schemas (via #360) (@ojab)
20
+
21
+ [Compare v1.6.2...v1.7.0](https://github.com/dry-rb/dry-schema/compare/v1.6.2...v1.7.0)
22
+
3
23
  ## 1.6.2 2021-04-15
4
24
 
5
25
 
data/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ <!--- this file is synced from dry-rb/template-gem project -->
1
2
  [gem]: https://rubygems.org/gems/dry-schema
2
3
  [actions]: https://github.com/dry-rb/dry-schema/actions
3
4
  [codacy]: https://www.codacy.com/gh/dry-rb/dry-schema
@@ -14,15 +15,15 @@
14
15
 
15
16
  ## Links
16
17
 
17
- * [User documentation](http://dry-rb.org/gems/dry-schema)
18
+ * [User documentation](https://dry-rb.org/gems/dry-schema)
18
19
  * [API documentation](http://rubydoc.info/gems/dry-schema)
19
20
 
20
21
  ## Supported Ruby versions
21
22
 
22
23
  This library officially supports the following Ruby versions:
23
24
 
24
- * MRI >= `2.5`
25
- * jruby >= `9.2`
25
+ * MRI `>= 2.6.0`
26
+ * ~~jruby~~ `>= 9.3` (we are waiting for [2.6 support](https://github.com/jruby/jruby/issues/6161))
26
27
 
27
28
  ## License
28
29
 
data/dry-schema.gemspec CHANGED
@@ -1,15 +1,16 @@
1
1
  # frozen_string_literal: true
2
- # this file is managed by dry-rb/devtools project
3
2
 
4
- lib = File.expand_path('lib', __dir__)
3
+ # this file is synced from dry-rb/template-gem project
4
+
5
+ lib = File.expand_path("lib", __dir__)
5
6
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
- require 'dry/schema/version'
7
+ require "dry/schema/version"
7
8
 
8
9
  Gem::Specification.new do |spec|
9
- spec.name = 'dry-schema'
10
+ spec.name = "dry-schema"
10
11
  spec.authors = ["Piotr Solnica"]
11
12
  spec.email = ["piotr.solnica@gmail.com"]
12
- spec.license = 'MIT'
13
+ spec.license = "MIT"
13
14
  spec.version = Dry::Schema::VERSION.dup
14
15
 
15
16
  spec.summary = "Coercion and validation for data structures"
@@ -17,19 +18,20 @@ Gem::Specification.new do |spec|
17
18
  dry-schema provides a DSL for defining schemas with keys and rules that should be applied to
18
19
  values. It supports coercion, input sanitization, custom types and localized error messages
19
20
  (with or without I18n gem). It's also used as the schema engine in dry-validation.
21
+
20
22
  TEXT
21
- spec.homepage = 'https://dry-rb.org/gems/dry-schema'
23
+ spec.homepage = "https://dry-rb.org/gems/dry-schema"
22
24
  spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-schema.gemspec", "lib/**/*", "config/*.yml"]
23
- spec.bindir = 'bin'
25
+ spec.bindir = "bin"
24
26
  spec.executables = []
25
- spec.require_paths = ['lib']
27
+ spec.require_paths = ["lib"]
26
28
 
27
- spec.metadata['allowed_push_host'] = 'https://rubygems.org'
28
- spec.metadata['changelog_uri'] = 'https://github.com/dry-rb/dry-schema/blob/master/CHANGELOG.md'
29
- spec.metadata['source_code_uri'] = 'https://github.com/dry-rb/dry-schema'
30
- spec.metadata['bug_tracker_uri'] = 'https://github.com/dry-rb/dry-schema/issues'
29
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
30
+ spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-schema/blob/master/CHANGELOG.md"
31
+ spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-schema"
32
+ spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-schema/issues"
31
33
 
32
- spec.required_ruby_version = ">= 2.5.0"
34
+ spec.required_ruby_version = ">= 2.6.0"
33
35
 
34
36
  # to update dependencies edit project.yml
35
37
  spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
@@ -151,7 +151,11 @@ module Dry
151
151
  # @api private
152
152
  def write(source, target)
153
153
  read(source) { |value|
154
- target[coerced_name] = value.is_a?(::Array) ? value.map { |el| member.write(el) } : value
154
+ target[coerced_name] = if value.is_a?(::Array)
155
+ value.map { |el| el.is_a?(::Hash) ? member.write(el) : el }
156
+ else
157
+ value
158
+ end
155
159
  }
156
160
  end
157
161
 
@@ -51,9 +51,14 @@ module Dry
51
51
  hash.flat_map { |key, _|
52
52
  case (value = hash[key])
53
53
  when Hash
54
+ next key.to_s if value.empty?
55
+
54
56
  [key].product(key_paths(hash[key])).map { |keys| keys.join(DOT) }
55
57
  when Array
56
- hashes_or_arrays = value.select { |e| e.is_a?(Array) || e.is_a?(Hash) }
58
+ hashes_or_arrays = value.select { |e| (e.is_a?(Array) || e.is_a?(Hash)) && !e.empty? }
59
+
60
+ next key.to_s if hashes_or_arrays.empty?
61
+
57
62
  hashes_or_arrays.flat_map.with_index { |el, idx|
58
63
  key_paths(el).map { |path| ["#{key}[#{idx}]", *path].join(DOT) }
59
64
  }
@@ -11,9 +11,14 @@ module Dry
11
11
  # @api private
12
12
  class Value < DSL
13
13
  # @api private
14
- def call(*predicates, **opts, &block)
14
+ def call(*args, **opts, &block)
15
+ types, predicates = args.partition { |arg| arg.is_a?(Dry::Types::Type) }
16
+
17
+ constructor = types.select { |type| type.is_a?(Dry::Types::Constructor) }.reduce(:>>)
15
18
  schema = predicates.detect { |predicate| predicate.is_a?(Processor) }
16
19
 
20
+ schema_dsl.set_type(name, constructor) if constructor
21
+
17
22
  type_spec = opts[:type_spec]
18
23
 
19
24
  if schema
@@ -71,6 +71,11 @@ module Dry
71
71
  @cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
72
72
  end
73
73
 
74
+ # @api private
75
+ def self.source_cache
76
+ @source_cache ||= Concurrent::Map.new
77
+ end
78
+
74
79
  # @api private
75
80
  def initialize(data: EMPTY_HASH, config: nil)
76
81
  super()
@@ -179,7 +184,9 @@ module Dry
179
184
 
180
185
  # @api private
181
186
  def load_translations(path)
182
- data = self.class.flat_hash(YAML.load_file(path))
187
+ data = self.class.source_cache.fetch_or_store(path) do
188
+ self.class.flat_hash(YAML.load_file(path)).freeze
189
+ end
183
190
 
184
191
  return data unless custom_top_namespace?(path)
185
192
 
@@ -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
@@ -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
@@ -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,7 +182,7 @@ 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
188
  if RUBY_VERSION >= "2.7"
@@ -182,15 +200,6 @@ module Dry
182
200
  def add_error(node)
183
201
  result_ast << node
184
202
  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
203
  end
195
204
  end
196
205
  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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module Schema
5
- VERSION = "1.6.2"
5
+ VERSION = "1.7.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.7.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: 2021-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -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: []
@@ -245,7 +246,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
245
246
  requirements:
246
247
  - - ">="
247
248
  - !ruby/object:Gem::Version
248
- version: 2.5.0
249
+ version: 2.6.0
249
250
  required_rubygems_version: !ruby/object:Gem::Requirement
250
251
  requirements:
251
252
  - - ">="