dry-schema 1.4.1 → 1.5.2

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +210 -73
  3. data/LICENSE +1 -1
  4. data/README.md +4 -6
  5. data/config/errors.yml +4 -0
  6. data/dry-schema.gemspec +46 -0
  7. data/lib/dry-schema.rb +1 -1
  8. data/lib/dry/schema.rb +20 -7
  9. data/lib/dry/schema/compiler.rb +5 -5
  10. data/lib/dry/schema/config.rb +15 -6
  11. data/lib/dry/schema/constants.rb +16 -7
  12. data/lib/dry/schema/dsl.rb +89 -31
  13. data/lib/dry/schema/extensions.rb +10 -2
  14. data/lib/dry/schema/extensions/hints.rb +15 -8
  15. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +2 -2
  16. data/lib/dry/schema/extensions/hints/message_set_methods.rb +0 -47
  17. data/lib/dry/schema/extensions/info.rb +27 -0
  18. data/lib/dry/schema/extensions/info/schema_compiler.rb +105 -0
  19. data/lib/dry/schema/extensions/monads.rb +1 -1
  20. data/lib/dry/schema/extensions/struct.rb +32 -0
  21. data/lib/dry/schema/json.rb +1 -1
  22. data/lib/dry/schema/key.rb +20 -5
  23. data/lib/dry/schema/key_coercer.rb +4 -4
  24. data/lib/dry/schema/key_map.rb +9 -4
  25. data/lib/dry/schema/key_validator.rb +67 -0
  26. data/lib/dry/schema/macros.rb +8 -8
  27. data/lib/dry/schema/macros/array.rb +17 -4
  28. data/lib/dry/schema/macros/core.rb +11 -6
  29. data/lib/dry/schema/macros/dsl.rb +44 -23
  30. data/lib/dry/schema/macros/each.rb +4 -4
  31. data/lib/dry/schema/macros/filled.rb +5 -5
  32. data/lib/dry/schema/macros/hash.rb +21 -3
  33. data/lib/dry/schema/macros/key.rb +10 -9
  34. data/lib/dry/schema/macros/maybe.rb +4 -5
  35. data/lib/dry/schema/macros/optional.rb +1 -1
  36. data/lib/dry/schema/macros/required.rb +1 -1
  37. data/lib/dry/schema/macros/schema.rb +23 -2
  38. data/lib/dry/schema/macros/value.rb +34 -7
  39. data/lib/dry/schema/message.rb +35 -9
  40. data/lib/dry/schema/message/or.rb +18 -39
  41. data/lib/dry/schema/message/or/abstract.rb +28 -0
  42. data/lib/dry/schema/message/or/multi_path.rb +37 -0
  43. data/lib/dry/schema/message/or/single_path.rb +64 -0
  44. data/lib/dry/schema/message_compiler.rb +58 -22
  45. data/lib/dry/schema/message_compiler/visitor_opts.rb +2 -2
  46. data/lib/dry/schema/message_set.rb +26 -37
  47. data/lib/dry/schema/messages.rb +6 -6
  48. data/lib/dry/schema/messages/abstract.rb +54 -62
  49. data/lib/dry/schema/messages/i18n.rb +36 -10
  50. data/lib/dry/schema/messages/namespaced.rb +12 -2
  51. data/lib/dry/schema/messages/template.rb +19 -44
  52. data/lib/dry/schema/messages/yaml.rb +61 -14
  53. data/lib/dry/schema/params.rb +1 -1
  54. data/lib/dry/schema/path.rb +44 -5
  55. data/lib/dry/schema/predicate.rb +4 -2
  56. data/lib/dry/schema/predicate_inferrer.rb +4 -184
  57. data/lib/dry/schema/predicate_registry.rb +2 -2
  58. data/lib/dry/schema/primitive_inferrer.rb +16 -0
  59. data/lib/dry/schema/processor.rb +50 -29
  60. data/lib/dry/schema/processor_steps.rb +50 -27
  61. data/lib/dry/schema/result.rb +53 -6
  62. data/lib/dry/schema/rule_applier.rb +7 -7
  63. data/lib/dry/schema/step.rb +79 -0
  64. data/lib/dry/schema/trace.rb +5 -4
  65. data/lib/dry/schema/type_container.rb +3 -3
  66. data/lib/dry/schema/type_registry.rb +2 -2
  67. data/lib/dry/schema/types.rb +1 -1
  68. data/lib/dry/schema/value_coercer.rb +2 -2
  69. data/lib/dry/schema/version.rb +1 -1
  70. metadata +21 -7
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/constants'
4
- require 'dry/schema/message'
3
+ require "dry/schema/constants"
4
+ require "dry/schema/message"
5
5
 
