tty-config 0.2.0 → 0.5.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.
data/lib/tty-config.rb CHANGED
@@ -1 +1 @@
1
- require_relative 'tty/config'
1
+ require_relative "tty/config"
data/lib/tty/config.rb CHANGED
@@ -1,11 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pathname'
3
+ require "pathname"
4
4
 
5
- require_relative 'config/version'
5
+ require_relative "config/version"
6
+ require_relative "config/marshallers"
7
+ require_relative "config/marshallers/ini_marshaller"
8
+ require_relative "config/marshallers/json_marshaller"
9
+ require_relative "config/marshallers/yaml_marshaller"
10
+ require_relative "config/marshallers/toml_marshaller"
11
+ require_relative "config/marshallers/hcl_marshaller"
12
+ require_relative "config/marshallers/java_props_marshaller"
6
13
 
7
14
  module TTY
15
+ # Responsible for managing application configuration
16
+ #
17
+ # @api public
8
18
  class Config
19
+ include Marshallers
20
+
21
+ # Error raised when failed to load a dependency
22
+ DependencyLoadError = Class.new(StandardError)
9
23
  # Error raised when key fails validation
10
24
  ReadError = Class.new(StandardError)
11
25
  # Error raised when issues writing configuration to a file
@@ -15,18 +29,30 @@ module TTY
15
29
  # Error raised when validation assertion fails
16
30
  ValidationError = Class.new(StandardError)
17
31
 
32
+ # Coerce a hash object into Config instance
33
+ #
34
+ # @return [TTY::Config]
35
+ #
36
+ # @api private
18
37
  def self.coerce(hash, &block)
19
38
  new(normalize_hash(hash), &block)
20
39
  end
21
40
 
22
41
  # Convert string keys via method
23
42
  #
43
+ # @param [Hash] hash
44
+ # the hash to normalize keys for
45
+ # @param [Symbol] method
46
+ # the method to use for converting keys
47
+ #
48
+ # @return [Hash{Symbol => Object}]
49
+ # the converted hash
50
+ #
24
51
  # @api private
25
52
  def self.normalize_hash(hash, method = :to_sym)
26
- hash.reduce({}) do |acc, (key, val)|
53
+ hash.each_with_object({}) do |(key, val), acc|
27
54
  value = val.is_a?(::Hash) ? normalize_hash(val, method) : val
28
55
  acc[key.public_send(method)] = value
29
- acc
30
56
  end
31
57
  end
32
58
 
@@ -50,14 +76,36 @@ module TTY
50
76
  # @api public
51
77
  attr_reader :validators
52
78
 
79
+ # The prefix used for searching ENV variables
80
+ # @api public
81
+ attr_accessor :env_prefix
82
+
83
+ # The string used to separate parts in ENV variable name
84
+ # @api public
85
+ attr_accessor :env_separator
86
+
87
+ # Create a configuration instance
88
+ #
89
+ # @api public
53
90
  def initialize(settings = {})
54
- @location_paths = []
55
91
  @settings = settings
92
+ @location_paths = []
56
93
  @validators = {}
57
- @filename = 'config'
58
- @extname = '.yml'
59
- @extensions = ['.yaml', '.yml', '.json', '.toml']
60
- @key_delim = '.'
94
+ @filename = "config"
95
+ @extname = ".yml"
96
+ @key_delim = "."
97
+ @envs = {}
98
+ @env_prefix = ""
99
+ @env_separator = "_"
100
+ @autoload_env = false
101
+ @aliases = {}
102
+
103
+ register_marshaller :yaml, Marshallers::YAMLMarshaller
104
+ register_marshaller :json, Marshallers::JSONMarshaller
105
+ register_marshaller :toml, Marshallers::TOMLMarshaller
106
+ register_marshaller :ini, Marshallers::INIMarshaller
107
+ register_marshaller :hcl, Marshallers::HCLMarshaller
108
+ register_marshaller :jprops, Marshallers::JavaPropsMarshaller
61
109
 
62
110
  yield(self) if block_given?
63
111
  end
@@ -68,14 +116,23 @@ module TTY
68
116
  #
69
117
  # api public
70
118
  def extname=(name)
71
- unless @extensions.include?(name)
119
+ unless extensions.include?(name)
72
120
  raise UnsupportedExtError, "Config file format `#{name}` is not supported."
