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.
@@ -15,7 +15,6 @@ module Dry
15
15
  def self.new
16
16
  opts = super
17
17
  opts[:path] = EMPTY_ARRAY
18
- opts[:rule] = nil
19
18
  opts[:message_type] = :failure
20
19
  opts[:current_messages] = EMPTY_ARRAY.dup
21
20
  opts
@@ -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
- alias_method :dump, :to_h
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?
@@ -6,35 +6,26 @@ module Dry
6
6
  #
7
7
  # @api private
8
8
  module Messages
9
- def self.setup(config)
10
- messages = build(config)
11
-
12
- if config.messages_file && config.namespace
13
- messages.merge(config.messages_file).namespaced(config.namespace)
14
- elsif config.messages_file
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
- # @api private
24
- def self.build(config)
25
- klass = case config.messages
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
- klass.build
33
- end
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
- # @api private
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 'pathname'
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
- extend Dry::Configurable
18
+ include Dry::Configurable
19
19
  include Dry::Equalizer(:config)
20
20
 
21
- DEFAULT_PATH = Pathname(__dir__).join('../../../../config/errors.yml').realpath.freeze
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, [:root, :predicate, :path, :val_type, :arg_type].freeze
26
-
27
- setting :lookup_paths, %w(
28
- %{root}.rules.%{path}.%{predicate}.arg.%{arg_type}
29
- %{root}.rules.%{path}.%{predicate}
30
- %{root}.%{predicate}.%{message_type}
31
- %{root}.%{predicate}.value.%{path}.arg.%{arg_type}
32
- %{root}.%{predicate}.value.%{path}
33
- %{root}.%{predicate}.value.%{val_type}.arg.%{arg_type}
34
- %{root}.%{predicate}.value.%{val_type}
35
- %{root}.%{predicate}.arg.%{arg_type}
36
- %{root}.%{predicate}
37
- ).freeze
38
-
39
- setting :rule_lookup_paths, %w(
40
- rules.%{name}
41
- ).freeze
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 { |*| config.val_type_default }.update(
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
- attr_reader :config
55
+ def self.build(options = EMPTY_HASH)
56
+ messages = new
62
57
 
63
- # @api private
64
- def initialize
65
- @config = self.class.config
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
- tokens[:path] = options[:rule] || Array(options[:path]).last
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
- Dry::Schema::Messages::Namespaced.new(namespace, self)
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
- ::I18n.exists?(key, options.fetch(:locale, default_locale)) ||
55
- ::I18n.exists?(key, I18n.default_locale)
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] path
44
+ # @param [String, Array<String>] paths
61
45
  #
62
46
  # @return [Messages::I18n]
63
47
  #
64
48
  # @api public
65
- def merge(path)
66
- ::I18n.load_path << path
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 :root
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
- super()
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
- configure do |config|
21
- config.root = '%{locale}.dry_schema.errors'
22
- config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
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
- # @api private
28
- def self.build(paths = config.paths)
29
- new(paths.map { |path| load_file(path) }.reduce(:merge))
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.load_file(path)
34
- flat_hash(YAML.load_file(path))
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
- # @api private
38
- def self.flat_hash(h, f = [], g = {})
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 = key.include?('%{locale}') ?
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
- evaluated_key = key.include?('%{locale}') ?
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(data.merge(self.class.flat_hash(overrides)))
79
+ self.class.new(
80
+ data: data.merge(self.class.flat_hash(overrides)),
81
+ config: config
82
+ )
89
83
  else
90
- self.class.new(data.merge(Messages::YAML.load_file(overrides)))
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