dry-schema 1.7.0 → 1.9.1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/README.md +2 -2
  4. data/dry-schema.gemspec +2 -2
  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 -74
  16. data/lib/dry/schema/key_coercer.rb +2 -2
  17. data/lib/dry/schema/key_validator.rb +44 -23
  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 +7 -0
  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 +6 -0
  30. data/lib/dry/schema/messages/yaml.rb +165 -158
  31. data/lib/dry/schema/predicate.rb +2 -2
  32. data/lib/dry/schema/predicate_inferrer.rb +2 -0
  33. data/lib/dry/schema/primitive_inferrer.rb +2 -0
  34. data/lib/dry/schema/processor.rb +4 -4
  35. data/lib/dry/schema/result.rb +5 -7
  36. data/lib/dry/schema/trace.rb +5 -1
  37. data/lib/dry/schema/type_registry.rb +1 -2
  38. data/lib/dry/schema/version.rb +1 -1
  39. metadata +9 -7
@@ -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
@@ -9,195 +9,202 @@ require "dry/schema/messages/abstract"
9
9
 
10
10
  module Dry
11
11
  module Schema
12
- # Plain YAML message backend
13
- #
14
- # @api public
15
- class Messages::YAML < Messages::Abstract
16
- LOCALE_TOKEN = "%<locale>s"
17
- TOKEN_REGEXP = /%{(\w*)}/.freeze
18
- EMPTY_CONTEXT = Object.new.tap { |ctx|
19
- def ctx.context
20
- binding
21
- end
22
- }.freeze.context
23
-
24
- include Dry::Equalizer(:data)
25
-
26
- # Loaded localized message templates
12
+ module Messages
13
+ # Plain YAML message backend
27
14
  #
28
- # @return [Hash]
29
- attr_reader :data
15
+ # @api public
16
+ class YAML < Abstract
17
+ LOCALE_TOKEN = "%<locale>s"
18
+ TOKEN_REGEXP = /%{(\w*)}/.freeze
19
+ EMPTY_CONTEXT = Object.new.tap { |ctx|
20
+ def ctx.context
21
+ binding
22
+ end
23
+ }.freeze.context
30
24
 
31
- # Translation function
32
- #
33
- # @return [Proc]
34
- attr_reader :t
25
+ include ::Dry::Equalizer(:data)
35
26
 
36
- # @api private
37
- def self.build(options = EMPTY_HASH)
38
- super do |config|
39
- config.default_locale = :en unless config.default_locale
27
+ # Loaded localized message templates
28
+ #
29
+ # @return [Hash]
30
+ attr_reader :data
40
31
 
41
- config.root = "%<locale>s.#{config.root}"
32
+ # Translation function
33
+ #
34
+ # @return [Proc]
35
+ attr_reader :t
42
36
 
43
- config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
44
- "%<locale>s.#{path}"
45
- }
46
- end
47
- end
37
+ # @api private
38
+ def self.build(options = EMPTY_HASH)
39
+ super do |config|
40
+ config.default_locale = :en unless config.default_locale
48
41
 
49
- # @api private
50
- def self.flat_hash(hash, path = [], keys = {})
51
- hash.each do |key, value|
52
- flat_hash(value, [*path, key], keys) if value.is_a?(Hash)
42
+ config.root = "%<locale>s.#{config.root}"
53
43
 
54
- if value.is_a?(String) && hash["text"] != value
55
- keys[[*path, key].join(DOT)] = {
56
- text: value,
57
- meta: EMPTY_HASH
58
- }
59
- elsif value.is_a?(Hash) && value["text"].is_a?(String)
60
- keys[[*path, key].join(DOT)] = {
61
- text: value["text"],
62
- meta: value.dup.delete_if { |k| k == "text" }.map { |k, v| [k.to_sym, v] }.to_h
44
+ config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
45
+ "%<locale>s.#{path}"
63
46
  }
64
47
  end
65
48
  end
66
- keys
67
- end
68
-
69
- # @api private
70
- def self.cache
71
- @cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
72
- end
73
49
 