73
121
  end
122
+
74
123
  @extname = name
75
124
  end
76
125
 
77
126
  # Add path to locations to search in
78
127
  #
128
+ # @example
129
+ # append_path(Dir.pwd)
130
+ #
131
+ # @param [String] path
132
+ # the path to append
133
+ #
134
+ # @return [Array<String>]
135
+ #
79
136
  # @api public
80
137
  def append_path(path)
81
138
  @location_paths << path
@@ -83,14 +140,55 @@ module TTY
83
140
 
84
141
  # Insert location path at the begining
85
142
  #
143
+ # @example
144
+ # prepend_path(Dir.pwd)
145
+ #
146
+ # @param [String] path
147
+ # the path to prepend
148
+ #
149
+ # @return [Array<String>]
150
+ #
86
151
  # @api public
87
152
  def prepend_path(path)
88
153
  @location_paths.unshift(path)
89
154
  end
90
155
 
91
- # Set a value for a composite key and overrides any existing keys.
156
+ # Check if env variables are auto loaded
157
+ #
158
+ # @return [Boolean]
159
+ #
160
+ # @api public
161
+ def autoload_env?
162
+ @autoload_env == true
163
+ end
164
+
165
+ # Auto load env variables
166
+ #
167
+ # @api public
168
+ def autoload_env
169
+ @autoload_env = true
170
+ end
171
+
172
+ # Set a value for a composite key and overrides any existing keys
92
173
  # Keys are case-insensitive
93
174
  #
175
+ # @example
176
+ # set(:foo, :bar, :baz, value: 2)
177
+ #
178
+ # @example
179
+ # set(:foo, :bar, :baz) { 2 }
180
+ #
181
+ # @example
182
+ # set("foo.bar.baz", value: 2)
183
+ #
184
+ # @param [Array<String, Symbol>, String] keys
185
+ # the nested key to set value for
186
+ # @param [Object] value
187
+ # the value to set
188
+ #
189
+ # @return [Object]
190
+ # the set value
191
+ #
94
192
  # @api public
95
193
  def set(*keys, value: nil, &block)
96
194
  assert_either_value_or_block(value, block)
@@ -114,45 +212,122 @@ module TTY
114
212
 
115
213
  # Set a value for a composite key if not present already
116
214
  #
117
- # @param [Array[String|Symbol]] keys
215
+ # @example
216
+ # set_if_empty(:foo, :bar, :baz, value: 2)
217
+ #
218
+ # @param [Array<String, Symbol>] keys
118
219
  # the keys to set value for
220
+ # @param [Object] value
221
+ # the value to set
222
+ #
223
+ # @return [Object, nil]
224
+ # the set value or nil
119
225
  #
120
226
  # @api public
121
227
  def set_if_empty(*keys, value: nil, &block)
122
- return unless deep_find(@settings, keys.last.to_s).nil?
228
+ keys = convert_to_keys(keys)
229
+ return unless deep_fetch(@settings, *keys).nil?
230
+
123
231
  block ? set(*keys, &block) : set(*keys, value: value)
124
232
  end
125
233
 
234
+ # Bind a key to ENV variable
235
+ #
236
+ # @example
237
+ # set_from_env(:host)
238
+ # set_from_env(:foo, :bar) { 'HOST' }
239
+ #
240
+ # @param [Array<String>] keys
241
+ # the keys to bind to ENV variables
242
+ #
243
+ # @api public
244
+ def set_from_env(*keys, &block)
245
+ key = flatten_keys(keys)
246
+ env_key = block.nil? ? key : block.()
247
+ env_key = to_env_key(env_key)
248
+ @envs[key.to_s.downcase] = env_key
249
+ end
250
+
251
+ # Convert config key to standard ENV var name
252
+ #
253
+ # @param [String] key
254
+ #
255
+ # @return [String]
256
+ #
257
+ # @api private
258
+ def to_env_key(key)
259
+ env_key = key.to_s.gsub(key_delim, env_separator).upcase
260
+ if @env_prefix == ""
261
+ env_key
262
+ else
263
+ "#{@env_prefix.to_s.upcase}#{env_separator}#{env_key}"
264
+ end
265
+ end
266
+
126
267
  # Fetch value under a composite key
127
268
  #