6
6
  module Dry
7
7
  module Schema
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/equalizer'
3
+ require "dry/equalizer"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -17,13 +17,7 @@ module Dry
17
17
  #
18
18
  # @return [Array<Message>]
19
19
  attr_reader :messages
20
-
21
- # An internal hash that is filled in with dumped messages
22
- # when a message set is coerced to a hash
23
- #
24
- # @return [Hash<Symbol=>[Array,Hash]>]
25
- attr_reader :placeholders
26
-
20
+
27
21
  # Options hash
28
22
  #
29
23
  # @return [Hash]
@@ -38,7 +32,6 @@ module Dry
38
32
  def initialize(messages, options = EMPTY_HASH)
39
33
  @messages = messages
40
34
  @options = options
41
- initialize_placeholders!
42
35
  end
43
36
 
44
37
  # Iterate over messages
@@ -112,43 +105,39 @@ module Dry
112
105
 
113
106
  # @api private
114
107
  def messages_map(messages = self.messages)
115
- return EMPTY_HASH if empty?
116
-
117
- messages.group_by(&:path).reduce(placeholders) do |hash, (path, msgs)|
118
- node = path.reduce(hash) { |a, e| a[e] }
108
+ combine_message_hashes(messages.map(&:to_h))
109
+ end
119
110
 
120
- msgs.each do |msg|
121
- node << msg
111
+ # @api private
112
+ def combine_message_hashes(hashes)
113
+ hashes.reduce(EMPTY_HASH.dup) do |a, e|
114
+ a.merge(e) do |_, *values|
115
+ combine_message_values(values)
122
116
  end
123
-
124
- node.map!(&:dump)
125
-
126
- hash
127
117
  end
128
118
  end
129
119
 
130
120
  # @api private
131
- def paths
132
- @paths ||= messages.map(&:path).uniq
121
+ def combine_message_values(values)
122
+ hashes, other = partition_message_values(values)
123
+ combined = combine_message_hashes(hashes)
124
+ flattened = other.flatten
125
+
126
+ if flattened.empty?
127
+ combined
128
+ elsif combined.empty?
129
+ flattened
130
+ else
131
+ [flattened, combined]
132
+ end
133
133
  end
134
134
 
135
135
  # @api private
136
- def initialize_placeholders!
137
- return @placeholders = EMPTY_HASH if empty?
138
-
139
- @placeholders = paths.reduce(EMPTY_HASH.dup) do |hash, path|
140
- curr_idx = 0
141
- last_idx = path.size - 1
142
- node = hash
143
-
144
- while curr_idx <= last_idx
145
- key = path[curr_idx]
146
- node = (node[key] || node[key] = curr_idx < last_idx ? {} : [])
147
- curr_idx += 1
148
- end
149
-
150
- hash
151
- end
136
+ def partition_message_values(values)
137
+ values
138
+ .map { |value| value.is_a?(Array) ? value : [value] }
139
+ .reduce(EMPTY_ARRAY.dup, :+)
140
+ .partition { |value| value.is_a?(Hash) && !value[:text].is_a?(String) }
152
141
  end
153
142
  end
154
143
  end
@@ -7,8 +7,8 @@ module Dry
7
7
  # @api private
8
8
  module Messages
9
9
  BACKENDS = {
10
- i18n: 'I18n',
11
- yaml: 'YAML'
10
+ i18n: "I18n",
11
+ yaml: "YAML"
12
12
  }.freeze
13
13
 
14
14
  module_function
@@ -31,7 +31,7 @@ module Dry
31
31
  end
32
32
  end
33
33
 
