dry-schema 1.14.1 → 1.15.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.
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015-2023 dry-rb team
3
+ Copyright (c) 2015-2026 Hanakai team
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
data/README.md CHANGED
@@ -1,22 +1,17 @@
1
- <!--- this file is synced from dry-rb/template-gem project -->
2
- [gem]: https://rubygems.org/gems/dry-schema
1
+ <!--- This file is synced from hanakai-rb/repo-sync -->
2
+
3
+ [rubygem]: https://rubygems.org/gems/dry-schema
3
4
  [actions]: https://github.com/dry-rb/dry-schema/actions
4
5
 
5
- # dry-schema [![Gem Version](https://badge.fury.io/rb/dry-schema.svg)][gem] [![CI Status](https://github.com/dry-rb/dry-schema/workflows/CI/badge.svg)][actions]
6
+ # dry-schema [![Gem Version](https://badge.fury.io/rb/dry-schema.svg)][rubygem] [![CI Status](https://github.com/dry-rb/dry-schema/workflows/CI/badge.svg)][actions]
6
7
 
7
8
  ## Links
8
9
 
9
- * [User documentation](https://dry-rb.org/gems/dry-schema)
10
- * [API documentation](http://rubydoc.info/gems/dry-schema)
11
- * [Forum](https://discourse.dry-rb.org)
12
-
13
- ## Supported Ruby versions
14
-
15
- This library officially supports the following Ruby versions:
16
-
17
- * MRI `>= 3.1`
18
- * jruby `>= 9.4` (not tested on CI)
10
+ - [User documentation](https://dry-rb.org/gems/dry-schema)
11
+ - [API documentation](http://rubydoc.info/gems/dry-schema)
12
+ - [Forum](https://discourse.dry-rb.org)
19
13
 
20
14
  ## License
21
15
 
22
16
  See `LICENSE` file.
17
+
data/config/errors.yml CHANGED
@@ -117,5 +117,11 @@ en:
117
117
 
118
118
  uuid_v5?: "is not a valid UUID"
119
119
 
120
+ uuid_v6?: "is not a valid UUID"
121
+
122
+ uuid_v7?: "is not a valid UUID"
123
+
124
+ uuid_v8?: "is not a valid UUID"
125
+
120
126
  not:
121
127
  empty?: "cannot be empty"
data/dry-schema.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # this file is synced from dry-rb/template-gem project
3
+ # This file is synced from hanakai-rb/repo-sync. To update it, edit repo-sync.yml.
4
4
 
5
5
  lib = File.expand_path("lib", __dir__)
6
6
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
@@ -8,8 +8,8 @@ require "dry/schema/version"
8
8
 
9
9
  Gem::Specification.new do |spec|
10
10
  spec.name = "dry-schema"
11
- spec.authors = ["Piotr Solnica"]
12
- spec.email = ["piotr.solnica@gmail.com"]
11
+ spec.authors = ["Hanakai team"]
12
+ spec.email = ["info@hanakai.org"]
13
13
  spec.license = "MIT"
14
14
  spec.version = Dry::Schema::VERSION.dup
15
15
 
@@ -18,29 +18,33 @@ Gem::Specification.new do |spec|
18
18
  dry-schema provides a DSL for defining schemas with keys and rules that should be applied to
19
19
  values. It supports coercion, input sanitization, custom types and localized error messages
20
20
  (with or without I18n gem). It's also used as the schema engine in dry-validation.
21
-
22
21
  TEXT
23
22
  spec.homepage = "https://dry-rb.org/gems/dry-schema"
24
- spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-schema.gemspec",
25
- "lib/**/*", "config/*.yml"]
23
+ spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-schema.gemspec", "lib/**/*", "config/*.yml"]
26
24
  spec.bindir = "bin"
27
25
  spec.executables = []
28
26
  spec.require_paths = ["lib"]
29
27
 
30
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
31
- spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-schema/blob/main/CHANGELOG.md"
32
- spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-schema"
33
- spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-schema/issues"
34
- spec.metadata["rubygems_mfa_required"] = "true"
35
-
36
- spec.required_ruby_version = ">= 3.1"
37
-
38
- # to update dependencies edit project.yml
39
- spec.add_dependency "concurrent-ruby", "~> 1.0"
40
- spec.add_dependency "dry-configurable", "~> 1.0", ">= 1.0.1"
41
- spec.add_dependency "dry-core", "~> 1.1"
42
- spec.add_dependency "dry-initializer", "~> 3.2"
43
- spec.add_dependency "dry-logic", "~> 1.5"
44
- spec.add_dependency "dry-types", "~> 1.8"
45
- spec.add_dependency "zeitwerk", "~> 2.6"
28
+ spec.extra_rdoc_files = ["README.md", "CHANGELOG.md", "LICENSE"]
29
+
30
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
31
+ spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-schema/blob/main/CHANGELOG.md"
32
+ spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-schema"
33
+ spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-schema/issues"
34
+ spec.metadata["funding_uri"] = "https://github.com/sponsors/hanami"
35
+
36
+ spec.required_ruby_version = ">= 3.2"
37
+
38
+ spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
39
+ spec.add_runtime_dependency "zeitwerk", "~> 2.6"
40
+ spec.add_runtime_dependency "dry-core", "~> 1.1"
41
+ spec.add_runtime_dependency "dry-configurable", "~> 1.0", ">= 1.0.1"
42
+ spec.add_runtime_dependency "dry-initializer", "~> 3.2"
43
+ spec.add_runtime_dependency "dry-logic", "~> 1.6"
44
+ spec.add_runtime_dependency "dry-types", "~> 1.8"
45
+ spec.add_development_dependency "bundler"
46
+ spec.add_development_dependency "rake"
47
+ spec.add_development_dependency "rspec"
48
+ spec.add_development_dependency "yard"
46
49
  end
50
+
@@ -46,7 +46,7 @@ module Dry
46
46
  setting :backend, default: :yaml
47
47
  setting :namespace
48
48
  setting :load_paths, default: ::Set[DEFAULT_MESSAGES_PATH], constructor: :dup.to_proc
49
- setting :top_namespace, default: DEFAULT_MESSAGES_ROOT
49
+ setting :top_namespace, default: DEFAULT_MESSAGES_ROOT, constructor: :to_s.to_proc
50
50
  setting :default_locale
51
51
  end
52
52
 
@@ -28,6 +28,14 @@ module Dry
28
28
  time?: {type: "string", format: "time"},
29
29
  min_size?: {minLength: TO_INTEGER},
30
30
  max_size?: {maxLength: TO_INTEGER},
31
+ size?: {maxLength: TO_INTEGER, minLength: TO_INTEGER},
32
+ format?: {
33
+ pattern: proc do |x|
34
+ x.to_s.delete_prefix("(?-mix:").delete_suffix(")")
35
+ end
36
+ },
37
+ true?: {},
38
+ false?: {},
31
39
  included_in?: {enum: ->(v, _) { v.to_a }},
32
40
  filled?: EMPTY_HASH,
33
41
  uri?: {format: "uri"},
@@ -46,6 +54,15 @@ module Dry
46
54
  uuid_v5?: {
47
55
  pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
48
56
  },
57
+ uuid_v6?: {
58
+ pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-6[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
59
+ },
60
+ uuid_v7?: {
61
+ pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-7[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
62
+ },
63
+ uuid_v8?: {
64
+ pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-8[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
65
+ },
49
66
  gt?: {exclusiveMinimum: IDENTITY},
50
67
  gteq?: {minimum: IDENTITY},
51
68
  lt?: {exclusiveMaximum: IDENTITY},
@@ -163,18 +180,64 @@ module Dry
163
180
  name, rest = node
164
181
 
165
182
  if name.equal?(:key?)
166
- prop_name = rest[0][1]
167
- keys[prop_name] = {}
183
+ handle_key_predicate(rest)
168
184
  else
169
- target = keys[opts[:key]]
170
- type_opts = fetch_type_opts_for_predicate(name, rest, target)
171
-
172
- if target[:type]&.include?("array")
173
- target[:items] ||= {}
174
- merge_opts!(target[:items], type_opts)
175
- else
176
- merge_opts!(target, type_opts)
177
- end
185
+ handle_value_predicate(name, rest, opts)
186
+ end
187
+ end
188
+
189
+ # @api private
190
+ def handle_key_predicate(rest)
191
+ prop_name = rest[0][1]
192
+ keys[prop_name] = {}
193
+ end
194
+
195
+ # @api private
196
+ def handle_value_predicate(name, rest, opts)
197
+ target = keys[opts[:key]]
198
+ type_opts = fetch_type_opts_for_predicate(name, rest, target)
199
+
200
+ if array_with_size_predicate?(target, name, opts)
201
+ apply_array_size_constraint(target, name, rest)
202
+ elsif target[:type]&.include?("array")
203
+ apply_array_item_constraint(target, type_opts)
204
+ else
205
+ merge_opts!(target, type_opts)
206
+ end
207
+ end
208
+
209
+ # @api private
210
+ def array_with_size_predicate?(target, name, opts)
211
+ target[:type]&.include?("array") && array_size_predicate?(name) && !opts[:member]
212
+ end
213
+
214
+ # @api private
215
+ def apply_array_size_constraint(target, name, rest)
216
+ array_type_opts = convert_array_size_predicate(name, rest)
217
+ merge_opts!(target, array_type_opts)
218
+ end
219
+
220
+ # @api private
221
+ def apply_array_item_constraint(target, type_opts)
222
+ target[:items] ||= {}
223
+ merge_opts!(target[:items], type_opts)
224
+ end
225
+
226
+ # @api private
227
+ def array_size_predicate?(name)
228
+ name == :min_size? || name == :max_size?
229
+ end
230
+
231
+ # @api private
232
+ def convert_array_size_predicate(name, rest)
233
+ value = rest[0][1].to_i
234
+ case name
235
+ when :min_size?
236
+ {minItems: value}
237
+ when :max_size?
238
+ {maxItems: value}
239
+ else
240
+ {}
178
241
  end
179
242
  end
180
243
 
@@ -27,7 +27,8 @@ module Dry
27
27
  "a struct class (#{name.inspect} => #{args[0]})"
28
28
  end
29
29
 
30
- schema = struct_compiler.(args[0])
30
+ struct_class = extract_struct_class(args[0])
31
+ schema = struct_compiler.(struct_class)
31
32
 
32
33
  super(schema, *args.drop(1))
33
34
  type(schema_dsl.types[name].constructor(schema))
@@ -39,7 +40,18 @@ module Dry
39
40
  private
40
41
 
41
42
  def struct?(type)
42
- type.is_a?(::Class) && type <= ::Dry::Struct
43
+ (type.is_a?(::Class) && type <= ::Dry::Struct) ||
44
+ (type.is_a?(::Dry::Types::Constructor) && type.primitive <= ::Dry::Struct)
45
+ end
46
+
47
+ def extract_struct_class(type)
48
+ if type.is_a?(::Class) && type <= ::Dry::Struct
49
+ type
50
+ elsif type.is_a?(::Dry::Types::Constructor) && type.primitive <= ::Dry::Struct
51
+ type.primitive
52
+ else
53
+ type
54
+ end
43
55
  end
44
56
  })
45
57
  end
@@ -9,6 +9,7 @@ module Dry
9
9
  class KeyValidator
10
10
  extend ::Dry::Initializer
11
11
 
12
+ MULTI_INDEX_REGEX = /(\[\d+\])+/
12
13
  INDEX_REGEX = /\[\d+\]/
13
14
  DIGIT_REGEX = /\A\d+\z/
14
15
  BRACKETS = "[]"
@@ -38,10 +39,10 @@ module Dry
38
39
 
39
40
  # @api private
40
41
  def validate_path(key_paths, path)
41
- if path[INDEX_REGEX]
42
- key = path.gsub(INDEX_REGEX, BRACKETS)
42
+ if path[MULTI_INDEX_REGEX]
43
+ key = path.gsub(MULTI_INDEX_REGEX, BRACKETS)
43
44
  if none_key_paths_match?(key_paths, key)
44
- arr = path.gsub(INDEX_REGEX) { ".#{_1[1]}" }
45
+ arr = path.gsub(INDEX_REGEX) { ".#{_1[1...-1]}" }
45
46
  arr.split(DOT).map { DIGIT_REGEX.match?(_1) ? Integer(_1, 10) : _1.to_sym }
46
47
  end
47
48
  elsif none_key_paths_match?(key_paths, path)
@@ -83,9 +84,7 @@ module Dry
83
84
  if hashes_or_arrays.empty?
84
85
  [key.to_s]
85
86
  else
86
- hashes_or_arrays.flat_map.with_index { |el, idx|
87
- key_paths(el).map { ["#{key}[#{idx}]", *_1].join(DOT) }
88
- }
87
+ traverse_array(value).map { |sub_path| [key, sub_path].join("") }
89
88
  end
90
89
  else
91
90
  key.to_s
@@ -99,6 +98,20 @@ module Dry
99
98
  (x.is_a?(::Array) || x.is_a?(::Hash)) && !x.empty?
100
99
  }
101
100
  end
101
+
102
+ # @api private
103
+ def traverse_array(arr)
104
+ arr.each_with_index.flat_map do |el, idx|
105
+ case el
106
+ when ::Hash
107
+ key_paths(el).map { ["[#{idx}]", *_1].join(DOT) }
108
+ when ::Array
109
+ traverse_array(el).map { ["[#{idx}]", *_1].join("") }
110
+ else
111
+ []
112
+ end
113
+ end
114
+ end
102
115
  end
103
116
  end
104
117
  end
@@ -234,6 +234,10 @@ module Dry
234
234
  type_rule = [type_spec.left, type_spec.right].map { |ts|
235
235
  new(klass: Core, chain: false).value(ts)
236
236
  }.reduce(:|)
237
+ elsif type_spec.is_a?(Dry::Types::Intersection) && set_type
238
+ type_rule = [type_spec.left, type_spec.right].map { |ts|
239
+ new(klass: Core, chain: false).value(ts)
240
+ }.reduce(:&)
237
241
  else
238
242
  type_predicates = predicate_inferrer[resolved_type]
239
243
 
@@ -81,7 +81,7 @@ module Dry
81
81
  # @api private
82
82
  def to_or(root)
83
83
  clone = dup
84
- clone.instance_variable_set("@path", path - root.to_a)
84
+ clone.instance_variable_set("@path", remove_prefix(path, prefix: root.to_a))
85
85
  clone.instance_variable_set("@_path", nil)
86
86
  clone
87
87
  end
@@ -104,6 +104,13 @@ module Dry
104
104
  def _path
105
105
  @_path ||= Path[path]
106
106
  end
107
+
108
+ private
109
+
110
+ def remove_prefix(array, prefix:)
111
+ has_prefix = array[0, prefix.length] == prefix
112
+ has_prefix ? array[prefix.length..] : array.dup
113
+ end
107
114
  end
108
115
  end
109
116
  end
@@ -224,9 +224,9 @@ module Dry
224
224
  # @api private
225
225
  def append_mapped_size_tokens(tokens)
226
226
  # this is a temporary fix for the inconsistency in the "size" errors arguments
227
- mapped_hash = tokens.each_with_object({}) { |(k, v), h|
228
- h[k.to_s.gsub("size", "num").to_sym] = v
229
- }
227
+ mapped_hash = tokens.transform_keys do |k|
228
+ k.to_s.gsub("size", "num").to_sym
229
+ end
230
230
  tokens.merge(mapped_hash)
231
231
  end
232
232
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require "concurrent/map"
5
4
 
6
5
  require "dry/schema/constants"
@@ -32,7 +32,8 @@ module Dry
32
32
  def get(key, options = EMPTY_HASH)
33
33
  return unless key
34
34
 
35
- result = t.(key, locale: options.fetch(:locale, default_locale))
35
+ options[:locale] ||= default_locale
36
+ result = t.(key, **options)
36
37
 
37
38
  if result.is_a?(Hash)
38
39
  text = result[:text]
@@ -112,7 +113,10 @@ module Dry
112
113
 
113
114
  resolved_key = key?(text_key, opts) ? text_key : key
114
115
 
115
- t.(resolved_key, **opts)
116
+ result = t.(resolved_key, **opts)
117
+ return result unless result.is_a?(Hash)
118
+
119
+ result[:text]
116
120
  end
117
121
 
118
122
  private
@@ -4,7 +4,16 @@ module Dry
4
4
  module Schema
5
5
  # @api private
6
6
  class PredicateInferrer < ::Dry::Types::PredicateInferrer
7
- Compiler = ::Class.new(superclass::Compiler)
7
+ Compiler = ::Class.new(superclass::Compiler) do
8
+ # @api private
9
+ def visit_intersection(node)
10
+ left_node, right_node, = node
11
+ left = visit(left_node)
12
+ right = visit(right_node)
13
+
14
+ [left, right].flatten.compact
15
+ end
16
+ end
8
17
 
9
18
  def initialize(registry = PredicateRegistry.new)
10
19
  super
@@ -4,7 +4,13 @@ module Dry
4
4
  module Schema
5
5
  # @api private
6
6
  class PrimitiveInferrer < ::Dry::Types::PrimitiveInferrer
7
- Compiler = ::Class.new(superclass::Compiler)
7
+ Compiler = ::Class.new(superclass::Compiler) do
8
+ # @api private
9
+ def visit_intersection(node)
10
+ left, right = node
11
+ [visit(left), visit(right)].flatten(1)
12
+ end
13
+ end
8
14
 
9
15
  def initialize
10
16
  super
@@ -7,6 +7,7 @@ module Dry
7
7
  # @api private
8
8
  class Trace < ::BasicObject
9
9
  INVALID_PREDICATES = %i[key?].freeze
10
+ RESPOND_TO_MISSING_METHOD = ::Kernel.instance_method(:respond_to_missing?)
10
11
 
11
12
  include ::Dry::Equalizer(:compiler, :captures)
12
13
 
@@ -86,12 +87,13 @@ module Dry
86
87
  end
87
88
 
88
89
  def respond_to_missing?(meth, include_private = false)
89
- super || (meth.to_s.end_with?(QUESTION_MARK) && compuiler.support?(meth))
90
+ RESPOND_TO_MISSING_METHOD.bind_call(self, meth, include_private) ||
91
+ (meth.to_s.end_with?(QUESTION_MARK) && compiler.support?(meth))
90
92
  end
91
93
 
92
94
  # @api private
93
95
  def method_missing(meth, *args, &block)
94
- if meth.to_s.end_with?(QUESTION_MARK)
96
+ if !meth.equal?(:respond_to_missing?) && meth.to_s.end_with?(QUESTION_MARK)
95
97
  if ::Dry::Schema::Trace::INVALID_PREDICATES.include?(meth)
96
98
  ::Kernel.raise InvalidSchemaError, "#{meth} predicate cannot be used in this context"
97
99
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module Schema
5
- VERSION = "1.14.1"
5
+ VERSION = "1.15.0"
6
6
  end
7
7
  end