128
- # @param [Array[String|Symbol]] keys
269
+ # @example
270
+ # fetch(:foo, :bar, :baz)
271
+ #
272
+ # @example
273
+ # fetch("foo.bar.baz")
274
+ #
275
+ # @param [Array<String, Symbol>, String] keys
129
276
  # the keys to get value at
130
277
  # @param [Object] default
278
+ # the default value
279
+ #
280
+ # @return [Object]
131
281
  #
132
282
  # @api public
133
283
  def fetch(*keys, default: nil, &block)
284
+ # check alias
285
+ real_key = @aliases[flatten_keys(keys)]
286
+ keys = real_key.split(key_delim) if real_key
287
+
134
288
  keys = convert_to_keys(keys)
289
+ env_key = autoload_env? ? to_env_key(keys[0]) : @envs[flatten_keys(keys)]
290
+ # first try settings
135
291
  value = deep_fetch(@settings, *keys)
292
+ # then try ENV var
293
+ if value.nil? && env_key
294
+ value = ENV[env_key]
295
+ end
296
+ # then try default
136
297
  value = block || default if value.nil?
298
+
137
299
  while callable_without_params?(value)
138
- value = value.call
300
+ value = value.()
139
301
  end
140
302
  value
141
303
  end
142
304
 
143
305
  # Merge in other configuration settings
144
306
  #
145
- # @param [Hash[Object]] other_settings
307
+ # @param [Hash{Symbol => Object]] other_settings
308
+ #
309
+ # @return [Hash, nil]
310
+ # the combined settings or nil
146
311
  #
147
312
  # @api public
148
313
  def merge(other_settings)
314
+ return unless other_settings.respond_to?(:to_hash)
315
+
149
316
  @settings = deep_merge(@settings, other_settings)
150
317
  end
151
318
 
152
319
  # Append values to an already existing nested key
153
320
  #
154
- # @param [Array[String|Symbol]] values
321
+ # @example
322
+ # append(1, 2, to: %i[foo bar])
323
+ #
324
+ # @param [Array<Object>] values
155
325
  # the values to append
326
+ # @param [Array<String, Symbol] to
327
+ # the nested key to append to
328
+ #
329
+ # @return [Array<Object>]
330
+ # the values for a nested key
156
331
  #
157
332
  # @api public
158
333
  def append(*values, to: nil)
@@ -162,27 +337,79 @@ module TTY
162
337
 
163
338
  # Remove a set of values from a nested key
164
339
  #
165
- # @param [Array[String|Symbol]] keys
166
- # the keys for a value removal
340
+ # @example
341
+ # remove(1, 2, from: :foo)
342
+ #
343
+ # @example
344
+ # remove(1, 2, from: %i[foo bar])
345
+ #
346
+ # @param [Array<Object>] values
347
+ # the values to remove from a nested key
348
+ # @param [Array<String, Symbol>, String] from
349
+ # the nested key to remove values from
167
350
  #
168
351
  # @api public
169
352
  def remove(*values, from: nil)
170
353
  keys = Array(from)
354
+ raise ArgumentError, "Need to set key to remove from" if keys.empty?
355
+
171
356
  set(*keys, value: Array(fetch(*keys)) - values)
172
357
  end
173
358
 
174
359
  # Delete a value from a nested key
175
360
  #
176
- # @param [Array[String|Symbol]] keys
361
+ # @example
362
+ # delete(:foo, :bar, :baz)
363
+ #
364
+ # @example
365
+ # delete(:unknown) { |key| "#{key} isn't set" }
366
+ #
367
+ # @param [Array<String, Symbol>] keys
177
368
  # the keys for a value deletion
178
369
  #
370
+ # @yield [key] Invoke the block with a missing key
371
+ #
372
+ # @return [Object]
373
+ # the deleted value(s)
374
+ #
179
375
  # @api public
180
- def delete(*keys)
376
+ def delete(*keys, &default)
181
377
  keys = convert_to_keys(keys)
