tty-config 0.4.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
@@ -12,6 +12,9 @@ require_relative "config/marshallers/hcl_marshaller"
12
12
  require_relative "config/marshallers/java_props_marshaller"
13
13
 
14
14
  module TTY
15
+ # Responsible for managing application configuration
16
+ #
17
+ # @api public
15
18
  class Config
16
19
  include Marshallers
17
20
 
@@ -26,18 +29,30 @@ module TTY
26
29
  # Error raised when validation assertion fails
27
30
  ValidationError = Class.new(StandardError)
28
31
 
32
+ # Coerce a hash object into Config instance
33
+ #
34
+ # @return [TTY::Config]
35
+ #
36
+ # @api private
29
37
  def self.coerce(hash, &block)
30
38
  new(normalize_hash(hash), &block)
31
39
  end
32
40
 
33
41
  # Convert string keys via method
34
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
+ #
35
51
  # @api private
36
52
  def self.normalize_hash(hash, method = :to_sym)
37
- hash.reduce({}) do |acc, (key, val)|
53
+ hash.each_with_object({}) do |(key, val), acc|
38
54
  value = val.is_a?(::Hash) ? normalize_hash(val, method) : val
39
55
  acc[key.public_send(method)] = value
40
- acc
41
56
  end
42
57
  end
43
58
 
@@ -65,6 +80,10 @@ module TTY
65
80
  # @api public
66
81
  attr_accessor :env_prefix
67
82
 
83
+ # The string used to separate parts in ENV variable name
84
+ # @api public
85
+ attr_accessor :env_separator
86
+
68
87
  # Create a configuration instance
69
88
  #
70
89
  # @api public
@@ -72,11 +91,12 @@ module TTY
72
91
  @settings = settings
73
92
  @location_paths = []
74
93
  @validators = {}
75
- @filename = 'config'
76
- @extname = '.yml'
77
- @key_delim = '.'
94
+ @filename = "config"
95
+ @extname = ".yml"
96
+ @key_delim = "."
78
97
  @envs = {}
79
- @env_prefix = ''
98
+ @env_prefix = ""
99
+ @env_separator = "_"
80
100
  @autoload_env = false
81
101
  @aliases = {}
82
102
 
@@ -99,11 +119,20 @@ module TTY
99
119
  unless extensions.include?(name)
100
120
  raise UnsupportedExtError, "Config file format `#{name}` is not supported."
101
121
  end
122
+
102
123
  @extname = name
103
124
  end
104
125
 
105
126
  # Add path to locations to search in
106
127
  #
128
+ # @example
129
+ # append_path(Dir.pwd)
130
+ #
131
+ # @param [String] path
132
+ # the path to append
133
+ #
134
+ # @return [Array<String>]
135
+ #
107
136
  # @api public
108
137
  def append_path(path)
109
138
  @location_paths << path
@@ -111,6 +140,14 @@ module TTY
111
140
 
112
141
  # Insert location path at the begining
113
142
  #
143
+ # @example
144
+ # prepend_path(Dir.pwd)
145
+ #
146
+ # @param [String] path
147
+ # the path to prepend
148
+ #
149
+ # @return [Array<String>]
150
+ #
114
151
  # @api public
115
152
  def prepend_path(path)
116
153
  @location_paths.unshift(path)
@@ -118,6 +155,8 @@ module TTY
118
155
 
119
156
  # Check if env variables are auto loaded
120
157
  #
158
+ # @return [Boolean]
159
+ #
121
160
  # @api public
122
161
  def autoload_env?
123
162
  @autoload_env == true
@@ -130,9 +169,26 @@ module TTY
130
169
  @autoload_env = true
131
170
  end
132
171
 
133
- # Set a value for a composite key and overrides any existing keys.
172
+ # Set a value for a composite key and overrides any existing keys
134
173
  # Keys are case-insensitive
135
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
+ #
136
192
  # @api public
137
193
  def set(*keys, value: nil, &block)
138
194
  assert_either_value_or_block(value, block)
@@ -156,12 +212,22 @@ module TTY
156
212
 
157
213
  # Set a value for a composite key if not present already
158
214
  #
