tty-config 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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