dry-schema 0.1.0 → 1.0.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 +166 -0
- data/LICENSE +1 -1
- data/README.md +13 -4
- data/config/errors.yml +59 -54
- data/lib/dry/schema/compiler.rb +24 -0
- data/lib/dry/schema/config.rb +23 -27
- data/lib/dry/schema/constants.rb +27 -3
- data/lib/dry/schema/dsl.rb +36 -49
- data/lib/dry/schema/extensions/hints/compiler_methods.rb +19 -0
- data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +84 -0
- data/lib/dry/schema/extensions/hints/message_set_methods.rb +44 -0
- data/lib/dry/schema/extensions/hints/result_methods.rb +47 -0
- data/lib/dry/schema/extensions/hints.rb +56 -0
- data/lib/dry/schema/extensions/monads.rb +15 -4
- data/lib/dry/schema/extensions.rb +6 -0
- data/lib/dry/schema/json.rb +4 -2
- data/lib/dry/schema/key.rb +8 -10
- data/lib/dry/schema/key_coercer.rb +6 -2
- data/lib/dry/schema/key_map.rb +5 -4
- data/lib/dry/schema/macros/array.rb +39 -0
- data/lib/dry/schema/macros/core.rb +2 -0
- data/lib/dry/schema/macros/dsl.rb +149 -4
- data/lib/dry/schema/macros/each.rb +12 -1
- data/lib/dry/schema/macros/filled.rb +14 -7
- data/lib/dry/schema/macros/hash.rb +6 -32
- data/lib/dry/schema/macros/key.rb +38 -60
- data/lib/dry/schema/macros/maybe.rb +5 -3
- data/lib/dry/schema/macros/optional.rb +8 -1
- data/lib/dry/schema/macros/required.rb +3 -1
- data/lib/dry/schema/macros/schema.rb +46 -0
- data/lib/dry/schema/macros/value.rb +18 -2
- data/lib/dry/schema/macros.rb +4 -0
- data/lib/dry/schema/message/or.rb +54 -0
- data/lib/dry/schema/message.rb +56 -72
- data/lib/dry/schema/message_compiler/visitor_opts.rb +28 -1
- data/lib/dry/schema/message_compiler.rb +85 -74
- data/lib/dry/schema/message_set.rb +82 -50
- data/lib/dry/schema/messages/abstract.rb +84 -44
- data/lib/dry/schema/messages/i18n.rb +45 -9
- data/lib/dry/schema/messages/namespaced.rb +22 -5
- data/lib/dry/schema/messages/template.rb +68 -0
- data/lib/dry/schema/messages/yaml.rb +76 -22
- data/lib/dry/schema/messages.rb +19 -24
- data/lib/dry/schema/namespaced_rule.rb +37 -0
- data/lib/dry/schema/params.rb +4 -2
- data/lib/dry/schema/path.rb +102 -0
- data/lib/dry/schema/predicate.rb +15 -0
- data/lib/dry/schema/predicate_inferrer.rb +116 -23
- data/lib/dry/schema/predicate_registry.rb +2 -0
- data/lib/dry/schema/processor.rb +70 -39
- data/lib/dry/schema/result.rb +16 -31
- data/lib/dry/schema/rule_applier.rb +10 -3
- data/lib/dry/schema/trace.rb +16 -3
- data/lib/dry/schema/type_registry.rb +5 -3
- data/lib/dry/schema/types.rb +3 -1
- data/lib/dry/schema/value_coercer.rb +10 -4
- data/lib/dry/schema/version.rb +3 -1
- data/lib/dry/schema.rb +39 -13
- data/lib/dry-schema.rb +2 -0
- metadata +43 -47
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
2
4
|
require 'concurrent/map'
|
|
3
5
|
require 'dry/equalizer'
|
|
4
6
|
require 'dry/configurable'
|
|
5
7
|
|
|
6
8
|
require 'dry/schema/constants'
|
|
9
|
+
require 'dry/schema/messages/template'
|
|
7
10
|
|
|
8
11
|
module Dry
|
|
9
12
|
module Schema
|
|
@@ -12,35 +15,32 @@ module Dry
|
|
|
12
15
|
#
|
|
13
16
|
# @api public
|
|
14
17
|
class Abstract
|
|
15
|
-
|
|
18
|
+
include Dry::Configurable
|
|
16
19
|
include Dry::Equalizer(:config)
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
setting :
|
|
21
|
-
setting :
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
setting :
|
|
37
|
-
setting :val_type_default, 'default'.freeze
|
|
38
|
-
|
|
39
|
-
setting :arg_types, Hash.new { |*| config.arg_type_default }.update(
|
|
21
|
+
setting :load_paths, Set[DEFAULT_MESSAGES_PATH]
|
|
22
|
+
setting :top_namespace, DEFAULT_MESSAGES_ROOT
|
|
23
|
+
setting :root, 'errors'
|
|
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',
|
|
31
|
+
'%<root>s.%<predicate>s.value.%<val_type>s.arg.%<arg_type>s',
|
|
32
|
+
'%<root>s.%<predicate>s.value.%<val_type>s',
|
|
33
|
+
'%<root>s.%<predicate>s.arg.%<arg_type>s',
|
|
34
|
+
'%<root>s.%<predicate>s'
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
setting :rule_lookup_paths, ['rules.%<name>s'].freeze
|
|
38
|
+
|
|
39
|
+
setting :arg_types, Hash.new { |*| 'default' }.update(
|
|
40
40
|
Range => 'range'
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
-
setting :val_types, Hash.new { |*|
|
|
43
|
+
setting :val_types, Hash.new { |*| 'default' }.update(
|
|
44
44
|
Range => 'range',
|
|
45
45
|
String => 'string'
|
|
46
46
|
)
|
|
@@ -51,11 +51,24 @@ module Dry
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# @api private
|
|
54
|
-
|
|
54
|
+
def self.build(options = EMPTY_HASH)
|
|
55
|
+
messages = new
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
messages.configure do |config|
|
|
58
|
+
options.each do |key, value|
|
|
59
|
+
config.public_send(:"#{key}=", value)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
config.root = "#{config.top_namespace}.#{config.root}"
|
|
63
|
+
|
|
64
|
+
config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
|
|
65
|
+
"#{config.top_namespace}.#{path}"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
yield(config) if block_given?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
messages.prepare
|
|
59
72
|
end
|
|
60
73
|
|
|
61
74
|
# @api private
|
|
@@ -63,21 +76,29 @@ module Dry
|
|
|
63
76
|
@hash ||= config.hash
|
|
64
77
|
end
|
|
65
78
|
|
|
79
|
+
# @api private
|
|
80
|
+
def translate(key, locale: default_locale)
|
|
81
|
+
t["#{config.top_namespace}.#{key}", locale: locale]
|
|
82
|
+
end
|
|
83
|
+
|
|
66
84
|
# @api private
|
|
67
85
|
def rule(name, options = {})
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
tokens = { name: name, locale: options.fetch(:locale, default_locale) }
|
|
87
|
+
path = rule_lookup_paths(tokens).detect { |key| key?(key, options) }
|
|
88
|
+
|
|
89
|
+
rule = get(path, options) if path
|
|
90
|
+
rule.is_a?(Hash) ? rule[:text] : rule
|
|
70
91
|
end
|
|
71
92
|
|
|
72
|
-
# Retrieve a message
|
|
93
|
+
# Retrieve a message template
|
|
73
94
|
#
|
|
74
|
-
# @return [
|
|
95
|
+
# @return [Template]
|
|
75
96
|
#
|
|
76
97
|
# @api public
|
|
77
98
|
def call(*args)
|
|
78
99
|
cache.fetch_or_store(args.hash) do
|
|
79
|
-
|
|
80
|
-
|
|
100
|
+
text, meta = lookup(*args)
|
|
101
|
+
[Template[text], meta] if text
|
|
81
102
|
end
|
|
82
103
|
end
|
|
83
104
|
alias_method :[], :call
|
|
@@ -85,38 +106,50 @@ module Dry
|
|
|
85
106
|
# Try to find a message for the given predicate and its options
|
|
86
107
|
#
|
|
87
108
|
# @api private
|
|
88
|
-
|
|
109
|
+
#
|
|
110
|
+
# rubocop:disable Metrics/AbcSize
|
|
111
|
+
def lookup(predicate, options)
|
|
89
112
|
tokens = options.merge(
|
|
90
|
-
root: options[:not] ? "#{root}.not" : root,
|
|
91
113
|
predicate: predicate,
|
|
114
|
+
root: options[:not] ? "#{root}.not" : root,
|
|
92
115
|
arg_type: config.arg_types[options[:arg_type]],
|
|
93
116
|
val_type: config.val_types[options[:val_type]],
|
|
94
117
|
message_type: options[:message_type] || :failure
|
|
95
118
|
)
|
|
96
119
|
|
|
97
|
-
tokens[:path] = options[:rule] || Array(options[:path]).join(DOT)
|
|
98
|
-
|
|
99
120
|
opts = options.reject { |k, _| config.lookup_options.include?(k) }
|
|
100
121
|
|
|
101
|
-
path = lookup_paths(tokens).detect
|
|
102
|
-
|
|
103
|
-
|
|
122
|
+
path = lookup_paths(tokens).detect { |key| key?(key, opts) }
|
|
123
|
+
|
|
124
|
+
return unless path
|
|
104
125
|
|
|
105
|
-
|
|
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
|
|
106
133
|
end
|
|
134
|
+
# rubocop:enable Metrics/AbcSize
|
|
107
135
|
|
|
108
136
|
# @api private
|
|
109
137
|
def lookup_paths(tokens)
|
|
110
138
|
config.lookup_paths.map { |path| path % tokens }
|
|
111
139
|
end
|
|
112
140
|
|
|
141
|
+
# @api private
|
|
142
|
+
def rule_lookup_paths(tokens)
|
|
143
|
+
config.rule_lookup_paths.map { |key| key % tokens }
|
|
144
|
+
end
|
|
145
|
+
|
|
113
146
|
# Return a new message backend that will look for messages under provided namespace
|
|
114
147
|
#
|
|
115
148
|
# @param [Symbol,String] namespace
|
|
116
149
|
#
|
|
117
150
|
# @api public
|
|
118
151
|
def namespaced(namespace)
|
|
119
|
-
|
|
152
|
+
Dry::Schema::Messages::Namespaced.new(namespace, self)
|
|
120
153
|
end
|
|
121
154
|
|
|
122
155
|
# Return root path to messages file
|
|
@@ -137,6 +170,13 @@ module Dry
|
|
|
137
170
|
def default_locale
|
|
138
171
|
:en
|
|
139
172
|
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
# @api private
|
|
177
|
+
def custom_top_namespace?(path)
|
|
178
|
+
path.to_s == DEFAULT_MESSAGES_PATH.to_s && config.top_namespace != DEFAULT_MESSAGES_ROOT
|
|
179
|
+
end
|
|
140
180
|
end
|
|
141
181
|
end
|
|
142
182
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'i18n'
|
|
2
4
|
require 'dry/schema/messages/abstract'
|
|
3
5
|
|
|
@@ -7,10 +9,11 @@ module Dry
|
|
|
7
9
|
#
|
|
8
10
|
# @api public
|
|
9
11
|
class Messages::I18n < Messages::Abstract
|
|
12
|
+
# Translation function
|
|
13
|
+
#
|
|
14
|
+
# @return [Method]
|
|
10
15
|
attr_reader :t
|
|
11
16
|
|
|
12
|
-
::I18n.load_path.concat(config.paths)
|
|
13
|
-
|
|
14
17
|
# @api private
|
|
15
18
|
def initialize
|
|
16
19
|
super
|
|
@@ -25,7 +28,7 @@ module Dry
|
|
|
25
28
|
# @return [String]
|
|
26
29
|
#
|
|
27
30
|
# @api public
|
|
28
|
-
def get(key, options =
|
|
31
|
+
def get(key, options = EMPTY_HASH)
|
|
29
32
|
t.(key, options) if key
|
|
30
33
|
end
|
|
31
34
|
|
|
@@ -35,26 +38,59 @@ module Dry
|
|
|
35
38
|
#
|
|
36
39
|
# @api public
|
|
37
40
|
def key?(key, options)
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
I18n.exists?(key, options.fetch(:locale, default_locale)) ||
|
|
42
|
+
I18n.exists?(key, I18n.default_locale)
|
|
40
43
|
end
|
|
41
44
|
|
|
42
45
|
# Merge messages from an additional path
|
|
43
46
|
#
|
|
44
|
-
# @param [String]
|
|
47
|
+
# @param [String, Array<String>] paths
|
|
45
48
|
#
|
|
46
49
|
# @return [Messages::I18n]
|
|
47
50
|
#
|
|
48
51
|
# @api public
|
|
49
|
-
def merge(
|
|
50
|
-
|
|
51
|
-
self
|
|
52
|
+
def merge(paths)
|
|
53
|
+
prepare(paths)
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
# @api private
|
|
55
57
|
def default_locale
|
|
56
58
|
I18n.locale || I18n.default_locale || super
|
|
57
59
|
end
|
|
60
|
+
|
|
61
|
+
# @api private
|
|
62
|
+
def prepare(paths = config.load_paths)
|
|
63
|
+
paths.each do |path|
|
|
64
|
+
data = YAML.load_file(path)
|
|
65
|
+
|
|
66
|
+
if custom_top_namespace?(path)
|
|
67
|
+
top_namespace = config.top_namespace
|
|
68
|
+
|
|
69
|
+
mapped_data = data
|
|
70
|
+
.map { |k, v| [k, { top_namespace => v[DEFAULT_MESSAGES_ROOT] }] }
|
|
71
|
+
.to_h
|
|
72
|
+
|
|
73
|
+
store_translations(mapped_data)
|
|
74
|
+
else
|
|
75
|
+
store_translations(data)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# @api private
|
|
85
|
+
def store_translations(data)
|
|
86
|
+
locales = data.keys.map(&:to_sym)
|
|
87
|
+
|
|
88
|
+
I18n.available_locales |= locales
|
|
89
|
+
|
|
90
|
+
locales.each do |locale|
|
|
91
|
+
I18n.backend.store_translations(locale, data[locale.to_s])
|
|
92
|
+
end
|
|
93
|
+
end
|
|
58
94
|
end
|
|
59
95
|
end
|
|
60
96
|
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Dry
|
|
2
4
|
module Schema
|
|
3
5
|
module Messages
|
|
4
6
|
# Namespaced messages backend
|
|
5
7
|
#
|
|
6
8
|
# @api public
|
|
7
|
-
class Namespaced <Dry::Schema::Messages::Abstract
|
|
9
|
+
class Namespaced < Dry::Schema::Messages::Abstract
|
|
8
10
|
# @api private
|
|
9
11
|
attr_reader :namespace
|
|
10
12
|
|
|
@@ -12,14 +14,17 @@ module Dry
|
|
|
12
14
|
attr_reader :messages
|
|
13
15
|
|
|
14
16
|
# @api private
|
|
15
|
-
attr_reader :
|
|
17
|
+
attr_reader :config
|
|
18
|
+
|
|
19
|
+
# @api private
|
|
20
|
+
attr_reader :call_opts
|
|
16
21
|
|
|
17
22
|
# @api private
|
|
18
23
|
def initialize(namespace, messages)
|
|
19
|
-
|
|
24
|
+
@config = messages.config
|
|
20
25
|
@namespace = namespace
|
|
21
26
|
@messages = messages
|
|
22
|
-
@
|
|
27
|
+
@call_opts = { namespace: namespace }.freeze
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
# Get a message for the given key and its options
|
|
@@ -34,6 +39,12 @@ module Dry
|
|
|
34
39
|
messages.get(key, options)
|
|
35
40
|
end
|
|
36
41
|
|
|
42
|
+
# @api public
|
|
43
|
+
def call(key, options = {})
|
|
44
|
+
super(key, options.empty? ? call_opts : options.merge(call_opts))
|
|
45
|
+
end
|
|
46
|
+
alias_method :[], :call
|
|
47
|
+
|
|
37
48
|
# Check if given key is defined
|
|
38
49
|
#
|
|
39
50
|
# @return [Boolean]
|
|
@@ -45,7 +56,13 @@ module Dry
|
|
|
45
56
|
|
|
46
57
|
# @api private
|
|
47
58
|
def lookup_paths(tokens)
|
|
48
|
-
super(tokens.merge(root: "#{root}
|
|
59
|
+
super(tokens.merge(root: "#{tokens[:root]}.#{namespace}")) + super
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @api private
|
|
63
|
+
def rule_lookup_paths(tokens)
|
|
64
|
+
base_paths = messages.rule_lookup_paths(tokens)
|
|
65
|
+
base_paths.map { |key| key.gsub('dry_schema', "dry_schema.#{namespace}") } + base_paths
|
|
49
66
|
end
|
|
50
67
|
end
|
|
51
68
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry/equalizer'
|
|
4
|
+
|
|
5
|
+
require 'dry/schema/constants'
|
|
6
|
+
|
|
7
|
+
module Dry
|
|
8
|
+
module Schema
|
|
9
|
+
module Messages
|
|
10
|
+
# Template wraps a string with interpolation tokens and defines evaluator function
|
|
11
|
+
# dynamically
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
class Template
|
|
15
|
+
include Dry::Equalizer(:text)
|
|
16
|
+
|
|
17
|
+
TOKEN_REGEXP = /%{([\w\d]*)}/
|
|
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
|
|
30
|
+
|
|
31
|
+
# @api private
|
|
32
|
+
def self.[](input)
|
|
33
|
+
new(*parse(input))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @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)]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @api private
|
|
49
|
+
def initialize(text, tokens, evaluator)
|
|
50
|
+
@text = text
|
|
51
|
+
@tokens = tokens
|
|
52
|
+
@evaluator = evaluator
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @api private
|
|
56
|
+
def data(input)
|
|
57
|
+
tokens.each_with_object({}) { |k, h| h[k] = input[k] }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @api private
|
|
61
|
+
def call(data = EMPTY_HASH)
|
|
62
|
+
data.empty? ? evaluator.() : evaluator.(data)
|
|
63
|
+
end
|
|
64
|
+
alias_method :[], :call
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'yaml'
|
|
2
4
|
require 'pathname'
|
|
3
5
|
|
|
4
6
|
require 'dry/equalizer'
|
|
7
|
+
require 'dry/schema/constants'
|
|
5
8
|
require 'dry/schema/messages/abstract'
|
|
6
9
|
|
|
7
10
|
module Dry
|
|
@@ -10,36 +13,57 @@ module Dry
|
|
|
10
13
|
#
|
|
11
14
|
# @api public
|
|
12
15
|
class Messages::YAML < Messages::Abstract
|
|
16
|
+
LOCALE_TOKEN = '%<locale>s'
|
|
17
|
+
|
|
13
18
|
include Dry::Equalizer(:data)
|
|
14
19
|
|
|
20
|
+
# Loaded localized message templates
|
|
21
|
+
#
|
|
22
|
+
# @return [Hash]
|
|
15
23
|
attr_reader :data
|
|
16
24
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
# Translation function
|
|
26
|
+
#
|
|
27
|
+
# @return [Proc]
|
|
28
|
+
attr_reader :t
|
|
21
29
|
|
|
22
30
|
# @api private
|
|
23
|
-
def self.
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
def self.build(options = EMPTY_HASH)
|
|
32
|
+
super do |config|
|
|
33
|
+
config.root = "%<locale>s.#{config.root}"
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
|
|
36
|
+
"%<locale>s.#{path}"
|
|
37
|
+
}
|
|
38
|
+
end
|
|
30
39
|
end
|
|
31
40
|
|
|
32
41
|
# @api private
|
|
33
|
-
def self.flat_hash(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
def self.flat_hash(hash, path = [], keys = {})
|
|
43
|
+
hash.each do |key, value|
|
|
44
|
+
flat_hash(value, [*path, key], keys) if value.is_a?(Hash)
|
|
45
|
+
|
|
46
|
+
if value.is_a?(String) && hash['text'] != value
|
|
47
|
+
keys[[*path, key].join(DOT)] = {
|
|
48
|
+
text: value,
|
|
49
|
+
meta: EMPTY_HASH
|
|
50
|
+
}
|
|
51
|
+
elsif value.is_a?(Hash) && value['text'].is_a?(String)
|
|
52
|
+
keys[[*path, key].join(DOT)] = {
|
|
53
|
+
text: value['text'],
|
|
54
|
+
meta: value.dup.delete_if { |k| k == 'text' }.map { |k, v| [k.to_sym, v] }.to_h
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
keys
|
|
37
59
|
end
|
|
38
60
|
|
|
39
61
|
# @api private
|
|
40
|
-
def initialize(data)
|
|
62
|
+
def initialize(data: EMPTY_HASH, config: nil)
|
|
41
63
|
super()
|
|
42
64
|
@data = data
|
|
65
|
+
@config = config if config
|
|
66
|
+
@t = proc { |key, locale: default_locale| get("%<locale>s.#{key}", locale: locale) }
|
|
43
67
|
end
|
|
44
68
|
|
|
45
69
|
# Get a message for the given key and its options
|
|
@@ -50,8 +74,8 @@ module Dry
|
|
|
50
74
|
# @return [String]
|
|
51
75
|
#
|
|
52
76
|
# @api public
|
|
53
|
-
def get(key, options =
|
|
54
|
-
data[key
|
|
77
|
+
def get(key, options = EMPTY_HASH)
|
|
78
|
+
data[evaluated_key(key, options)]
|
|
55
79
|
end
|
|
56
80
|
|
|
57
81
|
# Check if given key is defined
|
|
@@ -59,24 +83,54 @@ module Dry
|
|
|
59
83
|
# @return [Boolean]
|
|
60
84
|
#
|
|
61
85
|
# @api public
|
|
62
|
-
def key?(key, options =
|
|
63
|
-
data.key?(key
|
|
86
|
+
def key?(key, options = EMPTY_HASH)
|
|
87
|
+
data.key?(evaluated_key(key, options))
|
|
64
88
|
end
|
|
65
89
|
|
|
66
90
|
# Merge messages from an additional path
|
|
67
91
|
#
|
|
68
|
-
# @param [String]
|
|
92
|
+
# @param [String] overrides
|
|
69
93
|
#
|
|
70
94
|
# @return [Messages::I18n]
|
|
71
95
|
#
|
|
72
96
|
# @api public
|
|
73
97
|
def merge(overrides)
|
|
74
98
|
if overrides.is_a?(Hash)
|
|
75
|
-
self.class.new(
|
|
99
|
+
self.class.new(
|
|
100
|
+
data: data.merge(self.class.flat_hash(overrides)),
|
|
101
|
+
config: config
|
|
102
|
+
)
|
|
76
103
|
else
|
|
77
|
-
self.class.new(
|
|
104
|
+
self.class.new(
|
|
105
|
+
data: Array(overrides).reduce(data) { |a, e| a.merge(load_translations(e)) },
|
|
106
|
+
config: config
|
|
107
|
+
)
|
|
78
108
|
end
|
|
79
109
|
end
|
|
110
|
+
|
|
111
|
+
# @api private
|
|
112
|
+
def prepare
|
|
113
|
+
@data = config.load_paths.map { |path| load_translations(path) }.reduce(:merge)
|
|
114
|
+
self
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# @api private
|
|
120
|
+
def load_translations(path)
|
|
121
|
+
data = self.class.flat_hash(YAML.load_file(path))
|
|
122
|
+
|
|
123
|
+
return data unless custom_top_namespace?(path)
|
|
124
|
+
|
|
125
|
+
data.map { |k, v| [k.gsub(DEFAULT_MESSAGES_ROOT, config.top_namespace), v] }.to_h
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @api private
|
|
129
|
+
def evaluated_key(key, options)
|
|
130
|
+
return key unless key.include?(LOCALE_TOKEN)
|
|
131
|
+
|
|
132
|
+
key % { locale: options[:locale] || default_locale }
|
|
133
|
+
end
|
|
80
134
|
end
|
|
81
135
|
end
|
|
82
136
|
end
|
data/lib/dry/schema/messages.rb
CHANGED
|
@@ -1,36 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Dry
|
|
2
4
|
module Schema
|
|
3
5
|
# An API for configuring message backends
|
|
4
6
|
#
|
|
5
7
|
# @api private
|
|
6
8
|
module Messages
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
messages.merge(config.messages_file)
|
|
14
|
-
elsif config.namespace
|
|
15
|
-
messages.namespaced(config.namespace)
|
|
16
|
-
else
|
|
17
|
-
messages
|
|
18
|
-
end
|
|
19
|
-
end
|
|
9
|
+
BACKENDS = {
|
|
10
|
+
i18n: 'I18n',
|
|
11
|
+
yaml: 'YAML'
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
when :yaml then default
|
|
25
|
-
when :i18n then Messages::I18n.new
|
|
26
|
-
else
|
|
27
|
-
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"
|
|
28
19
|
end
|
|
29
|
-
end
|
|
30
20
|
|
|
31
|
-
|
|
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
|
|
27
|
+
|
|
28
|
+
messages
|
|
34
29
|
end
|
|
35
30
|
end
|
|
36
31
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dry
|
|
4
|
+
module Schema
|
|
5
|
+
# A special rule type that is configured under a specified namespace
|
|
6
|
+
#
|
|
7
|
+
# This is used internally to create rules that can be properly handled
|
|
8
|
+
# by the message compiler in situations where a schema reuses another schema
|
|
9
|
+
# but it is configured to use a message namespace
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class NamespacedRule
|
|
13
|
+
# @api private
|
|
14
|
+
attr_reader :rule
|
|
15
|
+
|
|
16
|
+
# @api private
|
|
17
|
+
attr_reader :namespace
|
|
18
|
+
|
|
19
|
+
# @api private
|
|
20
|
+
def initialize(namespace, rule)
|
|
21
|
+
@namespace = namespace
|
|
22
|
+
@rule = rule
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @api private
|
|
26
|
+
def call(input)
|
|
27
|
+
result = rule.call(input)
|
|
28
|
+
Logic::Result.new(result.success?) { [:namespace, [namespace, result.to_ast]] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @api private
|
|
32
|
+
def ast(input=Undefined)
|
|
33
|
+
[:namespace, [namespace, rule.ast(input)]]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|