159
- # @param [Array[String|Symbol]] keys
215
+ # @example
216
+ # set_if_empty(:foo, :bar, :baz, value: 2)
217
+ #
218
+ # @param [Array<String, Symbol>] keys
160
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
161
225
  #
162
226
  # @api public
163
227
  def set_if_empty(*keys, value: nil, &block)
164
- 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
+
165
231
  block ? set(*keys, &block) : set(*keys, value: value)
166
232
  end
167
233
 
@@ -171,12 +237,11 @@ module TTY
171
237
  # set_from_env(:host)
172
238
  # set_from_env(:foo, :bar) { 'HOST' }
173
239
  #
174
- # @param [Array[String]] keys
240
+ # @param [Array<String>] keys
175
241
  # the keys to bind to ENV variables
176
242
  #
177
243
  # @api public
178
244
  def set_from_env(*keys, &block)
179
- assert_keys_with_block(convert_to_keys(keys), block)
180
245
  key = flatten_keys(keys)
181
246
  env_key = block.nil? ? key : block.()
182
247
  env_key = to_env_key(env_key)
@@ -187,17 +252,32 @@ module TTY
187
252
  #
188
253
  # @param [String] key
189
254
  #
255
+ # @return [String]
256
+ #
190
257
  # @api private
191
258
  def to_env_key(key)
192
- env_key = key.to_s.upcase
193
- @env_prefix == '' ? env_key : "#{@env_prefix.to_s.upcase}_#{env_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
194
265
  end
195
266
 
196
267
  # Fetch value under a composite key
197
268
  #
198
- # @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
199
276
  # the keys to get value at
200
277
  # @param [Object] default
278
+ # the default value
279
+ #
280
+ # @return [Object]
201
281
  #
202
282
  # @api public
203
283
  def fetch(*keys, default: nil, &block)
@@ -217,14 +297,17 @@ module TTY
217
297
  value = block || default if value.nil?
218
298
 
219
299
  while callable_without_params?(value)
220
- value = value.call
300
+ value = value.()
221
301
  end
222
302
  value
223
303
  end
224
304
 
225
305
  # Merge in other configuration settings
226
306
  #
227
- # @param [Hash[Object]] other_settings
307
+ # @param [Hash{Symbol => Object]] other_settings
308
+ #
309
+ # @return [Hash, nil]
310
+ # the combined settings or nil
228
311
  #
229
312
  # @api public
230
313
  def merge(other_settings)
@@ -235,8 +318,16 @@ module TTY
235
318
 
236
319
  # Append values to an already existing nested key
237
320
  #
238
- # @param [Array[String|Symbol]] values
321
+ # @example
322
+ # append(1, 2, to: %i[foo bar])
323
+ #
324
+ # @param [Array<Object>] values
239
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
240
331
  #
241
332
  # @api public
242
333
  def append(*values, to: nil)
@@ -246,24 +337,45 @@ module TTY
246
337
 
247
338
  # Remove a set of values from a nested key
248
339
  #
249
- # @param [Array[String|Symbol]] keys
250
- # 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
251
350
  #
252
351
  # @api public
253
352
  def remove(*values, from: nil)
254
353
  keys = Array(from)
354
+ raise ArgumentError, "Need to set key to remove from" if keys.empty?
355
+
255
356
  set(*keys, value: Array(fetch(*keys)) - values)
256
357
  end
257
358
 
258
359
  # Delete a value from a nested key
259
360
  #
260
- # @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
261
368
  # the keys for a value deletion
262
369
  #
370
+ # @yield [key] Invoke the block with a missing key
371
+ #
372
+ # @return [Object]
373
+ # the deleted value(s)
374
+ #
263
375
  # @api public
264
- def delete(*keys)
376
+ def delete(*keys, &default)
265
377
  keys = convert_to_keys(keys)
266
- deep_delete(*keys, @settings)
378
+ deep_delete(*keys, @settings, &default)
267
379
  end
268
380
 
269
381
  # Define an alias to a nested key
@@ -271,7 +383,7 @@ module TTY
271
383
  # @example
272
384
  # alias_setting(:foo, to: :bar)
273
385
  #
274
- # @param [Array[String]] keys
386
+ # @param [Array<String>] keys
275
387
  # the alias key
