dry-schema 1.3.2 → 1.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +178 -108
- data/LICENSE +1 -1
- data/README.md +2 -2
- data/lib/dry/schema.rb +1 -1
- data/lib/dry/schema/constants.rb +3 -2
- data/lib/dry/schema/dsl.rb +65 -20
- data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +1 -1
- data/lib/dry/schema/key.rb +4 -4
- data/lib/dry/schema/macros/core.rb +2 -2
- data/lib/dry/schema/macros/dsl.rb +31 -8
- data/lib/dry/schema/macros/filled.rb +0 -7
- data/lib/dry/schema/macros/key.rb +2 -2
- data/lib/dry/schema/macros/schema.rb +2 -7
- data/lib/dry/schema/macros/value.rb +21 -2
- data/lib/dry/schema/message_compiler.rb +6 -5
- data/lib/dry/schema/messages/abstract.rb +43 -19
- data/lib/dry/schema/messages/i18n.rb +34 -1
- data/lib/dry/schema/messages/namespaced.rb +6 -1
- data/lib/dry/schema/messages/template.rb +1 -1
- data/lib/dry/schema/messages/yaml.rb +12 -0
- data/lib/dry/schema/namespaced_rule.rb +2 -1
- data/lib/dry/schema/predicate_registry.rb +2 -23
- data/lib/dry/schema/processor.rb +10 -27
- data/lib/dry/schema/processor_steps.rb +116 -0
- data/lib/dry/schema/result.rb +1 -1
- data/lib/dry/schema/type_registry.rb +2 -2
- data/lib/dry/schema/version.rb +1 -1
- metadata +5 -5
- data/lib/dry/schema/primitive_inferrer.rb +0 -98
@@ -41,7 +41,7 @@ module Dry
|
|
41
41
|
attr_reader :default_lookup_options
|
42
42
|
|
43
43
|
# @api private
|
44
|
-
def initialize(messages, options
|
44
|
+
def initialize(messages, **options)
|
45
45
|
super
|
46
46
|
@options = options
|
47
47
|
@default_lookup_options = options[:locale] ? { locale: locale } : EMPTY_HASH
|
@@ -55,7 +55,7 @@ module Dry
|
|
55
55
|
|
56
56
|
return self if updated_opts.eql?(options)
|
57
57
|
|
58
|
-
self.class.new(messages, updated_opts)
|
58
|
+
self.class.new(messages, **updated_opts)
|
59
59
|
end
|
60
60
|
|
61
61
|
# @api private
|
@@ -105,7 +105,7 @@ module Dry
|
|
105
105
|
left, right = node.map { |n| visit(n, opts) }
|
106
106
|
|
107
107
|
if [left, right].flatten.map(&:path).uniq.size == 1
|
108
|
-
Message::Or.new(left, right, proc { |k| messages.translate(k, default_lookup_options) })
|
108
|
+
Message::Or.new(left, right, proc { |k| messages.translate(k, **default_lookup_options) })
|
109
109
|
elsif right.is_a?(Array)
|
110
110
|
right
|
111
111
|
else
|
@@ -116,7 +116,7 @@ module Dry
|
|
116
116
|
# @api private
|
117
117
|
def visit_namespace(node, opts)
|
118
118
|
ns, rest = node
|
119
|
-
self.class.new(messages.namespaced(ns), options).visit(rest, opts)
|
119
|
+
self.class.new(messages.namespaced(ns), **options).visit(rest, opts)
|
120
120
|
end
|
121
121
|
|
122
122
|
# @api private
|
@@ -130,7 +130,8 @@ module Dry
|
|
130
130
|
path: path.last, **tokens, **lookup_options(arg_vals: arg_vals, input: input)
|
131
131
|
).to_h
|
132
132
|
|
133
|
-
template, meta = messages[predicate, options] ||
|
133
|
+
template, meta = messages[predicate, options] ||
|
134
|
+
raise(MissingMessageError.new(path, messages.looked_up_paths(predicate, options)))
|
134
135
|
|
135
136
|
text = message_text(template, tokens, options)
|
136
137
|
|
@@ -92,45 +92,49 @@ module Dry
|
|
92
92
|
#
|
93
93
|
# @api public
|
94
94
|
def call(predicate, options)
|
95
|
-
cache.fetch_or_store(
|
95
|
+
cache.fetch_or_store(cache_key(predicate, options)) do
|
96
96
|
text, meta = lookup(predicate, options)
|
97
97
|
[Template[text], meta] if text
|
98
98
|
end
|
99
99
|
end
|
100
100
|
alias_method :[], :call
|
101
101
|
|
102
|
+
# Retrieve an array of looked up paths
|
103
|
+
#
|
104
|
+
# @param [Symbol] predicate
|
105
|
+
# @param [Hash] options
|
106
|
+
#
|
107
|
+
# @return [String]
|
108
|
+
#
|
109
|
+
# @api public
|
110
|
+
def looked_up_paths(predicate, options)
|
111
|
+
tokens = lookup_tokens(predicate, options)
|
112
|
+
filled_lookup_paths(tokens)
|
113
|
+
end
|
114
|
+
|
102
115
|
# Try to find a message for the given predicate and its options
|
103
116
|
#
|
104
117
|
# @api private
|
105
118
|
#
|
106
119
|
# rubocop:disable Metrics/AbcSize
|
107
120
|
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
121
|
opts = options.reject { |k, _| config.lookup_options.include?(k) }
|
117
|
-
|
118
|
-
path = lookup_paths(tokens).detect { |key| key?(key, opts) }
|
122
|
+
path = lookup_paths(predicate, options).detect { |key| key?(key, opts) }
|
119
123
|
|
120
124
|
return unless path
|
121
125
|
|
122
|
-
|
123
|
-
|
124
|
-
if text.is_a?(Hash)
|
125
|
-
text.values_at(:text, :meta)
|
126
|
-
else
|
127
|
-
[text, EMPTY_HASH]
|
128
|
-
end
|
126
|
+
get(path, opts).values_at(:text, :meta)
|
129
127
|
end
|
130
128
|
# rubocop:enable Metrics/AbcSize
|
131
129
|
|
132
130
|
# @api private
|
133
|
-
def lookup_paths(
|
131
|
+
def lookup_paths(predicate, options)
|
132
|
+
tokens = lookup_tokens(predicate, options)
|
133
|
+
filled_lookup_paths(tokens)
|
134
|
+
end
|
135
|
+
|
136
|
+
# @api private
|
137
|
+
def filled_lookup_paths(tokens)
|
134
138
|
config.lookup_paths.map { |path| path % tokens }
|
135
139
|
end
|
136
140
|
|
@@ -167,8 +171,28 @@ module Dry
|
|
167
171
|
config.default_locale
|
168
172
|
end
|
169
173
|
|
174
|
+
# @api private
|
175
|
+
def cache_key(predicate, options)
|
176
|
+
if options.key?(:input)
|
177
|
+
[predicate, options.reject { |k,| k.equal?(:input) }]
|
178
|
+
else
|
179
|
+
[predicate, options]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
170
183
|
private
|
171
184
|
|
185
|
+
# @api private
|
186
|
+
def lookup_tokens(predicate, options)
|
187
|
+
options.merge(
|
188
|
+
predicate: predicate,
|
189
|
+
root: options[:not] ? "#{root}.not" : root,
|
190
|
+
arg_type: config.arg_types[options[:arg_type]],
|
191
|
+
val_type: config.val_types[options[:val_type]],
|
192
|
+
message_type: options[:message_type] || :failure
|
193
|
+
)
|
194
|
+
end
|
195
|
+
|
172
196
|
# @api private
|
173
197
|
def custom_top_namespace?(path)
|
174
198
|
path.to_s == DEFAULT_MESSAGES_PATH.to_s && config.top_namespace != DEFAULT_MESSAGES_ROOT
|
@@ -29,7 +29,24 @@ module Dry
|
|
29
29
|
#
|
30
30
|
# @api public
|
31
31
|
def get(key, options = EMPTY_HASH)
|
32
|
-
|
32
|
+
return unless key
|
33
|
+
|
34
|
+
opts = { locale: options.fetch(:locale, default_locale) }
|
35
|
+
|
36
|
+
translation = t.(key, opts)
|
37
|
+
text_key = "#{key}.text"
|
38
|
+
|
39
|
+
if !translation.is_a?(Hash) || !key?(text_key, opts)
|
40
|
+
return {
|
41
|
+
text: translation,
|
42
|
+
meta: EMPTY_HASH
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
{
|
47
|
+
text: t.(text_key, opts),
|
48
|
+
meta: extract_meta(key, translation, opts)
|
49
|
+
}
|
33
50
|
end
|
34
51
|
|
35
52
|
# Check if given key is defined
|
@@ -79,6 +96,15 @@ module Dry
|
|
79
96
|
self
|
80
97
|
end
|
81
98
|
|
99
|
+
# @api private
|
100
|
+
def cache_key(predicate, options)
|
101
|
+
if options[:locale]
|
102
|
+
super
|
103
|
+
else
|
104
|
+
[*super, I18n.locale]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
82
108
|
private
|
83
109
|
|
84
110
|
# @api private
|
@@ -91,6 +117,13 @@ module Dry
|
|
91
117
|
I18n.backend.store_translations(locale, data[locale.to_s])
|
92
118
|
end
|
93
119
|
end
|
120
|
+
|
121
|
+
def extract_meta(parent_key, translation, options)
|
122
|
+
translation.keys.each_with_object({}) do |k, meta|
|
123
|
+
meta_key = "#{parent_key}.#{k}"
|
124
|
+
meta[k] = t.(meta_key, options) if k != :text && key?(meta_key, options)
|
125
|
+
end
|
126
|
+
end
|
94
127
|
end
|
95
128
|
end
|
96
129
|
end
|
@@ -55,7 +55,7 @@ module Dry
|
|
55
55
|
end
|
56
56
|
|
57
57
|
# @api private
|
58
|
-
def
|
58
|
+
def filled_lookup_paths(tokens)
|
59
59
|
super(tokens.merge(root: "#{tokens[:root]}.#{namespace}")) + super
|
60
60
|
end
|
61
61
|
|
@@ -64,6 +64,11 @@ module Dry
|
|
64
64
|
base_paths = messages.rule_lookup_paths(tokens)
|
65
65
|
base_paths.map { |key| key.gsub('dry_schema', "dry_schema.#{namespace}") } + base_paths
|
66
66
|
end
|
67
|
+
|
68
|
+
# @api private
|
69
|
+
def cache_key(predicate, options)
|
70
|
+
messages.cache_key(predicate, options)
|
71
|
+
end
|
67
72
|
end
|
68
73
|
end
|
69
74
|
end
|
@@ -68,6 +68,18 @@ module Dry
|
|
68
68
|
@t = proc { |key, locale: default_locale| get("%<locale>s.#{key}", locale: locale) }
|
69
69
|
end
|
70
70
|
|
71
|
+
# Get an array of looked up paths
|
72
|
+
#
|
73
|
+
# @param [Symbol] predicate
|
74
|
+
# @param [Hash] options
|
75
|
+
#
|
76
|
+
# @return [String]
|
77
|
+
#
|
78
|
+
# @api public
|
79
|
+
def looked_up_paths(predicate, options)
|
80
|
+
super.map { |path| path % { locale: options[:locale] || default_locale } }
|
81
|
+
end
|
82
|
+
|
71
83
|
# Get a message for the given key and its options
|
72
84
|
#
|
73
85
|
# @param [Symbol] key
|
@@ -1,35 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'dry/logic/predicates'
|
4
|
+
require 'dry/types/predicate_registry'
|
4
5
|
|
5
6
|
module Dry
|
6
7
|
module Schema
|
7
8
|
# A registry with predicate objects from `Dry::Logic::Predicates`
|
8
9
|
#
|
9
10
|
# @api private
|
10
|
-
class PredicateRegistry
|
11
|
-
# @api private
|
12
|
-
attr_reader :predicates
|
13
|
-
|
14
|
-
# @api private
|
15
|
-
attr_reader :has_predicate
|
16
|
-
|
17
|
-
# @api private
|
18
|
-
def initialize(predicates = Dry::Logic::Predicates)
|
19
|
-
@predicates = predicates
|
20
|
-
@has_predicate = ::Kernel.instance_method(:respond_to?).bind(@predicates)
|
21
|
-
end
|
22
|
-
|
23
|
-
# @api private
|
24
|
-
def [](name)
|
25
|
-
predicates[name]
|
26
|
-
end
|
27
|
-
|
28
|
-
# @api private
|
29
|
-
def key?(name)
|
30
|
-
has_predicate.(name)
|
31
|
-
end
|
32
|
-
|
11
|
+
class PredicateRegistry < Dry::Types::PredicateRegistry
|
33
12
|
# @api private
|
34
13
|
def arg_list(name, *values)
|
35
14
|
predicate = self[name]
|
data/lib/dry/schema/processor.rb
CHANGED
@@ -5,6 +5,7 @@ require 'dry/initializer'
|
|
5
5
|
|
6
6
|
require 'dry/schema/type_registry'
|
7
7
|
require 'dry/schema/type_container'
|
8
|
+
require 'dry/schema/processor_steps'
|
8
9
|
require 'dry/schema/rule_applier'
|
9
10
|
require 'dry/schema/key_coercer'
|
10
11
|
require 'dry/schema/value_coercer'
|
@@ -12,14 +13,9 @@ require 'dry/schema/value_coercer'
|
|
12
13
|
module Dry
|
13
14
|
module Schema
|
14
15
|
# Processes input data using objects configured within the DSL
|
16
|
+
# Processing is split into steps represented by `ProcessorSteps`.
|
15
17
|
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# 1. Prepare input hash using a key map
|
19
|
-
# 2. Apply pre-coercion filtering rules (optional step, used only when `filter` was used)
|
20
|
-
# 3. Apply value coercions based on type specifications
|
21
|
-
# 4. Apply rules
|
22
|
-
#
|
18
|
+
# @see ProcessorSteps
|
23
19
|
# @see Params
|
24
20
|
# @see JSON
|
25
21
|
#
|
@@ -29,10 +25,10 @@ module Dry
|
|
29
25
|
extend Dry::Configurable
|
30
26
|
|
31
27
|
setting :key_map_type
|
32
|
-
setting :type_registry_namespace, :
|
28
|
+
setting :type_registry_namespace, :strict
|
33
29
|
setting :filter_empty_string, false
|
34
30
|
|
35
|
-
option :steps, default: -> {
|
31
|
+
option :steps, default: -> { ProcessorSteps.new }
|
36
32
|
|
37
33
|
option :schema_dsl
|
38
34
|
|
@@ -66,7 +62,7 @@ module Dry
|
|
66
62
|
# @api public
|
67
63
|
def new(options = nil, &block)
|
68
64
|
if options || block
|
69
|
-
processor = super
|
65
|
+
processor = super(**(options || EMPTY_HASH))
|
70
66
|
yield(processor) if block
|
71
67
|
processor
|
72
68
|
elsif definition
|
@@ -77,16 +73,6 @@ module Dry
|
|
77
73
|
end
|
78
74
|
end
|
79
75
|
|
80
|
-
# Append a step
|
81
|
-
#
|
82
|
-
# @return [Processor]
|
83
|
-
#
|
84
|
-
# @api private
|
85
|
-
def <<(step)
|
86
|
-
steps << step
|
87
|
-
self
|
88
|
-
end
|
89
|
-
|
90
76
|
# Apply processing steps to the provided input
|
91
77
|
#
|
92
78
|
# @param [Hash] input
|
@@ -96,10 +82,7 @@ module Dry
|
|
96
82
|
# @api public
|
97
83
|
def call(input)
|
98
84
|
Result.new(input, message_compiler: message_compiler) do |result|
|
99
|
-
steps.
|
100
|
-
output = step.(result)
|
101
|
-
result.replace(output) if output.is_a?(::Hash)
|
102
|
-
end
|
85
|
+
steps.call(result)
|
103
86
|
end
|
104
87
|
end
|
105
88
|
alias_method :[], :call
|
@@ -119,7 +102,7 @@ module Dry
|
|
119
102
|
#
|
120
103
|
# @api public
|
121
104
|
def key_map
|
122
|
-
|
105
|
+
steps[:key_coercer].key_map
|
123
106
|
end
|
124
107
|
|
125
108
|
# Return string represntation
|
@@ -139,7 +122,7 @@ module Dry
|
|
139
122
|
#
|
140
123
|
# @api private
|
141
124
|
def type_schema
|
142
|
-
|
125
|
+
steps[:value_coercer].type_schema
|
143
126
|
end
|
144
127
|
|
145
128
|
# Return the rules config
|
@@ -180,7 +163,7 @@ module Dry
|
|
180
163
|
#
|
181
164
|
# @api private
|
182
165
|
def rule_applier
|
183
|
-
|
166
|
+
steps[:rule_applier]
|
184
167
|
end
|
185
168
|
alias_method :to_rule, :rule_applier
|
186
169
|
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/initializer'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Schema
|
7
|
+
# Steps for the Dry::Schema::Processor
|
8
|
+
#
|
9
|
+
# There are 4 main steps:
|
10
|
+
#
|
11
|
+
# 1. `key_coercer` - Prepare input hash using a key map
|
12
|
+
# 2. `filter_schema` - Apply pre-coercion filtering rules
|
13
|
+
# (optional step, used only when `filter` was used)
|
14
|
+
# 3. `value_coercer` - Apply value coercions based on type specifications
|
15
|
+
# 4. `rule_applier` - Apply rules
|
16
|
+
#
|
17
|
+
# @see Processor
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
class ProcessorSteps
|
21
|
+
extend Dry::Initializer
|
22
|
+
|
23
|
+
STEPS_IN_ORDER = %i[key_coercer filter_schema value_coercer rule_applier].freeze
|
24
|
+
|
25
|
+
option :steps, default: -> { EMPTY_HASH.dup }
|
26
|
+
option :before_steps, default: -> { EMPTY_HASH.dup }
|
27
|
+
option :after_steps, default: -> { EMPTY_HASH.dup }
|
28
|
+
|
29
|
+
# Executes steps and callbacks in order
|
30
|
+
#
|
31
|
+
# @param [Result] result
|
32
|
+
#
|
33
|
+
# @return [Result]
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def call(result)
|
37
|
+
STEPS_IN_ORDER.each do |name|
|
38
|
+
before_steps[name]&.each { |step| process_step(step, result) }
|
39
|
+
process_step(steps[name], result)
|
40
|
+
after_steps[name]&.each { |step| process_step(step, result) }
|
41
|
+
end
|
42
|
+
result
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns step by name
|
46
|
+
#
|
47
|
+
# @param [Symbol] name The step name
|
48
|
+
#
|
49
|
+
# @api public
|
50
|
+
def [](name)
|
51
|
+
steps[name]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Sets step by name
|
55
|
+
#
|
56
|
+
# @param [Symbol] name The step name
|
57
|
+
#
|
58
|
+
# @api public
|
59
|
+
def []=(name, value)
|
60
|
+
validate_step_name(name)
|
61
|
+
steps[name] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
# Add passed block before mentioned step
|
65
|
+
#
|
66
|
+
# @param [Symbol] name The step name
|
67
|
+
#
|
68
|
+
# @return [ProcessorSteps]
|
69
|
+
#
|
70
|
+
# @api public
|
71
|
+
def after(name, &block)
|
72
|
+
validate_step_name(name)
|
73
|
+
after_steps[name] ||= EMPTY_ARRAY.dup
|
74
|
+
after_steps[name] << block.to_proc
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
# Add passed block before mentioned step
|
79
|
+
#
|
80
|
+
# @param [Symbol] name The step name
|
81
|
+
#
|
82
|
+
# @return [ProcessorSteps]
|
83
|
+
#
|
84
|
+
# @api public
|
85
|
+
def before(name, &block)
|
86
|
+
validate_step_name(name)
|
87
|
+
before_steps[name] ||= EMPTY_ARRAY.dup
|
88
|
+
before_steps[name] << block.to_proc
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
# @api private
|
93
|
+
def process_step(step, result)
|
94
|
+
return unless step
|
95
|
+
|
96
|
+
output = step.(result)
|
97
|
+
result.replace(output) if output.is_a?(::Hash)
|
98
|
+
end
|
99
|
+
|
100
|
+
# @api private
|
101
|
+
def validate_step_name(name)
|
102
|
+
return if STEPS_IN_ORDER.include?(name)
|
103
|
+
|
104
|
+
raise ArgumentError, "Undefined step name #{name}. Available names: #{STEPS_IN_ORDER}"
|
105
|
+
end
|
106
|
+
|
107
|
+
# @api private
|
108
|
+
def initialize_copy(source)
|
109
|
+
super
|
110
|
+
@steps = source.steps.dup
|
111
|
+
@before_steps = source.before_steps.dup
|
112
|
+
@after_steps = source.after_steps.dup
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|