34
- require 'dry/schema/messages/abstract'
35
- require 'dry/schema/messages/namespaced'
36
- require 'dry/schema/messages/yaml'
37
- require 'dry/schema/messages/i18n' if defined?(I18n)
34
+ require "dry/schema/messages/abstract"
35
+ require "dry/schema/messages/namespaced"
36
+ require "dry/schema/messages/yaml"
37
+ require "dry/schema/messages/i18n" if defined?(I18n)
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
- require 'concurrent/map'
5
- require 'dry/equalizer'
6
- require 'dry/configurable'
3
+ require "set"
4
+ require "concurrent/map"
5
+ require "dry/equalizer"
6
+ require "dry/configurable"
7
7
 
8
- require 'dry/schema/constants'
9
- require 'dry/schema/messages/template'
8
+ require "dry/schema/constants"
9
+ require "dry/schema/messages/template"
10
10
 
11
11
  module Dry
12
12
  module Schema
@@ -21,36 +21,31 @@ module Dry
21
21
  setting :default_locale, nil
22
22
  setting :load_paths, Set[DEFAULT_MESSAGES_PATH]
23
23
  setting :top_namespace, DEFAULT_MESSAGES_ROOT
24
- setting :root, 'errors'
24
+ setting :root, "errors"
25
25
  setting :lookup_options, %i[root predicate path val_type arg_type].freeze
26
26
 
27
27
  setting :lookup_paths, [
28
- '%<root>s.rules.%<path>s.%<predicate>s.arg.%<arg_type>s',
29
- '%<root>s.rules.%<path>s.%<predicate>s',
30
- '%<root>s.%<predicate>s.%<message_type>s',
31
- '%<root>s.%<predicate>s.value.%<path>s',
32
- '%<root>s.%<predicate>s.value.%<val_type>s.arg.%<arg_type>s',
33
- '%<root>s.%<predicate>s.value.%<val_type>s',
34
- '%<root>s.%<predicate>s.arg.%<arg_type>s',
35
- '%<root>s.%<predicate>s'
28
+ "%<root>s.rules.%<path>s.%<predicate>s.arg.%<arg_type>s",
29
+ "%<root>s.rules.%<path>s.%<predicate>s",
30
+ "%<root>s.%<predicate>s.%<message_type>s",
31
+ "%<root>s.%<predicate>s.value.%<path>s",
32
+ "%<root>s.%<predicate>s.value.%<val_type>s.arg.%<arg_type>s",
33
+ "%<root>s.%<predicate>s.value.%<val_type>s",
34
+ "%<root>s.%<predicate>s.arg.%<arg_type>s",
35
+ "%<root>s.%<predicate>s"
36
36
  ].freeze
37
37
 
38
- setting :rule_lookup_paths, ['rules.%<name>s'].freeze
38
+ setting :rule_lookup_paths, ["rules.%<name>s"].freeze
39
39
 