182
- deep_delete(*keys, @settings)
378
+ deep_delete(*keys, @settings, &default)
379
+ end
380
+
381
+ # Define an alias to a nested key
382
+ #
383
+ # @example
384
+ # alias_setting(:foo, to: :bar)
385
+ #
386
+ # @param [Array<String>] keys
387
+ # the alias key
388
+ #
389
+ # @api public
390
+ def alias_setting(*keys, to: nil)
391
+ flat_setting = flatten_keys(keys)
392
+ alias_keys = Array(to)
393
+ alias_key = flatten_keys(alias_keys)
394
+
395
+ if alias_key == flat_setting
396
+ raise ArgumentError, "Alias matches setting key"
397
+ end
398
+
399
+ if fetch(alias_key)
400
+ raise ArgumentError, "Setting already exists with an alias " \
401
+ "'#{alias_keys.map(&:inspect).join(', ')}'"
402
+ end
403
+
404
+ @aliases[alias_key] = flat_setting
183
405
  end
184
406
 
185
- # Register validation for a nested key
407
+ # Register a validation rule for a nested key
408
+ #
409
+ # @param [Array<String>] keys
410
+ # a deep nested keys
411
+ # @param [Proc] validator
412
+ # the logic to use to validate given nested key
186
413
  #
187
414
  # @api public
188
415
  def validate(*keys, &validator)
@@ -192,26 +419,8 @@ module TTY
192
419
  validators[key] = values
193
420
  end
194
421
 
195
- # Check if key passes all registered validations
196
- #
197
- # @api private
198
- def assert_valid(key, value)
199
- validators[key].each do |validator|
200
- validator.call(key, value)
201
- end
202
- end
203
-
204
- # Delay key validation
422
+ # Find configuration file matching filename and extension
205
423
  #
206
- # @api private
207
- def delay_validation(key, callback)
208
- -> do
209
- val = callback.()
210
- assert_valid(key, val)
211
- val
212
- end
213
- end
214
-
215
424
  # @api private
216
425
  def find_file
217
426
  @location_paths.each do |location_path|
@@ -227,9 +436,10 @@ module TTY
227
436
  # @return [Boolean]
228
437
  #
229
438
  # @api public
230
- def persisted?
439
+ def exist?
231
440
  !find_file.nil?
232
441
  end
442
+ alias persisted? exist?
233
443
 
234
444
  # Find and read a configuration file.
235
445
  #
@@ -239,41 +449,72 @@ module TTY
239
449
  # @param [String] file
240
450
  # the path to the configuration file to be read
241
451
  #
452
+ # @param [String] format
453
+ # the format to read configuration in
454
+ #
242
455
  # @raise [TTY::Config::ReadError]
243
456
  #
244
457
  # @api public
245
- def read(file = find_file)
458
+ def read(file = find_file, format: :auto)
246
459
  if file.nil?
247
460
  raise ReadError, "No file found to read configuration from!"
248
461
  elsif !::File.exist?(file)
249
462
  raise ReadError, "Configuration file `#{file}` does not exist!"
250
463
  end
251
464
 
252
- merge(unmarshal(file))
465
+ set_file_metadata(file)
466
+
467
+ ext = (format == :auto ? extname : ".#{format}")
468
+ content = ::File.read(file)
469
+
470
+ merge(unmarshal(content, ext: ext))
253
471
  end
254
472
 
255
473
  # Write current configuration to a file.
256
474
  #
475
+ # @example
476
+ # write(force: true, create: true)
477
+ #
257
478
  # @param [String] file
258
- # the path to a file
479
+ # the file to write to
480
+ # @param [Boolean] create
481
+ # whether or not to create missing path directories, false by default
482
+ # @param [Boolean] force
483
+ # whether or not to overwrite existing configuration file, false by default
484
+ # @param [String] format
485
+ # the format name for the configuration file, :auto by defualt
486
+ # @param [String] path
487
+ # the custom path to use to write a file to
488
+ #
489
+ # @raise [TTY::Config::WriteError]
259
490
  #
260
491
  # @api public
261
- def write(file = find_file, force: false)
262
- if file && ::File.exist?(file)
263
- if !force
264
- raise WriteError, "File `#{file}` already exists. " \
265
- "Use :force option to overwrite."
266
- elsif !::File.writable?(file)
267
- raise WriteError, "Cannot write to #{file}."
268
- end
269
- end
492
+ def write(file = find_file, create: false, force: false, format: :auto,
493
+ path: nil)
494
+ file = fullpath(file, path)
495
+ check_can_write(file, force)
270
496
 
