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
@@ -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
@@ -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
 
@@ -185,13 +185,11 @@ module Dry
185
185
  "#<#{self.class}#{to_h.inspect} errors=#{errors.to_h.inspect} path=#{path.keys.inspect}>"
186
186
  end
187
187
 
188
- if RUBY_VERSION >= "2.7"
189
- # Pattern matching support
190
- #
191
- # @api private
192
- def deconstruct_keys(_)
193
- output
194
- end
188
+ # Pattern matching support
189
+ #
190
+ # @api private
191
+ def deconstruct_keys(_)
192
+ output
195
193
  end
196
194
 
197
195
  # Add a new error AST node
@@ -89,6 +89,10 @@ module Dry
89
89
  captures.map(&:to_ast).map(&compiler.method(:visit)).reduce(:and)
90
90
  end
91
91
 
92
+ def respond_to_missing?(meth, include_private = false)
93
+ super || (meth.to_s.end_with?(QUESTION_MARK) && compuiler.support?(meth))
94
+ end
95
+
92
96
  # @api private
93
97
  def method_missing(meth, *args, &block)
94
98
  if meth.to_s.end_with?(QUESTION_MARK)
@@ -96,7 +100,7 @@ module Dry
96
100
  ::Kernel.raise InvalidSchemaError, "#{meth} predicate cannot be used in this context"
97
101
  end
98
102
 
99
- unless compiler.supports?(meth)
103
+ unless compiler.support?(meth)
100
104
  ::Kernel.raise ::ArgumentError, "#{meth} predicate is not defined"
101
105
  end
102
106
 
@@ -37,8 +37,7 @@ module Dry
37
37
  def [](name)
38
38
  key = [namespace, name].compact.join(DOT)
39
39
 
40
- type = types.registered?(key) ? types[key] : types[name.to_s]
41
- type
40
+ types.registered?(key) ? types[key] : types[name.to_s]
42
41
  end
43
42
  end
44
43
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module Schema
5
- VERSION = "1.8.0"
5
+ VERSION = "1.9.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.8.0
4
+ version: 1.9.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-09-12 00:00:00.000000000 Z
11
+ date: 2022-02-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -178,6 +178,8 @@ files:
178
178
  - lib/dry/schema/extensions/hints/result_methods.rb
179
179
  - lib/dry/schema/extensions/info.rb
180
180
  - lib/dry/schema/extensions/info/schema_compiler.rb
181
+ - lib/dry/schema/extensions/json_schema.rb
182
+ - lib/dry/schema/extensions/json_schema/schema_compiler.rb
181
183
  - lib/dry/schema/extensions/monads.rb
182
184
  - lib/dry/schema/extensions/struct.rb
183
185
  - lib/dry/schema/json.rb
@@ -246,7 +248,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
246
248
  requirements:
247
249
  - - ">="
248
250
  - !ruby/object:Gem::Version
249
- version: 2.6.0
251
+ version: 2.7.0
250
252
  required_rubygems_version: !ruby/object:Gem::Requirement
251
253
  requirements:
252
254
  - - ">="