40
- setting :arg_types, Hash.new { |*| 'default' }.update(
41
- Range => 'range'
40
+ setting :arg_types, Hash.new { |*| "default" }.update(
41
+ Range => "range"
42
42
  )
43
43
 
44
- setting :val_types, Hash.new { |*| 'default' }.update(
45
- Range => 'range',
46
- String => 'string'
44
+ setting :val_types, Hash.new { |*| "default" }.update(
45
+ Range => "range",
46
+ String => "string"
47
47
  )
48
48
 
49
- # @api private
50
- def self.cache
51
- @cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
52
- end
53
-
54
49
  # @api private
55
50
  def self.build(options = EMPTY_HASH)
56
51
  messages = new
@@ -79,7 +74,7 @@ module Dry
79
74
 
80
75
  # @api private
81
76
  def rule(name, options = {})
82
- tokens = { name: name, locale: options.fetch(:locale, default_locale) }
77
+ tokens = {name: name, locale: options.fetch(:locale, default_locale)}
83
78
  path = rule_lookup_paths(tokens).detect { |key| key?(key, options) }
84
79
 
85
80
  rule = get(path, options) if path
@@ -92,13 +87,35 @@ module Dry
92
87
  #
93
88
  # @api public
94
89
  def call(predicate, options)
95
- cache.fetch_or_store(cache_key(predicate, options)) do
96
- text, meta = lookup(predicate, options)
97
- [Template[text], meta] if text
98
- end
90
+ options = {locale: default_locale, **options}
91
+ opts = options.reject { |k,| config.lookup_options.include?(k) }
92
+ path = lookup_paths(predicate, options).detect { |key| key?(key, opts) }
93
+
94
+ return unless path
95
+
96
+ result = get(path, opts)
97
+
98
+ [
99
+ Template.new(
100
+ messages: self,
101
+ key: path,
102
+ options: opts
103
+ ),
104
+ result[:meta]
105
+ ]
99
106
  end
107
+
100
108
  alias_method :[], :call
101
109
 
110
+ # Check if given key is defined
111
+ #
112
+ # @return [Boolean]
113
+ #
114
+ # @api public
115
+ def key?(_key, _options = EMPTY_HASH)
116
+ raise NotImplementedError
117
+ end
118
+
102
119
  # Retrieve an array of looked up paths
103
120
  #
104
121
  # @param [Symbol] predicate
@@ -112,27 +129,6 @@ module Dry
112
129
  filled_lookup_paths(tokens)
113
130
  end
114
131
 
115
- # Try to find a message for the given predicate and its options
116
- #
117
- # @api private
118
- #
119
- # rubocop:disable Metrics/AbcSize
120
- def lookup(predicate, options)
121
- opts = options.reject { |k, _| config.lookup_options.include?(k) }
122
- path = lookup_paths(predicate, options).detect { |key| key?(key, opts) }
123
-
124
- return unless path
125
-
126
- text = get(path, opts)
127
-
128
- if text.is_a?(Hash)
129
- text.values_at(:text, :meta)
130
- else
131
- [text, EMPTY_HASH]
132
- end
133
- end
134
- # rubocop:enable Metrics/AbcSize
135
-
136
132
  # @api private
137
133
  def lookup_paths(predicate, options)
138
134
  tokens = lookup_tokens(predicate, options)
@@ -168,22 +164,18 @@ module Dry
168
164
  end
169
165
 
170
166
  # @api private
171
- def cache
172
- @cache ||= self.class.cache[self]
167
+ def default_locale
168
+ config.default_locale
173
169
  end
174
170
 
175
171
  # @api private
176
- def default_locale
177
- config.default_locale
172
+ def interpolatable_data(_key, _options, **_data)
173
+ raise NotImplementedError
178
174
  end
179
175
 
180
176
  # @api private
181
- def cache_key(predicate, options)
182
- if options.key?(:input)
183
- [predicate, options.reject { |k,| k.equal?(:input) }]
184
- else
185
- [predicate, options]
186
- end
177
+ def interpolate(_key, _options, **_data)
178
+ raise NotImplementedError
187
179
  end
188
180
 
189
181
  private
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n'
4
- require 'dry/schema/messages/abstract'
3
+ require "i18n"
4
+ require "dry/schema/messages/abstract"
5
5
 
6
6
  module Dry
7
7
  module Schema
@@ -29,7 +29,22 @@ module Dry
29
29
  #
30
30
  # @api public
31
31
  def get(key, options = EMPTY_HASH)
32
- t.(key, locale: options.fetch(:locale, default_locale)) if key
32
+ return unless key
33
+
34
+ result = t.(key, locale: options.fetch(:locale, default_locale))
35
+
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
42
+ end
43
+
44
+ {
45
+ text: text,
46
+ meta: meta
47
+ }
33
48
  end
34
49
 
35
50
  # Check if given key is defined
@@ -67,7 +82,7 @@ module Dry
67
82
  top_namespace = config.top_namespace
68
83
 
69
84
  mapped_data = data
70
- .map { |k, v| [k, { top_namespace => v[DEFAULT_MESSAGES_ROOT] }] }
85
+ .map { |k, v| [k, {top_namespace => v[DEFAULT_MESSAGES_ROOT]}] }
71
86
  .to_h
72
87
 
73
88
  store_translations(mapped_data)
@@ -80,12 +95,23 @@ module Dry
80
95
  end
81
96
 
82
97
  # @api private
83
- def cache_key(predicate, options)
84
- if options[:locale]
85
- super
86
- else
87
- [*super, I18n.locale]
88
- end
98
+ def interpolatable_data(_key, _options, **data)
99
+ data
100
+ end
101
+
102
+ # @api private
103
+ def interpolate(key, options, **data)
104
+ text_key = "#{key}.text"
105
+
106
+ opts = {
107
+ locale: default_locale,
108
+ **options,
109
+ **data
110
+ }
111
+
112
+ resolved_key = key?(text_key, opts) ? text_key : key
113
+
114
+ t.(resolved_key, **opts)
89
115
  end
90
116
 
91
117
  private
@@ -24,7 +24,7 @@ module Dry
24
24
  @config = messages.config
25
25
  @namespace = namespace
26
26
  @messages = messages
27
- @call_opts = { namespace: namespace }.freeze
27
+ @call_opts = {namespace: namespace}.freeze
28
28
  end
29
29
 
30
30
  # Get a message for the given key and its options
@@ -62,13 +62,23 @@ module Dry
62
62
  # @api private
63
63
  def rule_lookup_paths(tokens)
64
64
  base_paths = messages.rule_lookup_paths(tokens)
65
- base_paths.map { |key| key.gsub('dry_schema', "dry_schema.#{namespace}") } + base_paths
65
+ base_paths.map { |key| key.gsub("dry_schema", "dry_schema.#{namespace}") } + base_paths
66
66
  end
67
67
 
68
68
  # @api private
69
69
  def cache_key(predicate, options)
70
70
  messages.cache_key(predicate, options)
71
71
  end
72
+
73
+ # @api private
74
+ def interpolatable_data(key, options, **data)
75
+ messages.interpolatable_data(key, options, **data)
76
+ end
77
+
78
+ # @api private
79
+ def interpolate(key, options, **data)
80
+ messages.interpolate(key, options, **data)
81
+ end
72
82
  end
73
83
  end
74
84
  end
@@ -1,67 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/equalizer'
3
+ require "dry/initializer"
4
+ require "dry/equalizer"
4
5
 
5
- require 'dry/schema/constants'
6
+ require "dry/schema/constants"
6
7
 
7
8
  module Dry
8
9
  module Schema
9
10
  module Messages
10
- # Template wraps a string with interpolation tokens and defines evaluator function
11
- # dynamically
12
- #
13
11
  # @api private
14
12
  class Template
15
- include Dry::Equalizer(:text)
13
+ extend Dry::Initializer
14
+ include Dry::Equalizer(:messages, :key, :options)
16
15
 
17
- TOKEN_REGEXP = /%{(\w*)}/
18
-
19
- # !@attribute [r] text
20
- # @return [String]
21
- attr_reader :text
22
-
23
- # !@attribute [r] tokens
24
- # @return [Hash]
25
- attr_reader :tokens
26
-
27
- # !@attribute [r] evaluator
28
- # @return [Proc]
29
- attr_reader :evaluator
16
+ option :messages
17
+ option :key
18
+ option :options
30
19
 
31
20
  # @api private
32
- def self.[](input)
33
- new(*parse(input))
21
+ def data(data = EMPTY_HASH)
22
+ ensure_message!
23
+ messages.interpolatable_data(key, options, **options, **data)
34
24
  end
35
25
 
36
26
  # @api private
37
- def self.parse(input)
38
- tokens = input.scan(TOKEN_REGEXP).flatten(1).map(&:to_sym)
39
- text = input.gsub('%', '#')
40
-
41
- evaluator = <<-RUBY.strip
42
- -> (#{tokens.map { |token| "#{token}:" }.join(", ")}) { "#{text}" }
43
- RUBY
44
-
45
- [text, tokens, eval(evaluator, binding, __FILE__, __LINE__ - 3)]
27
+ def call(data = EMPTY_HASH)
28
+ ensure_message!
29
+ messages.interpolate(key, options, **data)
46
30
  end
31
+ alias_method :[], :call
47
32
 
48
- # @api private
49
- def initialize(text, tokens, evaluator)
50
- @text = text
51
- @tokens = tokens
52
- @evaluator = evaluator
53
- end
33
+ private
54
34
 
55
- # @api private
56
- def data(input)
57
- tokens.each_with_object({}) { |k, h| h[k] = input[k] }
58
- end
35
+ def ensure_message!
36
+ return if messages.key?(key, options)
59
37
 
60
- # @api private
61
- def call(data = EMPTY_HASH)
62
- data.empty? ? evaluator.() : evaluator.(data)
38
+ raise KeyError, "No message found for template, template=#{inspect}"
63
39
  end
64
- alias_method :[], :call
65
40
  end
66
41
  end
67
42
  end