276
388
  #
277
389
  # @api public
@@ -281,11 +393,11 @@ module TTY
281
393
  alias_key = flatten_keys(alias_keys)
282
394
 
283
395
  if alias_key == flat_setting
284
- raise ArgumentError, 'Alias matches setting key'
396
+ raise ArgumentError, "Alias matches setting key"
285
397
  end
286
398
 
287
399
  if fetch(alias_key)
288
- raise ArgumentError, 'Setting already exists with an alias ' \
400
+ raise ArgumentError, "Setting already exists with an alias " \
289
401
  "'#{alias_keys.map(&:inspect).join(', ')}'"
290
402
  end
291
403
 
@@ -294,7 +406,7 @@ module TTY
294
406
 
295
407
  # Register a validation rule for a nested key
296
408
  #
297
- # @param [Array[String]] keys
409
+ # @param [Array<String>] keys
298
410
  # a deep nested keys
299
411
  # @param [Proc] validator
300
412
  # the logic to use to validate given nested key
@@ -345,7 +457,7 @@ module TTY
345
457
  # @api public
346
458
  def read(file = find_file, format: :auto)
347
459
  if file.nil?
348
- raise ReadError, 'No file found to read configuration from!'
460
+ raise ReadError, "No file found to read configuration from!"
349
461
  elsif !::File.exist?(file)
350
462
  raise ReadError, "Configuration file `#{file}` does not exist!"
351
463
  end
@@ -360,36 +472,44 @@ module TTY
360
472
 
361
473
  # Write current configuration to a file.
362
474
  #
475
+ # @example
476
+ # write(force: true, create: true)
477
+ #
363
478
  # @param [String] file
364
- # 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]
365
490
  #
366
491
  # @api public
367
- def write(file = find_file, force: false, format: :auto)
368
- if file && ::File.exist?(file)
369
- if !force
370
- raise WriteError, "File `#{file}` already exists. " \
371
- 'Use :force option to overwrite.'
372
- elsif !::File.writable?(file)
373
- raise WriteError, "Cannot write to #{file}."
374
- end
375
- end
376
-
377
- if file.nil?
378
- dir = @location_paths.empty? ? Dir.pwd : @location_paths.first
379
- file = ::File.join(dir, "#{filename}#{@extname}")
380
- 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)
381
496
 
382
497
  set_file_metadata(file)
383
-
384
498
  ext = (format == :auto ? extname : ".#{format}")
385
499
  content = marshal(@settings, ext: ext)
500
+ filepath = Pathname.new(file)
386
501
 
387
- ::File.write(file, content)
502
+ create_missing_dirs(filepath, create)
503
+ ::File.write(filepath, content)
388
504
  end
389
505
 
390
506
  # Set file name and extension
391
507
  #
508
+ # @example
509
+ # set_file_metadata("config.yml")
510
+ #
392
511
  # @param [File] file
512
+ # the file to set metadata for
393
513
  #
394
514
  # @api public
395
515
  def set_file_metadata(file)
@@ -412,7 +532,7 @@ module TTY
412
532
  # @api private
413
533
  def assert_either_value_or_block(value, block)
414
534
  if value.nil? && block.nil?
415
- raise ArgumentError, 'Need to set either value or block'
535
+ raise ArgumentError, "Need to set either value or block"
416
536
  elsif !(value.nil? || block.nil?)
417
537
  raise ArgumentError, "Can't set both value and block"
418
538
  end
@@ -432,7 +552,11 @@ module TTY
432
552
  # that will be performed at point when a new proc is invoked.
433
553
  #
434
554
  # @param [String] key
555
+ # the key to set validation for
435
556
  # @param [Proc] callback
557
+ # the callback to wrap
558
+ #
559
+ # @return [Proc]
436
560
  #
437
561
  # @api private
438
562
  def delay_validation(key, callback)
@@ -446,18 +570,14 @@ module TTY
446
570
  # Check if key passes all registered validations for a key
447
571
  #
448
572
  # @param [String] key
573
+ # the key to validate a value for
449
574
  # @param [Object] value
575
+ # the value to check
450
576
  #
451
577
  # @api private
452
578
  def assert_valid(key, value)