74
- # @api private
75
- def self.source_cache
76
- @source_cache ||= Concurrent::Map.new
77
- end
78
-
79
- # @api private
80
- def initialize(data: EMPTY_HASH, config: nil)
81
- super()
82
- @data = data
83
- @config = config if config
84
- @t = proc { |key, locale: default_locale| get("%<locale>s.#{key}", locale: locale) }
85
- end
50
+ # @api private
51
+ # rubocop: disable Metrics/PerceivedComplexity
52
+ def self.flat_hash(hash, path = EMPTY_ARRAY, keys = {})
53
+ hash.each do |key, value|
54
+ flat_hash(value, [*path, key], keys) if value.is_a?(Hash)
55
+
56
+ if value.is_a?(String) && hash["text"] != value
57
+ keys[[*path, key].join(DOT)] = {
58
+ text: value,
59
+ meta: EMPTY_HASH
60
+ }
61
+ elsif value.is_a?(Hash) && value["text"].is_a?(String)
62
+ keys[[*path, key].join(DOT)] = {
63
+ text: value["text"],
64
+ meta: value.reject { _1.eql?("text") }.transform_keys(&:to_sym)
65
+ }
66
+ end
67
+ end
86
68
 
87
- # Get an array of looked up paths
88
- #
89
- # @param [Symbol] predicate
90
- # @param [Hash] options
91
- #
92
- # @return [String]
93
- #
94
- # @api public
95
- def looked_up_paths(predicate, options)
96
- super.map { |path| path % {locale: options[:locale] || default_locale} }
97
- end
69
+ keys
70
+ end
71
+ # rubocop: enable Metrics/PerceivedComplexity
98
72
 
99
- # Get a message for the given key and its options
100
- #
101
- # @param [Symbol] key
102
- # @param [Hash] options
103
- #
104
- # @return [String]
105
- #
106
- # @api public
107
- def get(key, options = EMPTY_HASH)
108
- data[evaluated_key(key, options)]
109
- end
73
+ # @api private
74
+ def self.cache
75
+ @cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
76
+ end
110
77
 
111
- # Check if given key is defined
112
- #
113
- # @return [Boolean]
114
- #
115
- # @api public
116
- def key?(key, options = EMPTY_HASH)
117
- data.key?(evaluated_key(key, options))
118
- end
78
+ # @api private
79
+ def self.source_cache
80
+ @source_cache ||= Concurrent::Map.new
81
+ end
119
82
 
120
- # Merge messages from an additional path
121
- #
122
- # @param [String] overrides
123
- #
124
- # @return [Messages::I18n]
125
- #
126
- # @api public
127
- def merge(overrides)
128
- if overrides.is_a?(Hash)
129
- self.class.new(
130
- data: data.merge(self.class.flat_hash(overrides)),
131
- config: config
132
- )
133
- else
134
- self.class.new(
135
- data: Array(overrides).reduce(data) { |a, e| a.merge(load_translations(e)) },
136
- config: config
137
- )
83
+ # @api private
84
+ def initialize(data: EMPTY_HASH, config: nil)
85
+ super()
86
+ @data = data
87
+ @config = config if config
88
+ @t = proc { |key, locale: default_locale| get("%<locale>s.#{key}", locale: locale) }
138
89
  end
139
- end
140
90
 
141
- # @api private
142
- def prepare
143
- @data = config.load_paths.map { |path| load_translations(path) }.reduce({}, :merge)
144
- self
145
- end
91
+ # Get an array of looked up paths
92
+ #
93
+ # @param [Symbol] predicate
94
+ # @param [Hash] options
95
+ #
96
+ # @return [String]
97
+ #
98
+ # @api public
99
+ def looked_up_paths(predicate, options)
100
+ super.map { |path| path % {locale: options[:locale] || default_locale} }
101
+ end
146
102
 
147
- # @api private
148
- def interpolatable_data(key, options, **data)
149
- tokens = evaluation_context(key, options).fetch(:tokens)
150
- data.select { |k,| tokens.include?(k) }
151
- end
103
+ # Get a message for the given key and its options
104
+ #
105
+ # @param [Symbol] key
106
+ # @param [Hash] options
107
+ #
108
+ # @return [String]
109
+ #
110
+ # @api public
111
+ def get(key, options = EMPTY_HASH)
112
+ data[evaluated_key(key, options)]
113
+ end
152
114
 
153
- # @api private
154
- def interpolate(key, options, **data)
155
- evaluator = evaluation_context(key, options).fetch(:evaluator)
156
- data.empty? ? evaluator.() : evaluator.(**data)
157
- end
115
+ # Check if given key is defined
116
+ #
117
+ # @return [Boolean]
118
+ #
119
+ # @api public
120
+ def key?(key, options = EMPTY_HASH)
121
+ data.key?(evaluated_key(key, options))
122
+ end
158
123
 
