dry-schema 1.8.0 → 1.9.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +3 -3
  4. data/dry-schema.gemspec +1 -1
  5. data/lib/dry/schema/compiler.rb +1 -1
  6. data/lib/dry/schema/dsl.rb +7 -4
  7. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +9 -4
  8. data/lib/dry/schema/extensions/hints.rb +11 -9
  9. data/lib/dry/schema/extensions/info/schema_compiler.rb +10 -1
  10. data/lib/dry/schema/extensions/json_schema/schema_compiler.rb +232 -0
  11. data/lib/dry/schema/extensions/json_schema.rb +29 -0
  12. data/lib/dry/schema/extensions/struct.rb +1 -1
  13. data/lib/dry/schema/extensions.rb +4 -0
  14. data/lib/dry/schema/key.rb +75 -74
  15. data/lib/dry/schema/key_coercer.rb +2 -2
  16. data/lib/dry/schema/key_validator.rb +44 -23
  17. data/lib/dry/schema/macros/array.rb +4 -0
  18. data/lib/dry/schema/macros/core.rb +1 -1
  19. data/lib/dry/schema/macros/dsl.rb +17 -15
  20. data/lib/dry/schema/macros/hash.rb +1 -1
  21. data/lib/dry/schema/macros/key.rb +2 -2
  22. data/lib/dry/schema/macros/schema.rb +2 -0
  23. data/lib/dry/schema/macros/value.rb +7 -0
  24. data/lib/dry/schema/message/or/multi_path.rb +7 -5
  25. data/lib/dry/schema/message_compiler.rb +13 -10
  26. data/lib/dry/schema/messages/i18n.rb +98 -96
  27. data/lib/dry/schema/messages/namespaced.rb +1 -0
  28. data/lib/dry/schema/messages/yaml.rb +165 -158
  29. data/lib/dry/schema/predicate.rb +2 -2
  30. data/lib/dry/schema/predicate_inferrer.rb +2 -0
  31. data/lib/dry/schema/primitive_inferrer.rb +2 -0
  32. data/lib/dry/schema/processor.rb +1 -1
  33. data/lib/dry/schema/result.rb +5 -7
  34. data/lib/dry/schema/trace.rb +5 -1
  35. data/lib/dry/schema/type_registry.rb +1 -2
  36. data/lib/dry/schema/version.rb +1 -1
  37. metadata +5 -3
@@ -24,17 +24,7 @@ module Dry
24
24
  key_paths = key_map.to_dot_notation
25
25
 
26
26
  input_paths.each do |path|
