dry-schema 1.8.0 → 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/README.md +4 -4
  4. data/dry-schema.gemspec +2 -2
  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 +6 -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 +2 -2
  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 +11 -8
@@ -17,8 +17,8 @@ module Dry
17
17
  attr_reader :key_map, :coercer
18
18
 
19
19
  # @api private
20
- def self.new(*args, &coercer)
21
- fetch_or_store(*args, coercer) { super(*args, &coercer) }
20
+ def self.new(*args)
21
+ fetch_or_store(*args) { super }
22
22
  end
23
23
 
24
24
  # @api private
@@ -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
@@ -79,6 +80,11 @@ module Dry
79
80
  def interpolate(key, options, **data)
80
81
  messages.interpolate(key, options, **data)
81
82
  end
83
+
84
+ # @api private
85
+ def translate(key, **args)
86
+ messages.translate(key, **args)
87
+ end
82
88
  end
83
89
  end
84
90
  end