159
- private
124
+ # Merge messages from an additional path
125
+ #
126
+ # @param [String] overrides
127
+ #
128
+ # @return [Messages::I18n]
129
+ #
130
+ # @api public
131
+ def merge(overrides)
132
+ if overrides.is_a?(Hash)
133
+ self.class.new(
134
+ data: data.merge(self.class.flat_hash(overrides)),
135
+ config: config
136
+ )
137
+ else
138
+ self.class.new(
139
+ data: Array(overrides).reduce(data) { |a, e| a.merge(load_translations(e)) },
140
+ config: config
141
+ )
142
+ end
143
+ end
160
144
 
161
- # @api private
162
- def evaluation_context(key, options)
163
- cache.fetch_or_store(get(key, options).fetch(:text)) do |input|
164
- tokens = input.scan(TOKEN_REGEXP).flatten(1).map(&:to_sym).to_set
165
- text = input.gsub("%", "#")
145
+ # @api private
146
+ def prepare
147
+ @data = config.load_paths.map { |path| load_translations(path) }.reduce({}, :merge)
148
+ self
149
+ end
166
150
 
167
- # rubocop:disable Security/Eval
168
- evaluator = eval(<<~RUBY, EMPTY_CONTEXT, __FILE__, __LINE__ + 1)
169
- -> (#{tokens.map { |token| "#{token}:" }.join(", ")}) { "#{text}" }
170
- RUBY
171
- # rubocop:enable Security/Eval
151
+ # @api private
152
+ def interpolatable_data(key, options, **data)
153
+ tokens = evaluation_context(key, options).fetch(:tokens)
154
+ data.select { |k,| tokens.include?(k) }
155
+ end
172
156
 
173
- {
174
- tokens: tokens,
175
- evaluator: evaluator
176
- }
157
+ # @api private
158
+ def interpolate(key, options, **data)
159
+ evaluator = evaluation_context(key, options).fetch(:evaluator)
160
+ data.empty? ? evaluator.() : evaluator.(**data)
177
161
  end
178
- end
179
162
 
180
- # @api private
181
- def cache
182
- @cache ||= self.class.cache[self]
183
- end
163
+ private
164
+
165
+ # @api private
166
+ def evaluation_context(key, options)
167
+ cache.fetch_or_store(get(key, options).fetch(:text)) do |input|
168
+ tokens = input.scan(TOKEN_REGEXP).flatten(1).map(&:to_sym).to_set
169
+ text = input.gsub("%", "#")
170
+
171
+ # rubocop:disable Security/Eval
172
+ # rubocop:disable Style/DocumentDynamicEvalDefinition
173
+ evaluator = eval(<<~RUBY, EMPTY_CONTEXT, __FILE__, __LINE__ + 1)
174
+ -> (#{tokens.map { |token| "#{token}:" }.join(", ")}) { "#{text}" }
175
+ RUBY
176
+ # rubocop:enable Style/DocumentDynamicEvalDefinition
177
+ # rubocop:enable Security/Eval
178
+
179
+ {
180
+ tokens: tokens,
181
+ evaluator: evaluator
182
+ }
183
+ end
184
+ end
184
185
 
185
- # @api private
186
- def load_translations(path)
187
- data = self.class.source_cache.fetch_or_store(path) do
188
- self.class.flat_hash(YAML.load_file(path)).freeze
186
+ # @api private
187
+ def cache
188
+ @cache ||= self.class.cache[self]
189
189
  end
190
190
 
191
- return data unless custom_top_namespace?(path)
191
+ # @api private
192
+ def load_translations(path)
193
+ data = self.class.source_cache.fetch_or_store(path) do
194
+ self.class.flat_hash(::YAML.load_file(path)).freeze
195
+ end
192
196
 
193
- data.map { |k, v| [k.gsub(DEFAULT_MESSAGES_ROOT, config.top_namespace), v] }.to_h
194
- end
197
+ return data unless custom_top_namespace?(path)
195
198
 
196
- # @api private
197
- def evaluated_key(key, options)
198
- return key unless key.include?(LOCALE_TOKEN)
199
+ data.transform_keys { _1.gsub(DEFAULT_MESSAGES_ROOT, config.top_namespace) }
200
+ end
201
+
202
+ # @api private
203
+ def evaluated_key(key, options)
204
+ return key unless key.include?(LOCALE_TOKEN)
199
205
 
200
- key % {locale: options[:locale] || default_locale}
206
+ key % {locale: options[:locale] || default_locale}
207
+ end
201
208
  end
202
209
  end
203
210
  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
@@ -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