27
- error_path =
28
- if path[INDEX_REGEX]
29
- key = path.gsub(INDEX_REGEX, BRACKETS)
30
-
31
- if key_paths.none? { |key_path| key_path.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.none? { |key_path| key_path.include?(path) }
36
- path
37
- end
27
+ error_path = validate_path(key_paths, path)
38
28
 
39
29
  next unless error_path
40
30
 
@@ -47,26 +37,57 @@ module Dry
47
37
  private
48
38
 
49
39
  # @api private
50
- def key_paths(hash)
51
- hash.flat_map { |key, _|
52
- case (value = hash[key])
53
- when Hash
54
- next key.to_s if value.empty?
40
+ def validate_path(key_paths, path)
41
+ if path[INDEX_REGEX]
42
+ key = path.gsub(INDEX_REGEX, BRACKETS)
55
43
 
56
- [key].product(key_paths(hash[key])).map { |keys| keys.join(DOT) }
57
- when Array
58
- hashes_or_arrays = value.select { |e| (e.is_a?(Array) || e.is_a?(Hash)) && !e.empty? }
44
+ if key_paths.none? { paths_match?(key, _1) }
45
+ arr = path.gsub(INDEX_REGEX) { ".#{_1[1]}" }
46
+ arr.split(DOT).map { DIGIT_REGEX.match?(_1) ? Integer(_1, 10) : _1.to_sym }
47
+ end
48
+ elsif key_paths.none? { paths_match?(path, _1) }
49
+ path
50
+ end
51
+ end
59
52
 
60
- next key.to_s if hashes_or_arrays.empty?
53
+ # @api private
54
+ def paths_match?(input_path, key_path)
55
+ residue = key_path.sub(input_path, "")
56
+ residue.empty? || residue.start_with?(DOT, BRACKETS)
57
+ end
61
58
 
62
- hashes_or_arrays.flat_map.with_index { |el, idx|
63
- key_paths(el).map { |path| ["#{key}[#{idx}]", *path].join(DOT) }
64
- }
59
+ # @api private
60
+ def key_paths(hash)
61
+ hash.flat_map { |key, value|
62
+ case value
63
+ when ::Hash
64
+ if value.empty?
65
+ [key.to_s]
66
+ else
67
+ [key].product(key_paths(hash[key])).map { _1.join(DOT) }
68
+ end
69
+ when ::Array
70
+ hashes_or_arrays = hashes_or_arrays(value)
71
+
72
+ if hashes_or_arrays.empty?
73
+ [key.to_s]
74
+ else
75
+ hashes_or_arrays.flat_map.with_index { |el, idx|
76
+ key_paths(el).map { ["#{key}[#{idx}]", *_1].join(DOT) }
77
+ }
78
+ end
65
79
  else
66
80
  key.to_s
67
81
  end
68
82
  }
69
83
  end
84
+
85
+ # @api private
86
+ def hashes_or_arrays(xs)
87
+ xs.select { |x|
88
+ (x.is_a?(::Array) || x.is_a?(::Hash)) && !x.empty?
89
+ }
90
+ end
70
91
  end
71
92
  end
72
93
  end
@@ -10,6 +10,8 @@ module Dry
10
10
  # @api private
11
11
  class Array < DSL
12
12
  # @api private
13
+ # rubocop: disable Metrics/PerceivedComplexity
14
+ # rubocop: disable Metrics/AbcSize
13
15
  def value(*args, **opts, &block)
14
16
  type(:array)
15
17
 
@@ -40,6 +42,8 @@ module Dry
40
42
 
41
43
  self
42
44
  end
45
+ # rubocop: enable Metrics/AbcSize
46
+ # rubocop: enable Metrics/PerceivedComplexity
43
47
 
44
48
  # @api private
45
49
  def to_ast(*)
@@ -16,7 +16,7 @@ module Dry
16
16
  extend Dry::Initializer
17
17
 
18
18
  # @api private
19
- option :name, default: proc { nil }, optional: true
19
+ option :name, default: proc {}, optional: true
20
20
 
21
21
  # @api private
22
22
  option :compiler, default: proc { Compiler.new }
@@ -58,9 +58,9 @@ module Dry
58
58
  # @return [Macros::Core]
59
59
  #
60
60
  # @api public
61
- def value(*predicates, &block)
61
+ def value(...)
62
62
  append_macro(Macros::Value) do |macro|
63
- macro.call(*predicates, &block)
63
+ macro.call(...)
64
64
  end
65
65
  end
66
66
  ruby2_keywords :value if respond_to?(:ruby2_keywords, true)
@@ -76,9 +76,9 @@ module Dry
76
76
  # @return [Macros::Core]
77
77
  #
78
78
  # @api public
79
- def filled(*args, &block)
79
+ def filled(...)
80
80
  append_macro(Macros::Filled) do |macro|
81
- macro.call(*args, &block)
81
+ macro.call(...)
82
82
  end
83
83
  end
84
84
  ruby2_keywords :filled if respond_to?(:ruby2_keywords, true)
@@ -97,9 +97,9 @@ module Dry
97
97
  # @return [Macros::Core]
98
98
  #
99
99
  # @api public
100
- def schema(*args, &block)
100
+ def schema(...)
101
101
  append_macro(Macros::Schema) do |macro|
102
- macro.call(*args, &block)
102
+ macro.call(...)
103
103
  end
104
104
  end
105
105
  ruby2_keywords :schema if respond_to?(:ruby2_keywords, true)
@@ -112,9 +112,9 @@ module Dry
112
112
  # end
113
113
  #
114
114
  # @api public
115
- def hash(*args, &block)
115
+ def hash(...)
116
116
  append_macro(Macros::Hash) do |macro|
117
- macro.call(*args, &block)
117
+ macro.call(...)
118
118
  end
119
119
  end
120
120
  ruby2_keywords :hash if respond_to?(:ruby2_keywords, true)
@@ -136,9 +136,9 @@ module Dry
136
136
  # @return [Macros::Core]
137
137
  #
138
138
  # @api public
139
- def each(*args, &block)
139
+ def each(...)
140
140
  append_macro(Macros::Each) do |macro|
141
- macro.value(*args, &block)
141
+ macro.value(...)
142
142
  end
143
143
  end
144
144
  ruby2_keywords :each if respond_to?(:ruby2_keywords, true)
@@ -156,9 +156,9 @@ module Dry
156
156
  # @return [Macros::Core]
157
157
  #
158
158
  # @api public
159
- def array(*args, &block)
159
+ def array(...)
160
160
  append_macro(Macros::Array) do |macro|
161
- macro.value(*args, &block)
161
+ macro.value(...)
162
162
  end
163
163
  end
164
164
  ruby2_keywords :array if respond_to?(:ruby2_keywords, true)
@@ -200,10 +200,11 @@ module Dry
200
200
  end
201
201
 
202
202
  # @api private
203
+ # rubocop: disable Metrics/PerceivedComplexity
203
204
  def extract_type_spec(*args, nullable: false, set_type: true)
204
205
  type_spec = args[0] unless schema_or_predicate?(args[0])
205
206
 
206
- predicates = Array(type_spec ? args[1..-1] : args)
207
+ predicates = Array(type_spec ? args[1..] : args)
207
208
  type_rule = nil
208
209
 
209
210
  if type_spec
@@ -228,6 +229,7 @@ module Dry
228
229
  yield(*predicates, type_spec: type_spec, type_rule: nil)
229
230
  end
230
231
  end
232
+ # rubocop: enable Metrics/PerceivedComplexity
231
233
 
232
234
  # @api private
233
235
  def resolve_type(type_spec, nullable)
@@ -243,8 +245,8 @@ module Dry
243
245
  # @api private
244
246
  def schema_or_predicate?(arg)
245
247
  arg.is_a?(Dry::Schema::Processor) ||
246
- arg.is_a?(Symbol) &&
247
- arg.to_s.end_with?(QUESTION_MARK)
248
+ (arg.is_a?(Symbol) &&
249
+ arg.to_s.end_with?(QUESTION_MARK))
248
250
  end
249
251
  end
250
252
  end
@@ -29,7 +29,7 @@ module Dry
29
29
  else
30
30
  trace << hash?
31
31
 
32
- super(*args, &block)
32
+ super
33
33
  end
34
34
  end
35
35
  end
@@ -26,8 +26,8 @@ module Dry
26
26
  # @return [Macros::Key]
27
27
  #
28
28
  # @api public
29
- def filter(*args, &block)
30
- (filter_schema_dsl[name] || filter_schema_dsl.optional(name)).value(*args, &block)
29
+ def filter(...)
30
+ (filter_schema_dsl[name] || filter_schema_dsl.optional(name)).value(...)
31
31
  self
32
32
  end
33
33
  ruby2_keywords(:filter) if respond_to?(:ruby2_keywords, true)
@@ -45,6 +45,7 @@ module Dry
45
45
  end
46
46
 
47
47
  # @api private
48
+ # rubocop: disable Metrics/AbcSize
48
49
  def define(*args, &block)
49
50
  definition = schema_dsl.new(path: schema_dsl.path, &block)
50
51
  schema = definition.call
@@ -66,6 +67,7 @@ module Dry
66
67
 
67
68
  schema
68
69
  end
70
+ # rubocop: enable Metrics/AbcSize
69
71
 
70
72
  # @api private
71
73
  def parent_type
@@ -11,6 +11,10 @@ module Dry
11
11
  # @api private
12
12
  class Value < DSL
13
13
  # @api private
14
+ #
15
+ # rubocop:disable Metrics/AbcSize
16
+ # rubocop:disable Metrics/CyclomaticComplexity
17
+ # rubocop:disable Metrics/PerceivedComplexity
14
18
  def call(*args, **opts, &block)
15
19
  types, predicates = args.partition { |arg| arg.is_a?(Dry::Types::Type) }
16
20
 
@@ -65,6 +69,9 @@ module Dry
65
69
 
66
70
  self
67
71
  end
72
+ # rubocop:enable Metrics/AbcSize
73
+ # rubocop:enable Metrics/CyclomaticComplexity
74
+ # rubocop:enable Metrics/PerceivedComplexity
68
75
 
69
76
  # @api private
70
77
  def array_type?(type)
@@ -17,17 +17,19 @@ module Dry
17
17
  attr_reader :root
18
18
 
19
19
  # @api private
20
- def initialize(*args)
20
+ def initialize(...)
21
21
  super
22
- @root = [left, right].flatten.map(&:_path).reduce(:&)
23
- @left = left.map { |msg| msg.to_or(root) }
24
- @right = right.map { |msg| msg.to_or(root) }
22
+ flat_left = left.flatten
23
+ flat_right = right.flatten
24
+ @root = [*flat_left, *flat_right].map(&:_path).reduce(:&)
25
+ @left = flat_left.map { _1.to_or(root) }
26
+ @right = flat_right.map { _1.to_or(root) }
25
27
  end
26
28
 
27
29
  # @api public
28
30
  def to_h
29
31
  @to_h ||= Path[[*root, :or]].to_h(
30
- [left.map(&:to_h).reduce(:merge), right.map(&:to_h).reduce(:merge)]
32
+ [MessageSet.new(left).to_h, MessageSet.new(right).to_h]
31
33
  )
32
34
  end
33
35
  end
@@ -30,14 +30,14 @@ module Dry
30
30
 
31
31
  EMPTY_OPTS = VisitorOpts.new
32
32
  EMPTY_MESSAGE_SET = MessageSet.new(EMPTY_ARRAY).freeze
33
- FULL_MESSAGE_WHITESPACE = Hash.new(' ').merge(
34
- ja: '',
35
- zh: '',
36
- bn: '',
37
- th: '',
38
- lo: '',
39
- my: '',
40
- )
33
+ FULL_MESSAGE_WHITESPACE = Hash.new(" ").merge(
34
+ ja: "",
35
+ zh: "",
36
+ bn: "",
37
+ th: "",
38
+ lo: "",
39
+ my: ""
40
+ )
41
41
 
42
42
  param :messages
43
43
 
@@ -206,7 +206,8 @@ module Dry
206
206
  return text if !text || !full
207
207
 
208
208
  rule = options[:path]
209
- [messages.rule(rule, options) || rule, text].join(FULL_MESSAGE_WHITESPACE[template.options[:locale]])
209
+ [messages.rule(rule, options) || rule,
210
+ text].join(FULL_MESSAGE_WHITESPACE[template.options[:locale]])
210
211
  end
211
212
 
212
213
  # @api private
@@ -228,7 +229,9 @@ module Dry
228
229
  # @api private
229
230
  def append_mapped_size_tokens(tokens)
230
231
  # this is a temporary fix for the inconsistency in the "size" errors arguments
231
- mapped_hash = tokens.each_with_object({}) { |(k, v), h| h[k.to_s.gsub("size", "num").to_sym] = v }
232
+ mapped_hash = tokens.each_with_object({}) { |(k, v), h|
233
+ h[k.to_s.gsub("size", "num").to_sym] = v
234
+ }
232
235
  tokens.merge(mapped_hash)
233
236
  end
234
237
  end
@@ -5,125 +5,127 @@ require "dry/schema/messages/abstract"
5
5
 
6
6
  module Dry
7
7
  module Schema
8
- # I18n message backend
9
- #
10
- # @api public
11
- class Messages::I18n < Messages::Abstract
12
- # Translation function
13
- #
14
- # @return [Method]
15
- attr_reader :t
16
-
17
- # @api private
18
- def initialize
19
- super
20
- @t = I18n.method(:t)
21
- end
22
-
23
- # Get a message for the given key and its options
24
- #
25
- # @param [Symbol] key
26
- # @param [Hash] options
27
- #
28
- # @return [String]
8
+ module Messages
9
+ # I18n message backend
29
10
  #
30
11
  # @api public
31
- def get(key, options = EMPTY_HASH)
32
- return unless key
12
+ class I18n < Abstract
13
+ # Translation function
14
+ #
15
+ # @return [Method]
16
+ attr_reader :t
17
+
18
+ # @api private
19
+ def initialize
20
+ super
21
+ @t = ::I18n.method(:t)
22
+ end
33
23
 
34
- result = t.(key, locale: options.fetch(:locale, default_locale))
24
+ # Get a message for the given key and its options
25
+ #
26
+ # @param [Symbol] key
27
+ # @param [Hash] options
28
+ #
29
+ # @return [String]
30
+ #
31
+ # @api public
32
+ def get(key, options = EMPTY_HASH)
33
+ return unless key
34
+
35
+ result = t.(key, locale: options.fetch(:locale, default_locale))
36
+
37
+ if result.is_a?(Hash)
38
+ text = result[:text]
39
+ meta = result.dup.tap { |h| h.delete(:text) }
40
+ else
41
+ text = result
42
+ meta = EMPTY_HASH.dup
43
+ end
35
44
 
36
- if result.is_a?(Hash)
37
- text = result[:text]
38
- meta = result.dup.tap { |h| h.delete(:text) }
39
- else
40
- text = result
41
- meta = EMPTY_HASH.dup
45
+ {
46
+ text: text,
47
+ meta: meta
48
+ }
42
49
  end
43
50
 
44
- {
45
- text: text,
46
- meta: meta
47
- }
48
- end
49
-
50
- # Check if given key is defined
51
- #
52
- # @return [Boolean]
53
- #
54
- # @api public
55
- def key?(key, options)
56
- I18n.exists?(key, options.fetch(:locale, default_locale)) ||
57
- I18n.exists?(key, I18n.default_locale)
58
- end
51
+ # Check if given key is defined
52
+ #
53
+ # @return [Boolean]
54
+ #
55
+ # @api public
56
+ def key?(key, options)
57
+ ::I18n.exists?(key, options.fetch(:locale, default_locale)) ||
58
+ ::I18n.exists?(key, ::I18n.default_locale)
59
+ end
59
60
 
60
- # Merge messages from an additional path
61
- #
62
- # @param [String, Array<String>] paths
63
- #
64
- # @return [Messages::I18n]
65
- #
66
- # @api public
67
- def merge(paths)
68
- prepare(paths)
69
- end
61
+ # Merge messages from an additional path
62
+ #
63
+ # @param [String, Array<String>] paths
64
+ #
65
+ # @return [Messages::I18n]
66
+ #
67
+ # @api public
68
+ def merge(paths)
69
+ prepare(paths)
70
+ end
70
71
 
71
- # @api private
72
- def default_locale
73
- super || I18n.locale || I18n.default_locale
74
- end
72
+ # @api private
73
+ def default_locale
74
+ super || ::I18n.locale || ::I18n.default_locale
75
+ end
75
76
 
76
- # @api private
77
- def prepare(paths = config.load_paths)
78
- paths.each do |path|
79
- data = YAML.load_file(path)
77
+ # @api private
78
+ def prepare(paths = config.load_paths)
79
+ paths.each do |path|
80
+ data = ::YAML.load_file(path)
80
81
 
81
- if custom_top_namespace?(path)
82
- top_namespace = config.top_namespace
82
+ if custom_top_namespace?(path)
83
+ top_namespace = config.top_namespace
83
84
 
84
- mapped_data = data
85
- .map { |k, v| [k, {top_namespace => v[DEFAULT_MESSAGES_ROOT]}] }
86
- .to_h
85
+ mapped_data = data.transform_values { |v|
86
+ {top_namespace => v[DEFAULT_MESSAGES_ROOT]}
87
+ }
87
88
 
88
- store_translations(mapped_data)
89
- else
90
- store_translations(data)
89
+ store_translations(mapped_data)
90
+ else
91
+ store_translations(data)
92
+ end
91
93
  end
92
- end
93
94
 
94
- self
95
- end
95
+ self
96
+ end
96
97
 
97
- # @api private
98
- def interpolatable_data(_key, _options, **data)
99
- data
100
- end
98
+ # @api private
99
+ def interpolatable_data(_key, _options, **data)
100
+ data
101
+ end
101
102
 
102
- # @api private
103
- def interpolate(key, options, **data)
104
- text_key = "#{key}.text"
103
+ # @api private
104
+ def interpolate(key, options, **data)
105
+ text_key = "#{key}.text"
105
106
 
106
- opts = {
107
- locale: default_locale,
108
- **options,
109
- **data
110
- }
107
+ opts = {
108
+ locale: default_locale,
109
+ **options,
110
+ **data
111
+ }
111
112
 
112
- resolved_key = key?(text_key, opts) ? text_key : key
113
+ resolved_key = key?(text_key, opts) ? text_key : key
113
114
 
114
- t.(resolved_key, **opts)
115
- end
115
+ t.(resolved_key, **opts)
116
+ end
116
117
 
117
- private
118
+ private
118
119
 
119
- # @api private
120
- def store_translations(data)
121
- locales = data.keys.map(&:to_sym)
120
+ # @api private
121
+ def store_translations(data)
122
+ locales = data.keys.map(&:to_sym)
122
123
 
123
- I18n.available_locales |= locales
124
+ ::I18n.available_locales |= locales
124
125
 
125
- locales.each do |locale|
126
- I18n.backend.store_translations(locale, data[locale.to_s])
126
+ locales.each do |locale|
127
+ ::I18n.backend.store_translations(locale, data[locale.to_s])
128
+ end
127
129
  end
128
130
  end
129
131
  end
@@ -21,6 +21,7 @@ module Dry
21
21
 
22
22
  # @api private
23
23
  def initialize(namespace, messages)
24
+ super()
24
25
  @config = messages.config
25
26
  @namespace = namespace
26
27
  @messages = messages