271
- if file.nil?
272
- dir = @location_paths.empty? ? Dir.pwd : @location_paths.first
273
- file = ::File.join(dir, "#{filename}#{@extname}")
274
- end
497
+ set_file_metadata(file)
498
+ ext = (format == :auto ? extname : ".#{format}")
499
+ content = marshal(@settings, ext: ext)
500
+ filepath = Pathname.new(file)
275
501
 
276
- marshal(file, @settings)
502
+ create_missing_dirs(filepath, create)
503
+ ::File.write(filepath, content)
504
+ end
505
+
506
+ # Set file name and extension
507
+ #
508
+ # @example
509
+ # set_file_metadata("config.yml")
510
+ #
511
+ # @param [File] file
512
+ # the file to set metadata for
513
+ #
514
+ # @api public
515
+ def set_file_metadata(file)
516
+ self.extname = ::File.extname(file)
517
+ self.filename = ::File.basename(file, extname)
277
518
  end
278
519
 
279
520
  # Current configuration
@@ -286,11 +527,9 @@ module TTY
286
527
 
287
528
  private
288
529
 
289
- def callable_without_params?(object)
290
- object.respond_to?(:call) &&
291
- (!object.respond_to?(:arity) || object.arity.zero?)
292
- end
293
-
530
+ # Ensure that value is set either through parameter or block
531
+ #
532
+ # @api private
294
533
  def assert_either_value_or_block(value, block)
295
534
  if value.nil? && block.nil?
296
535
  raise ArgumentError, "Need to set either value or block"
@@ -299,6 +538,49 @@ module TTY
299
538
  end
300
539
  end
301
540
 
541
+ # Check if object is a proc with no arguments
542
+ #
543
+ # @return [Boolean]
544
+ #
545
+ # @api private
546
+ def callable_without_params?(object)
547
+ object.respond_to?(:call) &&
548
+ (!object.respond_to?(:arity) || object.arity.zero?)
549
+ end
550
+
551
+ # Wrap callback in a proc object that includes validation
552
+ # that will be performed at point when a new proc is invoked.
553
+ #
554
+ # @param [String] key
555
+ # the key to set validation for
556
+ # @param [Proc] callback
557
+ # the callback to wrap
558
+ #
559
+ # @return [Proc]
560
+ #
561
+ # @api private
562
+ def delay_validation(key, callback)
563
+ -> do
564
+ val = callback.()
565
+ assert_valid(key, val)
566
+ val
567
+ end
568
+ end
569
+
570
+ # Check if key passes all registered validations for a key
571
+ #
572
+ # @param [String] key
573
+ # the key to validate a value for
574
+ # @param [Object] value
575
+ # the value to check
576
+ #
577
+ # @api private
578
+ def assert_valid(key, value)
579
+ validators[key].each do |validator|
580
+ validator.(key, value)
581
+ end
582
+ end
583
+
302
584
  # Set value under deeply nested keys
303
585
  #
304
586
  # The scan starts with the top level key and follows
@@ -307,12 +589,16 @@ module TTY
307
589
  #
308
590
  # @param [Hash] settings
309
591
  #
310
- # @param [Array[Object]]
592
+ # @param [Array<Object>] keys
311
593
  # the keys to nest
312
594
  #
595
+ # @return [Hash]
596
+ # the nested setting
597
+ #
313
598
  # @api private
314
599
  def deep_set(settings, *keys)
315
600
  return settings if keys.empty?
601
+
316
602
  key, *rest = *keys
317
603
  value = settings[key]
318
604
 
@@ -327,15 +613,13 @@ module TTY
327
613
  end
328
614
  end
329
615
 
330
- def deep_find(settings, key, found = nil)
331
- if settings.respond_to?(:key?) && settings.key?(key)
332
- settings[key]
333
- elsif settings.is_a?(Enumerable)
334
- settings.each { |obj| found = deep_find(obj, key) }
335
- found
336
- end
337
- end
338
-
616
+ # Convert key to an array of key elements
617
+ #
618
+ # @param [String, Array<String, Symbol>] keys
619
+ #
620
+ # @return [Array<String>]
621
+ #
622
+ # @api private
339
623
  def convert_to_keys(keys)
340
624
  first_key = keys[0]
341
625
  if first_key.to_s.include?(key_delim)
@@ -345,6 +629,18 @@ module TTY
345
629
  end
346
630
  end
