messages_dictionary 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 270aca02bdc13e851f345d7ce783fbd8329e95f2054c3865fa5b5c1cc2b09c6c
4
- data.tar.gz: be5c5615e7c87d7423019fcd96afdf6695ea8527b7c63777c550ca8f6c4fdde7
3
+ metadata.gz: 3a6f7d3a4a63be59119844b7d3139f10b82118a7a726cbfb59f078dc68edb916
4
+ data.tar.gz: 1884503b8aad4f2c452666833a7a48867c2c0fa55c74854e94ea0a5a6c0bed79
5
5
  SHA512:
6
- metadata.gz: 9661d7b64b05ee3d68042938cf05f56b4606107104122e087b39ee86c0b45db79e6ec304e89785d53c1565e7f18a200e542f3ef0cff81acf94afd1cae31aa222
7
- data.tar.gz: e401f904c1e3efa0215a0551a33f4dced1a915f7173705232aee7f660c21f04b0f4f6853b2b280a167183d9e8c2960d6287ad0f156f7f54e967f85eba88d2781
6
+ metadata.gz: 2979cbdfacfdfdd47985304f3d6535ce4fe121ca240807e1700a8f9a15aa54e4301e487eeddbcaf9f456f4814f0f51d881e026cdafd65a23ce96d3c539b29057
7
+ data.tar.gz: eb7c8163836c469604d4fb1af38c84fc9c4d3a450dba73cb429e018a741e55768b0b25fa4f6be2715157e9ec794daf31bd21505db0179f9fe84f23b75cd289d6
data/CHANGELOG.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # Changelog
2
2
 
3
- ## 2.0.0
3
+ ## 2.1.0 (22-Nov-2022)
4
+
5
+ * `pou` and `pretty_output` are now available inside class methods
6
+ * `DICTIONARY_CONF` now contains an instance of the `Config` class that takes care of all configuration options
7
+ * Added `lazy` option that enables lazy loading
8
+ * Added `on_key_missing` option which is set to `:raise` by default. You can pass a proc or a lambda to this option in order to provide a custom handler that fires when a given key cannot be found.
9
+ * Added `file_loader` option to handle custom file loading
10
+
11
+ ## 2.0.0 (21-Nov-2022)
4
12
 
5
13
  This is a major re-write of the gem. All core features stay the same and there should not be any breaking changes, except for one thing: you should not use "destructive" methods when transforming your messages.
6
14
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- messages_dictionary (2.0.0)
4
+ messages_dictionary (2.1.0)
5
5
  hashie (~> 5.0)
6
6
  zeitwerk (~> 2.4)
7
7
 
@@ -70,6 +70,7 @@ PLATFORMS
70
70
 
71
71
  DEPENDENCIES
72
72
  codecov (~> 0.1)
73
+ json (~> 2)
73
74
  messages_dictionary!
74
75
  rake (~> 13.0)
75
76
  rspec (~> 3.6)
