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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +166 -0
  3. data/LICENSE +1 -1
  4. data/README.md +13 -4
  5. data/config/errors.yml +59 -54
  6. data/lib/dry/schema/compiler.rb +24 -0
  7. data/lib/dry/schema/config.rb +23 -27
  8. data/lib/dry/schema/constants.rb +27 -3
  9. data/lib/dry/schema/dsl.rb +36 -49
  10. data/lib/dry/schema/extensions/hints/compiler_methods.rb +19 -0
  11. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +84 -0
  12. data/lib/dry/schema/extensions/hints/message_set_methods.rb +44 -0
  13. data/lib/dry/schema/extensions/hints/result_methods.rb +47 -0
  14. data/lib/dry/schema/extensions/hints.rb +56 -0
  15. data/lib/dry/schema/extensions/monads.rb +15 -4
  16. data/lib/dry/schema/extensions.rb +6 -0
  17. data/lib/dry/schema/json.rb +4 -2
  18. data/lib/dry/schema/key.rb +8 -10
  19. data/lib/dry/schema/key_coercer.rb +6 -2
  20. data/lib/dry/schema/key_map.rb +5 -4
  21. data/lib/dry/schema/macros/array.rb +39 -0
  22. data/lib/dry/schema/macros/core.rb +2 -0
  23. data/lib/dry/schema/macros/dsl.rb +149 -4
  24. data/lib/dry/schema/macros/each.rb +12 -1
  25. data/lib/dry/schema/macros/filled.rb +14 -7
  26. data/lib/dry/schema/macros/hash.rb +6 -32
  27. data/lib/dry/schema/macros/key.rb +38 -60
  28. data/lib/dry/schema/macros/maybe.rb +5 -3
  29. data/lib/dry/schema/macros/optional.rb +8 -1
  30. data/lib/dry/schema/macros/required.rb +3 -1
  31. data/lib/dry/schema/macros/schema.rb +46 -0
  32. data/lib/dry/schema/macros/value.rb +18 -2
  33. data/lib/dry/schema/macros.rb +4 -0
  34. data/lib/dry/schema/message/or.rb +54 -0
  35. data/lib/dry/schema/message.rb +56 -72
  36. data/lib/dry/schema/message_compiler/visitor_opts.rb +28 -1
  37. data/lib/dry/schema/message_compiler.rb +85 -74
  38. data/lib/dry/schema/message_set.rb +82 -50
  39. data/lib/dry/schema/messages/abstract.rb +84 -44
  40. data/lib/dry/schema/messages/i18n.rb +45 -9
  41. data/lib/dry/schema/messages/namespaced.rb +22 -5
  42. data/lib/dry/schema/messages/template.rb +68 -0
  43. data/lib/dry/schema/messages/yaml.rb +76 -22
  44. data/lib/dry/schema/messages.rb +19 -24
  45. data/lib/dry/schema/namespaced_rule.rb +37 -0
  46. data/lib/dry/schema/params.rb +4 -2
  47. data/lib/dry/schema/path.rb +102 -0
  48. data/lib/dry/schema/predicate.rb +15 -0
  49. data/lib/dry/schema/predicate_inferrer.rb +116 -23
  50. data/lib/dry/schema/predicate_registry.rb +2 -0
  51. data/lib/dry/schema/processor.rb +70 -39
  52. data/lib/dry/schema/result.rb +16 -31
  53. data/lib/dry/schema/rule_applier.rb +10 -3
  54. data/lib/dry/schema/trace.rb +16 -3
  55. data/lib/dry/schema/type_registry.rb +5 -3
  56. data/lib/dry/schema/types.rb +3 -1
  57. data/lib/dry/schema/value_coercer.rb +10 -4
  58. data/lib/dry/schema/version.rb +3 -1
  59. data/lib/dry/schema.rb +39 -13
  60. data/lib/dry-schema.rb +2 -0
  61. metadata +43 -47
@@ -1,9 +1,12 @@
1
- require 'pathname'
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
- extend Dry::Configurable
18
+ include Dry::Configurable
16
19
  include Dry::Equalizer(:config)
17
20
 