347
631
 
632
+ # Convert nested key from an array to a string
633
+ #
634
+ # @example
635
+ # flatten_keys(%i[foo bar baz]) # => "foo.bar.baz"
636
+ #
637
+ # @param [Array<String, Symbol>] keys
638
+ # the nested key to convert
639
+ #
640
+ # @return [String]
641
+ # the delimited nested key
642
+ #
643
+ # @api private
348
644
  def flatten_keys(keys)
349
645
  first_key = keys[0]
350
646
  if first_key.to_s.include?(key_delim)
@@ -357,8 +653,12 @@ module TTY
357
653
  # Fetch value under deeply nested keys with indiffernt key access
358
654
  #
359
655
  # @param [Hash] settings
656
+ # the settings to search
657
+ # @param [Array<Object>] keys
658
+ # the nested key to look up
360
659
  #
361
- # @param [Array[Object]] keys
660
+ # @return [Object, nil]
661
+ # the value or nil
362
662
  #
363
663
  # @api private
364
664
  def deep_fetch(settings, *keys)
@@ -371,6 +671,14 @@ module TTY
371
671
  end
372
672
  end
373
673
 
674
+ # Merge two deeply nested hash objects
675
+ #
676
+ # @param [Hash] this_hash
677
+ # @param [Hash] other_hash
678
+ #
679
+ # @return [Hash]
680
+ # the merged hash object
681
+ #
374
682
  # @api private
375
683
  def deep_merge(this_hash, other_hash, &block)
376
684
  this_hash.merge(other_hash) do |key, this_val, other_val|
@@ -384,21 +692,41 @@ module TTY
384
692
  end
385
693
  end
386
694
 
695
+ # Delete a deeply nested key
696
+ #
697
+ # @param [Array<String>] keys
698
+ # the nested key to delete
699
+ # @param [Hash{String => Object}]
700
+ # the settings to delete key from
701
+ #
702
+ # @return [Object]
703
+ # the deleted object(s)
704
+ #
387
705
  # @api private
388
- def deep_delete(*keys, settings)
706
+ def deep_delete(*keys, settings, &default)
389
707
  key, *rest = keys
390
708
  value = settings[key]
391
- if !value.nil? && value.is_a?(::Hash)
392
- deep_delete(*rest, value)
709
+ if !rest.empty? && value.is_a?(::Hash)
710
+ deep_delete(*rest, value, &default)
393
711
  elsif !value.nil?
394
712
  settings.delete(key)
713
+ elsif default
714
+ default.(key)
395
715
  end
396
716
  end
397
717
 
718
+ # Search for a configuration file in a path
719
+ #
720
+ # @param [String] path
721
+ # the path to search
722
+ #
723
+ # @return [String, nil]
724
+ # the configuration file path or nil
725
+ #
398
726
  # @api private
399
727
  def search_in_path(path)
400
728
  path = Pathname.new(path)
401
- @extensions.each do |ext|
729
+ extensions.each do |ext|
402
730
  if ::File.exist?(path.join("#{filename}#{ext}").to_s)
403
731
  return path.join("#{filename}#{ext}").to_s
404
732
  end
@@ -406,62 +734,120 @@ module TTY
406
734
  nil
407
735
  end
408
736
 
737
+ # Create a full path to a configuration file
738
+ #
739
+ # @param [String] file
740
+ # the configuration file
741
+ # @param [String] path
742
+ # the path to configuration file
743
+ #
744
+ # @return [String]
745
+ # the full path to a file
746
+ #
409
747
  # @api private