453
579
  validators[key].each do |validator|
454
- validator.call(key, value)
455
- end
456
- end
457
-
458
- def assert_keys_with_block(keys, block)
459
- if keys.size > 1 && block.nil?
460
- raise ArgumentError, 'Need to set env var in block'
580
+ validator.(key, value)
461
581
  end
462
582
  end
463
583
 
@@ -469,12 +589,16 @@ module TTY
469
589
  #
470
590
  # @param [Hash] settings
471
591
  #
472
- # @param [Array[Object]]
592
+ # @param [Array<Object>] keys
473
593
  # the keys to nest
474
594
  #
595
+ # @return [Hash]
596
+ # the nested setting
597
+ #
475
598
  # @api private
476
599
  def deep_set(settings, *keys)
477
600
  return settings if keys.empty?
601
+
478
602
  key, *rest = *keys
479
603
  value = settings[key]
480
604
 
@@ -489,15 +613,13 @@ module TTY
489
613
  end
490
614
  end
491
615
 
492
- def deep_find(settings, key, found = nil)
493
- if settings.respond_to?(:key?) && settings.key?(key)
494
- settings[key]
495
- elsif settings.is_a?(Enumerable)
496
- settings.each { |obj| found = deep_find(obj, key) }
497
- found
498
- end
499
- end
500
-
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
501
623
  def convert_to_keys(keys)
502
624
  first_key = keys[0]
503
625
  if first_key.to_s.include?(key_delim)
@@ -507,6 +629,18 @@ module TTY
507
629
  end
508
630
  end
509
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
510
644
  def flatten_keys(keys)
511
645
  first_key = keys[0]
512
646
  if first_key.to_s.include?(key_delim)
@@ -519,8 +653,12 @@ module TTY
519
653
  # Fetch value under deeply nested keys with indiffernt key access
520
654
  #
521
655
  # @param [Hash] settings
656
+ # the settings to search
657
+ # @param [Array<Object>] keys
658
+ # the nested key to look up
522
659
  #
523
- # @param [Array[Object]] keys
660
+ # @return [Object, nil]
661
+ # the value or nil
524
662
  #
525
663
  # @api private
526
664
  def deep_fetch(settings, *keys)
@@ -533,6 +671,14 @@ module TTY
533
671
  end
534
672
  end
535
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
+ #
536
682
  # @api private
537
683
  def deep_merge(this_hash, other_hash, &block)
538
684
  this_hash.merge(other_hash) do |key, this_val, other_val|
@@ -546,17 +692,37 @@ module TTY
546
692
  end
547
693
  end
548
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
+ #
549
705
  # @api private
550
- def deep_delete(*keys, settings)
706
+ def deep_delete(*keys, settings, &default)
551
707
  key, *rest = keys
552
708
  value = settings[key]
553
- if !value.nil? && value.is_a?(::Hash)
554
- deep_delete(*rest, value)
709
+ if !rest.empty? && value.is_a?(::Hash)
710
+ deep_delete(*rest, value, &default)
555
711
  elsif !value.nil?
556
712
  settings.delete(key)
713
+ elsif default
714
+ default.(key)
557
715
  end
558
716
  end
559
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
+ #
560
726
  # @api private
561
727
  def search_in_path(path)
562
728
  path = Pathname.new(path)
@@ -568,8 +734,77 @@ module TTY
568
734
  nil
569
735
  end
570
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
+ #
747
+ # @api private
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
+
571
803
  # Crate a marshaller instance based on the extension name
572
804
  #
805
+ # @param [String] ext
806
+ # the extension name
807
+ #
573
808
  # @return [nil, Marshaller]
574
809
  #
575
810
  # @api private
@@ -581,6 +816,13 @@ module TTY
581
816
  marshaller.new
582
817
  end
583
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
+ #
584
826
  # @api private
585
827
  def unmarshal(content, ext: nil)
586
828
  ext ||= extname
@@ -593,6 +835,9 @@ module TTY
593
835
 
594
836
  # Marshal hash object into a configuration file content
595
837
  #
838
+ # @param [Hash{String => Object}] object
839
+ # the object to convert to string
840
+ #
596
841
  # @return [String]
597
842
  #
598
843
  # @api private