dry-schema 1.3.4 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +253 -101
  3. data/LICENSE +1 -1
  4. data/README.md +6 -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 +4 -4
  10. data/lib/dry/schema/config.rb +15 -6
  11. data/lib/dry/schema/constants.rb +19 -9
  12. data/lib/dry/schema/dsl.rb +144 -38
  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 +66 -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 +53 -21
  30. data/lib/dry/schema/macros/each.rb +4 -4
  31. data/lib/dry/schema/macros/filled.rb +5 -6
  32. data/lib/dry/schema/macros/hash.rb +21 -3
  33. data/lib/dry/schema/macros/key.rb +10 -10
  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 +40 -19
  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 +79 -66
  49. data/lib/dry/schema/messages/i18n.rb +36 -10
  50. data/lib/dry/schema/messages/namespaced.rb +13 -3
  51. data/lib/dry/schema/messages/template.rb +19 -44
  52. data/lib/dry/schema/messages/yaml.rb +72 -13
  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 +2 -2
  56. data/lib/dry/schema/predicate_inferrer.rb +4 -184
  57. data/lib/dry/schema/predicate_registry.rb +3 -24
  58. data/lib/dry/schema/primitive_inferrer.rb +3 -86
  59. data/lib/dry/schema/processor.rb +54 -50
  60. data/lib/dry/schema/processor_steps.rb +139 -0
  61. data/lib/dry/schema/result.rb +52 -5
  62. data/lib/dry/schema/rule_applier.rb +8 -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,45 +87,56 @@ 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
 
102
- # Try to find a message for the given predicate and its options
110
+ # Check if given key is defined
103
111
  #
104
- # @api private
112
+ # @return [Boolean]
105
113
  #
106
- # rubocop:disable Metrics/AbcSize
107
- def lookup(predicate, options)
108
- tokens = options.merge(
109
- predicate: predicate,
110
- root: options[:not] ? "#{root}.not" : root,
111
- arg_type: config.arg_types[options[:arg_type]],
112
- val_type: config.val_types[options[:val_type]],
113
- message_type: options[:message_type] || :failure
114
- )
115
-
116
- opts = options.reject { |k, _| config.lookup_options.include?(k) }
117
-
118
- path = lookup_paths(tokens).detect { |key| key?(key, opts) }
119
-
120
- return unless path
114
+ # @api public
115
+ def key?(_key, _options = EMPTY_HASH)
116
+ raise NotImplementedError
117
+ end
121
118
 
122
- text = get(path, opts)
119
+ # Retrieve an array of looked up paths
120
+ #
121
+ # @param [Symbol] predicate
122
+ # @param [Hash] options
123
+ #
124
+ # @return [String]
125
+ #
126
+ # @api public
127
+ def looked_up_paths(predicate, options)
128
+ tokens = lookup_tokens(predicate, options)
129
+ filled_lookup_paths(tokens)
130
+ end
123
131
 
124
- if text.is_a?(Hash)
125
- text.values_at(:text, :meta)
126
- else
127
- [text, EMPTY_HASH]
128
- end
132
+ # @api private
133
+ def lookup_paths(predicate, options)
134
+ tokens = lookup_tokens(predicate, options)
135
+ filled_lookup_paths(tokens)
129
136
  end
130
- # rubocop:enable Metrics/AbcSize
131
137
 
132
138
  # @api private
133
- def lookup_paths(tokens)
139
+ def filled_lookup_paths(tokens)
134
140
  config.lookup_paths.map { |path| path % tokens }
135
141
  end
136
142
 
@@ -158,26 +164,33 @@ module Dry
158
164
  end
159
165
 
160
166
  # @api private
161
- def cache
162
- @cache ||= self.class.cache[self]
167
+ def default_locale
168
+ config.default_locale
163
169
  end
164
170
 
165
171
  # @api private
166
- def default_locale
167
- config.default_locale
172
+ def interpolatable_data(_key, _options, **_data)
173
+ raise NotImplementedError
168
174
  end
169
175
 
170
176
  # @api private
171
- def cache_key(predicate, options)
172
- if options.key?(:input)
173
- [predicate, options.reject { |k,| k.equal?(:input) }]
174
- else
175
- [predicate, options]
176
- end
177
+ def interpolate(_key, _options, **_data)
178
+ raise NotImplementedError
177
179
  end
178
180
 
179
181
  private
180
182
 
183
+ # @api private
184
+ def lookup_tokens(predicate, options)
185
+ options.merge(
186
+ predicate: predicate,
187
+ root: options[:not] ? "#{root}.not" : root,
188
+ arg_type: config.arg_types[options[:arg_type]],
189
+ val_type: config.val_types[options[:val_type]],
190
+ message_type: options[:message_type] || :failure
191
+ )
192
+ end
193
+
181
194
  # @api private
182
195
  def custom_top_namespace?(path)
183
196
  path.to_s == DEFAULT_MESSAGES_PATH.to_s && config.top_namespace != DEFAULT_MESSAGES_ROOT
@@ -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
@@ -55,20 +55,30 @@ module Dry
55
55
  end
56
56
 
57
57
  # @api private
58
- def lookup_paths(tokens)
58
+ def filled_lookup_paths(tokens)
59
59
  super(tokens.merge(root: "#{tokens[:root]}.#{namespace}")) + super
60
60
  end
61
61
 
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