410
- def unmarshal(file)
411
- ext = ::File.extname(file)
412
- self.extname = ext
413
- self.filename = ::File.basename(file, ext)
414
- gem_name = nil
415
-
416
- case ext
417
- when '.yaml', '.yml'
418
- require 'yaml'
419
- if YAML.respond_to?(:safe_load)
420
- YAML.safe_load(File.read(file))
421
- else
422
- YAML.load(File.read(file))
423
- end
424
- when '.json'
425
- require 'json'
426
- JSON.parse(File.read(file))
427
- when '.toml'
428
- gem_name = 'toml'
429
- require 'toml'
430
- TOML.load(::File.read(file))
748
+ def fullpath(file, path)
749
+ if file.nil?
750
+ dir = path || @location_paths.first || Dir.pwd
751
+ ::File.join(dir, "#{filename}#{@extname}")
752
+ elsif file && path
753
+ ::File.join(path, ::File.basename(file))
754
+ else
755
+ file
756
+ end
757
+ end
758
+
759
+ # Check if a file can be written to
760
+ #
761
+ # @param [String] file
762
+ # the configuration file
763
+ # @param [Boolean] force
764
+ # whether or not to force writing
765
+ #
766
+ # @raise [TTY::Config::WriteError]
767
+ #
768
+ # @return [nil]
769
+ #
770
+ # @api private
771
+ def check_can_write(file, force)
772
+ return unless file && ::File.exist?(file)
773
+
774
+ if !force
775
+ raise WriteError, "File `#{file}` already exists. " \
776
+ "Use :force option to overwrite."
777
+ elsif !::File.writable?(file)
778
+ raise WriteError, "Cannot write to #{file}."
779
+ end
780
+ end
781
+
782
+ # Create any missing directories
783
+ #
784
+ # @param [Pathname] filepath
785
+ # the file path
786
+ # @param [Boolean] create
787
+ # whether or not to create missing directories
788
+ #
789
+ # @raise [TTY::Config::WriteError]
790
+ #
791
+ # @return [nil]
792
+ #
793
+ # @api private
794
+ def create_missing_dirs(filepath, create)
795
+ if !filepath.dirname.exist? && !create
796
+ raise WriteError, "Directory `#{filepath.dirname}` doesn't exist. " \
797
+ "Use :create option to create missing directories."
798
+ else
799
+ filepath.dirname.mkpath
800
+ end
801
+ end
802
+
803
+ # Crate a marshaller instance based on the extension name
804
+ #
805
+ # @param [String] ext
806
+ # the extension name
807
+ #
808
+ # @return [nil, Marshaller]
809
+ #
810
+ # @api private
811
+ def create_marshaller(ext)
812
+ marshaller = marshallers.find { |marsh| marsh.ext.include?(ext) }
813
+
814
+ return nil if marshaller.nil?
815
+
816
+ marshaller.new
817
+ end
818
+
819
+ # Unmarshal content into a hash object
820
+ #
821
+ # @param [String] content
822
+ # the content to convert into a hash object
823
+ #
824
+ # @return [Hash{String => Object}]
825
+ #
826
+ # @api private
827
+ def unmarshal(content, ext: nil)
828
+ ext ||= extname
829
+ if marshaller = create_marshaller(ext)
830
+ marshaller.unmarshal(content)
431
831
  else
432
832
  raise ReadError, "Config file format `#{ext}` is not supported."
433
833
  end
434
- rescue LoadError
435
- puts "Please install `#{gem_name}`"
436
- raise ReadError, "Gem `#{gem_name}` is missing. Please install it " \
437
- "to read #{ext} configuration format."
438
834
  end
439
835
 
836
+ # Marshal hash object into a configuration file content
837
+ #
838
+ # @param [Hash{String => Object}] object
839
+ # the object to convert to string
840
+ #
841
+ # @return [String]
842
+ #
440
843
  # @api private
441
- def marshal(file, data)
442
- ext = ::File.extname(file)
443
- self.extname = ext
444
- self.filename = ::File.basename(file, ext)
445
- gem_name = nil
446
-
447
- case ext
448
- when '.yaml', '.yml'
449
- require 'yaml'
450
- ::File.write(file, YAML.dump(self.class.normalize_hash(data, :to_s)))
451
- when '.json'
452
- require 'json'
453
- ::File.write(file, JSON.pretty_generate(data))
454
- when '.toml'
455
- gem_name = 'toml'
456
- require 'toml'
457
- ::File.write(file, TOML::Generator.new(data).body)
844
+ def marshal(object, ext: nil)
845
+ ext ||= extname
846
+ if marshaller = create_marshaller(ext)
847
+ marshaller.marshal(object)
458
848
  else
459
849
  raise WriteError, "Config file format `#{ext}` is not supported."
460
850
  end
461
- rescue LoadError
462
- puts "Please install `#{gem_name}`"
463
- raise ReadError, "Gem `#{gem_name}` is missing. Please install it " \
464
- "to read #{ext} configuration format."
465
851
  end
466
852
  end # Config
467
853
  end # TTY