dry-schema 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/config/errors.yml +1 -1
- data/lib/dry/schema.rb +0 -9
- data/lib/dry/schema/config.rb +21 -27
- data/lib/dry/schema/constants.rb +17 -3
- data/lib/dry/schema/dsl.rb +13 -22
- data/lib/dry/schema/extensions/hints/message_set_methods.rb +2 -7
- data/lib/dry/schema/extensions/hints/result_methods.rb +3 -3
- data/lib/dry/schema/extensions/monads.rb +1 -1
- data/lib/dry/schema/macros/hash.rb +0 -1
- data/lib/dry/schema/macros/key.rb +13 -8
- data/lib/dry/schema/macros/optional.rb +5 -0
- data/lib/dry/schema/macros/schema.rb +1 -1
- data/lib/dry/schema/message.rb +3 -8
- data/lib/dry/schema/message_compiler.rb +47 -42
- data/lib/dry/schema/message_compiler/visitor_opts.rb +0 -1
- data/lib/dry/schema/message_set.rb +13 -1
- data/lib/dry/schema/messages.rb +16 -25
- data/lib/dry/schema/messages/abstract.rb +53 -36
- data/lib/dry/schema/messages/i18n.rb +40 -23
- data/lib/dry/schema/messages/namespaced.rb +3 -4
- data/lib/dry/schema/messages/yaml.rb +54 -33
- data/lib/dry/schema/predicate_inferrer.rb +56 -14
- data/lib/dry/schema/processor.rb +59 -40
- data/lib/dry/schema/result.rb +2 -2
- data/lib/dry/schema/rule_applier.rb +5 -4
- data/lib/dry/schema/trace.rb +11 -3
- data/lib/dry/schema/type_registry.rb +2 -2
- data/lib/dry/schema/types.rb +1 -1
- data/lib/dry/schema/value_coercer.rb +5 -3
- data/lib/dry/schema/version.rb +1 -1
- metadata +15 -15
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'dry/equalizer'
|
4
|
+
|
3
5
|
module Dry
|
4
6
|
module Schema
|
5
7
|
# A set of messages used to generate errors
|
@@ -9,6 +11,7 @@ module Dry
|
|
9
11
|
# @api public
|
10
12
|
class MessageSet
|
11
13
|
include Enumerable
|
14
|
+
include Dry::Equalizer(:messages, :options)
|
12
15
|
|
13
16
|
attr_reader :messages, :placeholders, :options
|
14
17
|
|
@@ -35,7 +38,16 @@ module Dry
|
|
35
38
|
messages_map
|
36
39
|
end
|
37
40
|
alias_method :to_hash, :to_h
|
38
|
-
|
41
|
+
|
42
|
+
# @api public
|
43
|
+
def [](key)
|
44
|
+
to_h[key]
|
45
|
+
end
|
46
|
+
|
47
|
+
# @api public
|
48
|
+
def fetch(key)
|
49
|
+
self[key] || raise(KeyError, "+#{key}+ message was not found")
|
50
|
+
end
|
39
51
|
|
40
52
|
# @api private
|
41
53
|
def empty?
|
data/lib/dry/schema/messages.rb
CHANGED
@@ -6,35 +6,26 @@ module Dry
|
|
6
6
|
#
|
7
7
|
# @api private
|
8
8
|
module Messages
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
messages.merge(config.messages_file)
|
16
|
-
elsif config.namespace
|
17
|
-
messages.namespaced(config.namespace)
|
18
|
-
else
|
19
|
-
messages
|
20
|
-
end
|
21
|
-
end
|
9
|
+
BACKENDS = {
|
10
|
+
i18n: 'I18n',
|
11
|
+
yaml: 'YAML'
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
module_function
|
22
15
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
when :yaml then default
|
27
|
-
when :i18n then Messages::I18n
|
28
|
-
else
|
29
|
-
raise "+#{config.messages}+ is not a valid messages identifier"
|
16
|
+
public def setup(config)
|
17
|
+
backend_class = BACKENDS.fetch(config.backend) do
|
18
|
+
raise "+#{config.backend}+ is not a valid messages identifier"
|
30
19
|
end
|
31
20
|
|
32
|
-
|
33
|
-
|
21
|
+
namespace = config.namespace
|
22
|
+
options = config.to_h.select { |k, _| Abstract.settings.include?(k) }
|
23
|
+
|
24
|
+
messages = Messages.const_get(backend_class).build(options)
|
25
|
+
|
26
|
+
return messages.namespaced(namespace) if namespace
|
34
27
|
|
35
|
-
|
36
|
-
def self.default
|
37
|
-
Messages::YAML
|
28
|
+
messages
|
38
29
|
end
|
39
30
|
end
|
40
31
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'set'
|
4
4
|
require 'concurrent/map'
|
5
5
|
require 'dry/equalizer'
|
6
6
|
require 'dry/configurable'
|
@@ -15,39 +15,33 @@ module Dry
|
|
15
15
|
#
|
16
16
|
# @api public
|
17
17
|
class Abstract
|
18
|
-
|
18
|
+
include Dry::Configurable
|
19
19
|
include Dry::Equalizer(:config)
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
setting :paths, [DEFAULT_PATH]
|
21
|
+
setting :load_paths, Set[DEFAULT_MESSAGES_PATH]
|
22
|
+
setting :top_namespace, DEFAULT_MESSAGES_ROOT
|
24
23
|
setting :root, 'errors'
|
25
|
-
setting :lookup_options, [
|
26
|
-
|
27
|
-
setting :lookup_paths,
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
setting :rule_lookup_paths,
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
setting :arg_type_default, 'default'
|
44
|
-
setting :val_type_default, 'default'
|
45
|
-
|
46
|
-
setting :arg_types, Hash.new { |*| config.arg_type_default }.update(
|
24
|
+
setting :lookup_options, %i[root predicate path val_type arg_type].freeze
|
25
|
+
|
26
|
+
setting :lookup_paths, [
|
27
|
+
'%<root>s.rules.%<path>s.%<predicate>s.arg.%<arg_type>s',
|
28
|
+
'%<root>s.rules.%<path>s.%<predicate>s',
|
29
|
+
'%<root>s.%<predicate>s.%<message_type>s',
|
30
|
+
'%<root>s.%<predicate>s.value.%<path>s.arg.%<arg_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
|
+
].freeze
|
37
|
+
|
38
|
+
setting :rule_lookup_paths, ['rules.%<name>s'].freeze
|
39
|
+
|
40
|
+
setting :arg_types, Hash.new { |*| 'default' }.update(
|
47
41
|
Range => 'range'
|
48
42
|
)
|
49
43
|
|
50
|
-
setting :val_types, Hash.new { |*|
|
44
|
+
setting :val_types, Hash.new { |*| 'default' }.update(
|
51
45
|
Range => 'range',
|
52
46
|
String => 'string'
|
53
47
|
)
|
@@ -58,11 +52,24 @@ module Dry
|
|
58
52
|
end
|
59
53
|
|
60
54
|
# @api private
|
61
|
-
|
55
|
+
def self.build(options = EMPTY_HASH)
|
56
|
+
messages = new
|
62
57
|
|
63
|
-
|
64
|
-
|
65
|
-
|
58
|
+
messages.configure do |config|
|
59
|
+
options.each do |key, value|
|
60
|
+
config.public_send(:"#{key}=", value)
|
61
|
+
end
|
62
|
+
|
63
|
+
config.root = "#{config.top_namespace}.#{config.root}"
|
64
|
+
|
65
|
+
config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
|
66
|
+
"#{config.top_namespace}.#{path}"
|
67
|
+
}
|
68
|
+
|
69
|
+
yield(config) if block_given?
|
70
|
+
end
|
71
|
+
|
72
|
+
messages.prepare
|
66
73
|
end
|
67
74
|
|
68
75
|
# @api private
|
@@ -70,6 +77,11 @@ module Dry
|
|
70
77
|
@hash ||= config.hash
|
71
78
|
end
|
72
79
|
|
80
|
+
# @api private
|
81
|
+
def translate(key, locale: default_locale)
|
82
|
+
t["#{config.top_namespace}.#{key}", locale: locale]
|
83
|
+
end
|
84
|
+
|
73
85
|
# @api private
|
74
86
|
def rule(name, options = {})
|
75
87
|
tokens = { name: name, locale: options.fetch(:locale, default_locale) }
|
@@ -103,9 +115,7 @@ module Dry
|
|
103
115
|
message_type: options[:message_type] || :failure
|
104
116
|
)
|
105
117
|
|
106
|
-
|
107
|
-
|
108
|
-
opts = options.select { |k, _| !config.lookup_options.include?(k) }
|
118
|
+
opts = options.reject { |k, _| config.lookup_options.include?(k) }
|
109
119
|
|
110
120
|
path = lookup_paths(tokens).detect do |key|
|
111
121
|
key?(key, opts) && get(key, opts).is_a?(String)
|
@@ -130,7 +140,7 @@ module Dry
|
|
130
140
|
#
|
131
141
|
# @api public
|
132
142
|
def namespaced(namespace)
|
133
|
-
|
143
|
+
Dry::Schema::Messages::Namespaced.new(namespace, self)
|
134
144
|
end
|
135
145
|
|
136
146
|
# Return root path to messages file
|
@@ -151,6 +161,13 @@ module Dry
|
|
151
161
|
def default_locale
|
152
162
|
:en
|
153
163
|
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
# @api private
|
168
|
+
def custom_top_namespace?(path)
|
169
|
+
path.to_s == DEFAULT_MESSAGES_PATH.to_s && config.top_namespace != DEFAULT_MESSAGES_ROOT
|
170
|
+
end
|
154
171
|
end
|
155
172
|
end
|
156
173
|
end
|
@@ -11,22 +11,6 @@ module Dry
|
|
11
11
|
class Messages::I18n < Messages::Abstract
|
12
12
|
attr_reader :t
|
13
13
|
|
14
|
-
configure do |config|
|
15
|
-
config.root = 'dry_schema.errors'
|
16
|
-
config.rule_lookup_paths = config.rule_lookup_paths.map { |path| "dry_schema.#{path}" }
|
17
|
-
end
|
18
|
-
|
19
|
-
# @api private
|
20
|
-
def self.build(paths = config.paths)
|
21
|
-
set_load_paths(paths)
|
22
|
-
new
|
23
|
-
end
|
24
|
-
|
25
|
-
# @api private
|
26
|
-
def self.set_load_paths(paths)
|
27
|
-
::I18n.load_path.concat(paths)
|
28
|
-
end
|
29
|
-
|
30
14
|
# @api private
|
31
15
|
def initialize
|
32
16
|
super
|
@@ -41,7 +25,7 @@ module Dry
|
|
41
25
|
# @return [String]
|
42
26
|
#
|
43
27
|
# @api public
|
44
|
-
def get(key, options =
|
28
|
+
def get(key, options = EMPTY_HASH)
|
45
29
|
t.(key, options) if key
|
46
30
|
end
|
47
31
|
|
@@ -51,26 +35,59 @@ module Dry
|
|
51
35
|
#
|
52
36
|
# @api public
|
53
37
|
def key?(key, options)
|
54
|
-
|
55
|
-
|
38
|
+
I18n.exists?(key, options.fetch(:locale, default_locale)) ||
|
39
|
+
I18n.exists?(key, I18n.default_locale)
|
56
40
|
end
|
57
41
|
|
58
42
|
# Merge messages from an additional path
|
59
43
|
#
|
60
|
-
# @param [String]
|
44
|
+
# @param [String, Array<String>] paths
|
61
45
|
#
|
62
46
|
# @return [Messages::I18n]
|
63
47
|
#
|
64
48
|
# @api public
|
65
|
-
def merge(
|
66
|
-
|
67
|
-
self
|
49
|
+
def merge(paths)
|
50
|
+
prepare(paths)
|
68
51
|
end
|
69
52
|
|
70
53
|
# @api private
|
71
54
|
def default_locale
|
72
55
|
I18n.locale || I18n.default_locale || super
|
73
56
|
end
|
57
|
+
|
58
|
+
# @api private
|
59
|
+
def prepare(paths = config.load_paths)
|
60
|
+
paths.each do |path|
|
61
|
+
data = YAML.load_file(path)
|
62
|
+
|
63
|
+
if custom_top_namespace?(path)
|
64
|
+
top_namespace = config.top_namespace
|
65
|
+
|
66
|
+
mapped_data = data
|
67
|
+
.map { |k, v| [k, { top_namespace => v[DEFAULT_MESSAGES_ROOT] }] }
|
68
|
+
.to_h
|
69
|
+
|
70
|
+
store_translations(mapped_data)
|
71
|
+
else
|
72
|
+
store_translations(data)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# @api private
|
82
|
+
def store_translations(data)
|
83
|
+
locales = data.keys.map(&:to_sym)
|
84
|
+
|
85
|
+
I18n.available_locales += locales
|
86
|
+
|
87
|
+
locales.each do |locale|
|
88
|
+
I18n.backend.store_translations(locale, data[locale.to_s])
|
89
|
+
end
|
90
|
+
end
|
74
91
|
end
|
75
92
|
end
|
76
93
|
end
|
@@ -6,7 +6,7 @@ module Dry
|
|
6
6
|
# Namespaced messages backend
|
7
7
|
#
|
8
8
|
# @api public
|
9
|
-
class Namespaced <Dry::Schema::Messages::Abstract
|
9
|
+
class Namespaced < Dry::Schema::Messages::Abstract
|
10
10
|
# @api private
|
11
11
|
attr_reader :namespace
|
12
12
|
|
@@ -14,17 +14,16 @@ module Dry
|
|
14
14
|
attr_reader :messages
|
15
15
|
|
16
16
|
# @api private
|
17
|
-
attr_reader :
|
17
|
+
attr_reader :config
|
18
18
|
|
19
19
|
# @api private
|
20
20
|
attr_reader :call_opts
|
21
21
|
|
22
22
|
# @api private
|
23
23
|
def initialize(namespace, messages)
|
24
|
-
|
24
|
+
@config = messages.config
|
25
25
|
@namespace = namespace
|
26
26
|
@messages = messages
|
27
|
-
@root = messages.root
|
28
27
|
@call_opts = { namespace: namespace }.freeze
|
29
28
|
end
|
30
29
|
|
@@ -4,6 +4,7 @@ require 'yaml'
|
|
4
4
|
require 'pathname'
|
5
5
|
|
6
6
|
require 'dry/equalizer'
|
7
|
+
require 'dry/schema/constants'
|
7
8
|
require 'dry/schema/messages/abstract'
|
8
9
|
|
9
10
|
module Dry
|
@@ -12,39 +13,37 @@ module Dry
|
|
12
13
|
#
|
13
14
|
# @api public
|
14
15
|
class Messages::YAML < Messages::Abstract
|
16
|
+
LOCALE_TOKEN = '%<locale>s'
|
17
|
+
|
15
18
|
include Dry::Equalizer(:data)
|
16
19
|
|
17
|
-
attr_reader :data
|
20
|
+
attr_reader :data, :t
|
18
21
|
|
19
22
|
# @api private
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
"%{locale}.dry_schema.#{path}"
|
24
|
-
}
|
25
|
-
end
|
23
|
+
def self.build(options = EMPTY_HASH)
|
24
|
+
super do |config|
|
25
|
+
config.root = "%<locale>s.#{config.root}"
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
|
28
|
+
"%<locale>s.#{path}"
|
29
|
+
}
|
30
|
+
end
|
30
31
|
end
|
31
32
|
|
32
33
|
# @api private
|
33
|
-
def self.
|
34
|
-
|
35
|
-
end
|
34
|
+
def self.flat_hash(hash, acc = [], result = {})
|
35
|
+
return result.update(acc.join(DOT) => hash) unless hash.is_a?(Hash)
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
return g.update(f.join('.') => h) unless h.is_a? Hash
|
40
|
-
h.each { |k, r| flat_hash(r, f + [k], g) }
|
41
|
-
g
|
37
|
+
hash.each { |k, v| flat_hash(v, acc + [k], result) }
|
38
|
+
result
|
42
39
|
end
|
43
40
|
|
44
41
|
# @api private
|
45
|
-
def initialize(data)
|
42
|
+
def initialize(data: EMPTY_HASH, config: nil)
|
46
43
|
super()
|
47
44
|
@data = data
|
45
|
+
@config = config if config
|
46
|
+
@t = proc { |key, locale: default_locale| get("%<locale>s.#{key}", locale: locale) }
|
48
47
|
end
|
49
48
|
|
50
49
|
# Get a message for the given key and its options
|
@@ -55,12 +54,8 @@ module Dry
|
|
55
54
|
# @return [String]
|
56
55
|
#
|
57
56
|
# @api public
|
58
|
-
def get(key, options =
|
59
|
-
evaluated_key
|
60
|
-
key % { locale: options.fetch(:locale, default_locale) } :
|
61
|
-
key
|
62
|
-
|
63
|
-
data[evaluated_key]
|
57
|
+
def get(key, options = EMPTY_HASH)
|
58
|
+
data[evaluated_key(key, options)]
|
64
59
|
end
|
65
60
|
|
66
61
|
# Check if given key is defined
|
@@ -68,12 +63,8 @@ module Dry
|
|
68
63
|
# @return [Boolean]
|
69
64
|
#
|
70
65
|
# @api public
|
71
|
-
def key?(key, options =
|
72
|
-
|
73
|
-
key % { locale: options.fetch(:locale, default_locale) } :
|
74
|
-
key
|
75
|
-
|
76
|
-
data.key?(evaluated_key)
|
66
|
+
def key?(key, options = EMPTY_HASH)
|
67
|
+
data.key?(evaluated_key(key, options))
|
77
68
|
end
|
78
69
|
|
79
70
|
# Merge messages from an additional path
|
@@ -85,11 +76,41 @@ module Dry
|
|
85
76
|
# @api public
|
86
77
|
def merge(overrides)
|
87
78
|
if overrides.is_a?(Hash)
|
88
|
-
self.class.new(
|
79
|
+
self.class.new(
|
80
|
+
data: data.merge(self.class.flat_hash(overrides)),
|
81
|
+
config: config
|
82
|
+
)
|
89
83
|
else
|
90
|
-
self.class.new(
|
84
|
+
self.class.new(
|
85
|
+
data: Array(overrides).reduce(data) { |a, e| a.merge(load_translations(e)) },
|
86
|
+
config: config
|
87
|
+
)
|
91
88
|
end
|
92
89
|
end
|
90
|
+
|
91
|
+
# @api private
|
92
|
+
def prepare
|
93
|
+
@data = config.load_paths.map { |path| load_translations(path) }.reduce(:merge)
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# @api private
|
100
|
+
def load_translations(path)
|
101
|
+
data = self.class.flat_hash(YAML.load_file(path))
|
102
|
+
|
103
|
+
return data unless custom_top_namespace?(path)
|
104
|
+
|
105
|
+
data.map { |k, v| [k.gsub(DEFAULT_MESSAGES_ROOT, config.top_namespace), v] }.to_h
|
106
|
+
end
|
107
|
+
|
108
|
+
# @api private
|
109
|
+
def evaluated_key(key, options)
|
110
|
+
return key unless key.include?(LOCALE_TOKEN)
|
111
|
+
|
112
|
+
key % { locale: options[:locale] || default_locale }
|
113
|
+
end
|
93
114
|
end
|
94
115
|
end
|
95
116
|
end
|