data/README.md CHANGED
@@ -35,9 +35,7 @@ Another, a bit more complex, use case in the [lessons_indexer gem](https://githu
35
35
  * [Other classes simply inherit from it](https://github.com/bodrovis/lessons_indexer/blob/master/lib/lessons_indexer/indexer.rb#L2)
36
36
  * [Messages are fetched easily](https://github.com/bodrovis/lessons_indexer/blob/master/lib/lessons_indexer/indexer.rb#L45)
37
37
 
38
- ## Usage
39
-
40
- ### Basic Example
38
+ ## Basic usage
41
39
 
42
40
  Suppose you have the following program:
43
41
 
@@ -86,7 +84,7 @@ class MyOtherClass
86
84
  def greet
87
85
  pretty_output(:welcome)
88
86
  # Or simply
89
- pou(:welcome)
87
+ pou :welcome
90
88
  end
91
89
  end
92
90
  ```
@@ -156,9 +154,9 @@ class MyClass
156
154
  end
157
155
  ```
158
156
 
159
- ### Further Customization
157
+ ## Further Customization
160
158
 
161
- #### Specifying File Name and Directory
159
+ ### Specifying File Name and Directory
162
160
 
163
161
  By default `messages_dictionary` will search for a *.yml* file named after your class (converted to snake case,
164
162
  so for the `MyClass` the file should be named *my_class.yml*)
@@ -170,13 +168,29 @@ inside the same directory. However, this behavior can be easily changed with the
170
168
  ```ruby
171
169
  class MyClass
172
170
  include MessagesDictionary
173
- has_messages_dictionary file: 'some_file.yml', dir: 'C:\my_docs'
171
+ has_messages_dictionary file: 'some_file.yml', dir: 'my_docs'
174
172
  end
175
173
  ```
176
174
 
177
175
  Both of these options are not mandatory.
178
176
 
179
- #### Specifying Messages Hash
177
+ ### Providing a custom file loader
178
+
179
+ By default the gem a messages file in YAML format. However, you might want to use a different format: for example, JSON. In this case you'll have to provide a custom loader:
180
+
181
+ ```ruby
182
+ class MyClass
183
+ include MessagesDictionary
184
+ has_messages_dictionary file: 'test_file.json', dir: 'my_dir',
185
+ file_loader: ->(file_path) { JSON.parse(File.read(file_path)) }
186
+ end
187
+ ```
188
+
189
+ The `:file_loader` option accepts a proc or a lambda that receives a path to your messages file as an argument. This lambda must return a hash object with keys and the corresponding values.
190
+
191
+ The default value for the `:file_loader` is `->(f) { YAML.load_file(f) }`.
192
+
193
+ ### Specifying Messages Hash
180
194
 
181
195
  Instead of loading messages from a file, you can pass hash to the `has_messages_dictionary` using `:messages` option:
182
196
 
@@ -189,7 +203,7 @@ end
189
203
 
190
204
  Nesting and all other features are supported as well.
191
205
 
192
- #### Specifying Output and Display Method
206
+ ### Specifying Output and Display Method
193
207
 
194
208
  By default all messages will be outputted to `STDOUT` using `puts` method, however this can be changed:
195
209
 
@@ -204,7 +218,32 @@ class MyClass
204
218
  end
205
219
  ```
206
220
 
207
- #### Providing Custom Transformation Logic
221
+ ### "Lazy" mode
222
+
223
+ By default this gem will load all messages from the given file. However, you can enable a "lazy" mode so that messages are not loaded until `pou` or `pretty_output` methods have been called. The "lazy" mode can only be enabled when the `:file` option is provided (in other words, `:lazy` has no effect with the `:messages` setting):
224
+
225
+ ```ruby
226
+ class MyClass
227
+ include MessagesDictionary
228
+ has_messages_dictionary lazy: true, file: 'my_file.yml'
229
+
230
+ def greet
231
+ pou :hi
232
+ end
233
+ end
234
+
235
+ # At this point no messages are loaded from the given file
236
+
237
+ obj = MyClass.new
238
+
239
+ # ... doing some other stuff ...
240
+
241
+ # Messages are still not loaded at this point!
242
+
243
+ obj.greet # Now all messages will be loaded from the YAML file
244
+ ```
245
+
246
+ ### Providing Custom Transformation Logic
208
247
 
209
248
  Suppose you want to transform your message somehow or even simply return it instead of printing on the screen.
210
249
  `pretty_output` method accepts an optional block for this purpose:
@@ -262,6 +301,45 @@ def greet
262
301
  end
263
302
  ```
264
303
 
304
+ ### Handling missing keys
305
+
306
+ By default when a non-existent key is requested, an error will be raised:
307
+
308
+ ```ruby
309
+ class MyClass
310
+ include MessagesDictionary
311
+ has_messages_dictionary messages: {key: 'value'}
312
+
313
+ def greet
314
+ pou :unknown_key # trying to use some unknown key...
315
+ end
316
+ end
317
+
318
+ obj = MyClass.new
319
+
320
+ obj.greet # KeyError is raised here!
321
+ ```
322
+
323
+ However, you can adjust the `:on_key_missing` option and provide a custom proc or lambda to handle all missing keys:
324
+
325
+ ```ruby
326
+ class MyClass
327
+ include MessagesDictionary
328
+ has_messages_dictionary messages: {key: 'value'},
329
+ on_key_missing: ->(key) { key } # We simply return the requested key itself
330
+
331
+ def greet
332
+ pou :unknown_key
333
+ end
334
+ end
335
+
336
+ obj = MyClass.new
337
+
338
+ obj.greet # Prints "unknown_key" to the screen, no errors will be raised
339
+ ```
340
+
341
+ So, in the example above we simply return the key itself if it was not found in the messages hash.
342
+
265
343
  ## License
266
344
 
267
345
  Licensed under the [MIT License](https://github.com/bodrovis-learning/messages_dictionary/blob/master/LICENSE).
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MessagesDictionary
4
+ # This class contains all configuration params
5
+ class Config
6
+ using MessagesDictionary::Utils::StringUtils
7
+
8
+ attr_reader :msgs, :output_target, :output_method, :transform, :file, :on_key_missing, :file_loader
9
+
10
+ def initialize(opts, klass)
11
+ @__klass = klass
12
+ @__lazy = opts[:lazy]
13
+
14
+ prepare_storage_for opts
15
+
16
+ @on_key_missing = opts[:on_key_missing] || :raise
17
+ @output_target = opts[:output] || $stdout
18
+ @output_method = (opts[:method] || :puts).to_sym
19
+ @transform = opts[:transform]
20
+
21
+ load_messages
22
+ end
23
+
24
+ # This method loads messages from a file but respects the "lazy" option.
25
+ # In other words, it does not load anything if "lazy" is "true".
26
+ # To force messages loading, use the load_messages! method instead.
27
+ def load_messages
28
+ return if @__lazy
29
+
30
+ do_load_messages!
31
+ end
32
+
33
+ # Loads messages from the file even if "lazy" is "true"
34
+ def load_messages!
35
+ do_load_messages!
36
+ end
37
+
38
+ private
39
+
40
+ def do_load_messages!
41
+ return if @__storage == :hash || @__loaded
42
+
43
+ begin
44
+ data = @file_loader.call @file
45
+ rescue Errno::ENOENT
46
+ raise Errno::ENOENT, "File #{@file} does not exist..."
47
+ else
48
+ @msgs = MessagesDictionary::Utils::Dict.new data
49
+ @__loaded = true
50
+ end
51
+ end
52
+
53
+ def prepare_storage_for(opts)
54
+ if opts.key?(:messages)
55
+ @__storage = :hash
56
+ @msgs = MessagesDictionary::Utils::Dict.new opts[:messages]
57
+ @__loaded = true
58
+ else
59
+ @__storage = :file
60
+ @file = opts[:file] || "#{@__klass.name.nil? ? 'unknown' : @__klass.name.snakecase}.yml"
61
+ @file = File.expand_path(@file, opts[:dir] || '.')
62
+ @file_loader = opts[:file_loader] || ->(f) { YAML.load_file(f) }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -3,11 +3,10 @@
3
3
  module MessagesDictionary
4
4
  # Main module that injects all the necessary methods in the target class
5
5
  module Injector
6
- using MessagesDictionary::Utils::StringUtils
7
-
8
6
  def self.included(klass)
9
7
  klass.extend ClassMethods
10
8
  klass.include InstanceMethods
9
+ klass.extend InstanceMethods
11
10
  end
12
11
 
13
12
  # Class methods to be defined in the target (where the module was included)
@@ -17,27 +16,8 @@ module MessagesDictionary
17
16
  # with all the necesary goodies
18
17
  def has_messages_dictionary(opts = {})
19
18
  # rubocop:enable Naming/PredicateName
20
- messages = MessagesDictionary::Utils::Dict.new(
21
- opts.fetch(:messages) { __from_file(opts) }
22
- )
23
-
24
- const_set(:DICTIONARY_CONF, {msgs: messages,
25
- output: opts[:output] || $stdout,
26
- method: opts[:method] || :puts,
27
- transform: opts[:transform]})
28
- end
29
-
30
- private
31
-
32
- def __from_file(opts)
33
- file = opts[:file] || "#{name.nil? ? 'unknown' : name.snakecase}.yml"
34
- file = File.expand_path(file, opts[:dir]) if opts[:dir]
35
19
 
36
- begin
37
- YAML.load_file(file)
38
- rescue Errno::ENOENT
39
- abort "File #{file} does not exist..."
40
- end
20
+ const_set :DICTIONARY_CONF, MessagesDictionary::Config.new(opts, self)
41
21
  end
42
22
  end
43
23
 
@@ -46,8 +26,10 @@ module MessagesDictionary
46
26
  # This method will output your messages, perform interpolation,
47
27
  # and transformations
48
28
  def pretty_output(key, values = {}, &block)
49
- msg = self.class::DICTIONARY_CONF[:msgs].deep_fetch(*key.to_s.split('.')) do
50
- raise KeyError, "#{key} cannot be found in the provided file..."
29
+ __config.load_messages!
30
+
31
+ msg = __config.msgs.deep_fetch(*key.to_s.split('.')) do
32
+ handle_key_missing(key)
51
33
  end
52
34
 
53
35
  __process(
@@ -61,6 +43,16 @@ module MessagesDictionary
61
43
 
62
44
  private
63
45
 
46
+ def handle_key_missing(key)
47
+ raise KeyError, "#{key} cannot be found..." if __config.on_key_missing == :raise
48
+
49
+ __config.on_key_missing.call(key)
50
+ end
51
+
52
+ def __config
53
+ @__config ||= respond_to?(:const_get) ? const_get(:DICTIONARY_CONF) : self.class.const_get(:DICTIONARY_CONF)
54
+ end
55
+
64
56
  def __replace(msg, values)
65
57
  values.each do |k, v|
66
58
  msg.gsub!(Regexp.new("\\{\\{#{k}\\}\\}"), v.to_s)
@@ -70,12 +62,12 @@ module MessagesDictionary
70
62
  end
71
63
 
72
64
  def __process(msg, &block)
73
- transform = block || self.class::DICTIONARY_CONF[:transform]
65
+ transform = block || __config.transform
74
66
 
75
67
  if transform
76
68
  transform.call(msg)
77
69
  else
78
- self.class::DICTIONARY_CONF[:output].send(self.class::DICTIONARY_CONF[:method].to_sym, msg)
70
+ __config.output_target.send(__config.output_method, msg)
79
71
  end
80
72
  end
81
73
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MessagesDictionary
4
- VERSION = '2.0.0'
4
+ VERSION = '2.1.0'
5
5
  end
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
24
24
  spec.add_dependency 'zeitwerk', '~> 2.4'
25
25
 
26
26
  spec.add_development_dependency 'codecov', '~> 0.1'
27
+ spec.add_development_dependency 'json', '~> 2'
27
28
  spec.add_development_dependency 'rake', '~> 13.0'
28
29
  spec.add_development_dependency 'rspec', '~> 3.6'
29
30
  spec.add_development_dependency 'rubocop', '~> 1.6'
@@ -1,16 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  RSpec.describe MessagesDictionary::Injector do
4
6
  let(:dummy) { Class.new { include MessagesDictionary::Injector } }
5
7
 
6
8
  context 'with messages' do
9
+ context 'when lazy-loading' do
10
+ it 'loads messages on demand' do
11
+ in_dir 'my_test_file.yml', 'my_test_dir' do
12
+ dummy.class_eval do
13
+ has_messages_dictionary file: 'my_test_file.yml', dir: 'my_test_dir', lazy: true
14
+ end
15
+
16
+ expect(dummy::DICTIONARY_CONF.instance_variable_get(:@__loaded)).to be_falsey
17
+ expect(dummy::DICTIONARY_CONF.msgs).to be_nil
18
+
19
+ object = dummy.new
20
+ expect(object.send(:pretty_output, :test) { |msg| msg }).to eq('string')
21
+ expect(object.send(:pretty_output, :interpolated, a: 42) { |msg| msg }).to eq('Value is 42')
22
+
23
+ expect(dummy::DICTIONARY_CONF.instance_variable_get(:@__loaded)).to be true
24
+ expect(dummy::DICTIONARY_CONF.msgs.keys).to include('test', 'interpolated')
25
+ end
26
+ end
27
+ end
28
+
7
29
  context 'when outputting' do
8
30
  it 'uses STDOUT by default' do
9
31
  dummy.class_eval do
10
32
  has_messages_dictionary messages: {test: 'string'}
11
33
  end
12
34
 
13
- expect(dummy::DICTIONARY_CONF[:output]).to eq($stdout)
35
+ expect(dummy::DICTIONARY_CONF.output_target).to eq($stdout)
14
36
  end
15
37
 
16
38
  it 'uses puts method by default' do
@@ -18,7 +40,7 @@ RSpec.describe MessagesDictionary::Injector do
18
40
  has_messages_dictionary messages: {test: 'string'}
19
41
  end
20
42
 
21
- expect(dummy::DICTIONARY_CONF[:method]).to eq(:puts)
43
+ expect(dummy::DICTIONARY_CONF.output_method).to eq(:puts)
22
44
  end
23
45
 
24
46
  it 'allows customizing output and method' do
@@ -52,6 +74,22 @@ RSpec.describe MessagesDictionary::Injector do
52
74
 
53
75
  expect(output).to have_received(:puts).with('string').exactly(1).time
54
76
  end
77
+
78
+ it 'works for class methods' do
79
+ dummy.class_eval do
80
+ has_messages_dictionary messages: {test: 'string'}
81
+
82
+ define_singleton_method :run_klass do
83
+ pou(:test)
84
+ end
85
+ end
86
+
87
+ allow($stdout).to receive(:puts).with('string')
88
+
89
+ dummy.run_klass
90
+
91
+ expect($stdout).to have_received(:puts).exactly(1).time
92
+ end
55
93
  end
56
94
 
57
95
  context 'when passed as hash' do
@@ -60,6 +98,7 @@ RSpec.describe MessagesDictionary::Injector do
60
98
  has_messages_dictionary messages: {parent: {child: 'child_string'}}
61
99
  end
62
100
 
101
+ expect(dummy::DICTIONARY_CONF.instance_variable_get(:@__loaded)).to be true
63
102
  object = dummy.new
64
103
  expect(object.send(:pretty_output, 'parent.child') { |msg| msg }).to eq('child_string')
65
104
  end
@@ -101,6 +140,19 @@ RSpec.describe MessagesDictionary::Injector do
101
140
  expect(object.send(:pretty_output, :interpolated, a: 42) { |msg| msg }).to eq('Value is 42')
102
141
  end
103
142
  end
143
+
144
+ it 'allows providing a custom file loader' do
145
+ in_dir 'test_file.json', 'my_dir' do
146
+ dummy.class_eval do
147
+ has_messages_dictionary file: 'test_file.json', dir: 'my_dir',
148
+ file_loader: ->(f) { JSON.parse(File.read(f)) }
149
+ end
150
+
151
+ object = dummy.new
152
+ expect(object.send(:pretty_output, 'nested.test') { |msg| msg }).to eq('string')
153
+ expect(object.send(:pretty_output, :interpolated, a: 42) { |msg| msg }).to eq('Value is 42')
154
+ end
155
+ end
104
156
  end
105
157
  end
106
158
 
@@ -110,8 +162,28 @@ RSpec.describe MessagesDictionary::Injector do
110
162
  has_messages_dictionary messages: {test: 'string'}
111
163
  end
112
164
 
165
+ key = :does_not_exist
166
+
167
+ object = dummy.new
168
+ expect { object.send(:pretty_output, key) }.to raise_error(
169
+ an_instance_of(KeyError).
170
+ and(
171
+ having_attributes(message: "#{key} cannot be found...")
172
+ )
173
+ )
174
+ end
175
+
176
+ it 'is allows to use custom missing key handler' do
177
+ dummy.class_eval do
178
+ has_messages_dictionary messages: {test: 'string'},
179
+ on_key_missing: ->(key) { key.upcase.split('.') },
180
+ transform: ->(msg) { msg }
181
+ end
182
+
183
+ key = 'does.not.exist'
184
+
113
185
  object = dummy.new
114
- expect { object.send(:pretty_output, :does_not_exist) }.to raise_error(KeyError)
186
+ expect(object.send(:pou, key)).to eq(%w[DOES NOT EXIST])
115
187
  end
116
188
 
117
189
  it 'is raised when file is not found and the program aborts' do
@@ -120,8 +192,12 @@ RSpec.describe MessagesDictionary::Injector do
120
192
  has_messages_dictionary dir: 'random', file: 'not_exist.yml'
121
193
  end
122
194
  end.to raise_error(
123
- an_instance_of(SystemExit).
124
- and(having_attributes(message: "File #{File.expand_path('random/not_exist.yml')} does not exist..."))
195
+ an_instance_of(Errno::ENOENT).
196
+ and(having_attributes(
197
+ message: "No such file or directory - File #{File.expand_path(
198
+ 'random/not_exist.yml'
199
+ )} does not exist..."
200
+ ))
125
201
  )
126
202
  end
127
203
  end
@@ -8,12 +8,38 @@ module SpecFilesSetup
8
8
 
9
9
  FileUtils.mkdir_p full_path
10
10
 
11
- f = File.new(File.join(full_path, file), 'w+')
12
- f.write("test: string\ninterpolated: Value is {{a}}")
13
- f.close
11
+ File.open(File.join(full_path, file), 'w+') do |f|
12
+ if /\.ya?ml\z/i.match?(file)
13
+ f.write yaml_data
14
+ else
15
+ f.write json_data
16
+ end
17
+ end
14
18
  end
15
19
 
16
20
  def clear_env!(path)
17
21
  FileUtils.remove_entry("#{RSPEC_ROOT}/dummy/#{path}")
22
+ rescue Errno::EACCES
23
+ puts "Cannot remove #{path}"
24
+ end
25
+
26
+ private
27
+
28
+ def json_data
29
+ <<~DATA
30
+ {
31
+ "nested": {
32
+ "test": "string"
33
+ },
34
+ "interpolated": "Value is {{a}}"
35
+ }
36
+ DATA
37
+ end
38
+
39
+ def yaml_data
40
+ <<~DATA
41
+ test: string
42
+ interpolated: Value is {{a}}
43
+ DATA
18
44
  end
19
45
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: messages_dictionary
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ilya Krukowski
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-11-21 00:00:00.000000000 Z
12
+ date: 2022-11-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: hashie
@@ -53,6 +53,20 @@ dependencies:
53
53
  - - "~>"
54
54
  - !ruby/object:Gem::Version
55
55
  version: '0.1'
56
+ - !ruby/object:Gem::Dependency
57
+ name: json
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '2'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '2'
56
70
  - !ruby/object:Gem::Dependency
57
71
  name: rake
58
72
  requirement: !ruby/object:Gem::Requirement
@@ -179,6 +193,7 @@ files:
179
193
  - README.md
180
194
  - Rakefile
181
195
  - lib/messages_dictionary.rb
196
+ - lib/messages_dictionary/config.rb
182
197
  - lib/messages_dictionary/injector.rb
183
198
  - lib/messages_dictionary/utils/dict.rb
184
199
  - lib/messages_dictionary/utils/string_utils.rb