18
- DEFAULT_PATH = Pathname(__dir__).join('../../../../config/errors.yml').realpath.freeze
19
-
20
- setting :paths, [DEFAULT_PATH]
21
- setting :root, 'errors'.freeze
22
- setting :lookup_options, [:root, :predicate, :path, :val_type, :arg_type].freeze
23
-
24
- setting :lookup_paths, %w(
25
- %{root}.rules.%{path}.%{predicate}.arg.%{arg_type}
26
- %{root}.rules.%{path}.%{predicate}
27
- %{root}.%{predicate}.%{message_type}
28
- %{root}.%{predicate}.value.%{path}.arg.%{arg_type}
29
- %{root}.%{predicate}.value.%{path}
30
- %{root}.%{predicate}.value.%{val_type}.arg.%{arg_type}
31
- %{root}.%{predicate}.value.%{val_type}
32
- %{root}.%{predicate}.arg.%{arg_type}
33
- %{root}.%{predicate}
34
- ).freeze
35
-
36
- setting :arg_type_default, 'default'.freeze
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 { |*| config.val_type_default }.update(
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
- attr_reader :config
54
+ def self.build(options = EMPTY_HASH)
55
+ messages = new
55
56
 
56
- # @api private
57
- def initialize
58
- @config = self.class.config
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
- path = "%{locale}.rules.#{name}"
69
- get(path, options) if key?(path, options)
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 [String]
95
+ # @return [Template]
75
96
  #
76
97
  # @api public
77
98
  def call(*args)
78
99
  cache.fetch_or_store(args.hash) do
79
- path, opts = lookup(*args)
80
- get(path, opts) if path
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
- def lookup(predicate, options = {})
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 do |key|
102
- key?(key, opts) && get(key, opts).is_a?(String)
103
- end
122
+ path = lookup_paths(tokens).detect { |key| key?(key, opts) }
123
+
124
+ return unless path
104
125
 
105
- [path, opts]
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
- Dry::Schema::Messages::Namespaced.new(namespace, self)
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
- ::I18n.exists?(key, options.fetch(:locale, default_locale)) ||
39
- ::I18n.exists?(key, I18n.default_locale)
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] path
47
+ # @param [String, Array<String>] paths
45
48
  #
46
49
  # @return [Messages::I18n]
47
50
  #
48
51
  # @api public
49
- def merge(path)
50
- ::I18n.load_path << path
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 :root
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
- super()
24
+ @config = messages.config
20
25
  @namespace = namespace
21
26
  @messages = messages
22
- @root = messages.root
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}.rules.#{namespace}")) + super
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
- # @api private
18
- configure do |config|
19
- config.root = '%{locale}.errors'.freeze
20
- end
25
+ # Translation function
26
+ #
27
+ # @return [Proc]
28
+ attr_reader :t
21
29
 
22
30
  # @api private
23
- def self.load(paths = config.paths)
24
- new(paths.map { |path| load_file(path) }.reduce(:merge))
25
- end
31
+ def self.build(options = EMPTY_HASH)
32
+ super do |config|
33
+ config.root = "%<locale>s.#{config.root}"
26
34
 
27
- # @api private
28
- def self.load_file(path)
29
- flat_hash(YAML.load_file(path))
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(h, f = [], g = {})
34
- return g.update(f.join('.'.freeze) => h) unless h.is_a? Hash
35
- h.each { |k, r| flat_hash(r, f + [k], g) }
36
- g
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 % { locale: options.fetch(:locale, default_locale) }]
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 % { locale: options.fetch(:locale, default_locale) })
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] path
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(data.merge(self.class.flat_hash(overrides)))
99
+ self.class.new(
100
+ data: data.merge(self.class.flat_hash(overrides)),
101
+ config: config
102
+ )
76
103
  else
77
- self.class.new(data.merge(Messages::YAML.load_file(overrides)))
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
@@ -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
- def self.setup(config)
8
- messages = build(config)
9
-
10
- if config.messages_file && config.namespace
11
- messages.merge(config.messages_file).namespaced(config.namespace)
12
- elsif config.messages_file
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
- # @api private
22
- def self.build(config)
23
- case config.messages
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
- # @api private
32
- def self.default
33
- Messages::YAML.load
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