tty-config 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +17 -0
- data/README.md +332 -38
- data/lib/tty/config.rb +269 -67
- data/lib/tty/config/version.rb +3 -1
- data/spec/spec_helper.rb +54 -0
- data/spec/unit/alias_setting_spec.rb +72 -0
- data/spec/unit/append_spec.rb +26 -0
- data/spec/unit/autoload_env_spec.rb +62 -0
- data/spec/unit/delete_spec.rb +22 -0
- data/spec/unit/exist_spec.rb +24 -0
- data/spec/unit/fetch_spec.rb +45 -0
- data/spec/unit/generate_spec.rb +70 -0
- data/spec/unit/merge_spec.rb +13 -0
- data/spec/unit/new_spec.rb +6 -0
- data/spec/unit/normalize_hash_spec.rb +21 -0
- data/spec/unit/read_spec.rb +109 -0
- data/spec/unit/remove_spec.rb +16 -0
- data/spec/unit/set_from_env_spec.rb +78 -0
- data/spec/unit/set_if_empty_spec.rb +26 -0
- data/spec/unit/set_spec.rb +62 -0
- data/spec/unit/validate_spec.rb +76 -0
- data/spec/unit/write_spec.rb +197 -0
- data/tty-config.gemspec +4 -3
- metadata +35 -9
- data/.gitignore +0 -12
- data/.rspec +0 -3
- data/.travis.yml +0 -23
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -16
- data/appveyor.yml +0 -23
data/lib/tty/config.rb
CHANGED
@@ -30,6 +30,65 @@ module TTY
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
+
# Generate file content based on the data hash
|
34
|
+
#
|
35
|
+
# @param [Hash] data
|
36
|
+
#
|
37
|
+
# @return [String]
|
38
|
+
# the file content
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
def self.generate(data, separator: '=')
|
42
|
+
content = []
|
43
|
+
values = {}
|
44
|
+
sections = {}
|
45
|
+
|
46
|
+
data.keys.sort.each do |key|
|
47
|
+
val = data[key]
|
48
|
+
if val.is_a?(NilClass)
|
49
|
+
next
|
50
|
+
elsif val.is_a?(Hash) ||
|
51
|
+
(val.is_a?(Array) && val.first.is_a?(Hash))
|
52
|
+
sections[key] = val
|
53
|
+
elsif val.is_a?(Array)
|
54
|
+
values[key] = val.join(',')
|
55
|
+
else
|
56
|
+
values[key] = val
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# values
|
61
|
+
values.each do |key, val|
|
62
|
+
content << "#{key} #{separator} #{val}"
|
63
|
+
end
|
64
|
+
content << '' unless values.empty?
|
65
|
+
|
66
|
+
# sections
|
67
|
+
sections.each do |section, object|
|
68
|
+
next if object.empty? # only add section if values present
|
69
|
+
|
70
|
+
content << "[#{section}]"
|
71
|
+
if object.is_a?(Array)
|
72
|
+
object = object.reduce({}, :merge!)
|
73
|
+
end
|
74
|
+
object.each do |key, val|
|
75
|
+
val = val.join(',') if val.is_a?(Array)
|
76
|
+
content << "#{key} #{separator} #{val}" if val
|
77
|
+
end
|
78
|
+
content << ''
|
79
|
+
end
|
80
|
+
content.join("\n")
|
81
|
+
end
|
82
|
+
|
83
|
+
# Storage for suppported format & extensions pairs
|
84
|
+
# @api public
|
85
|
+
EXTENSIONS = {
|
86
|
+
yaml: %w(.yaml .yml),
|
87
|
+
json: %w(.json),
|
88
|
+
toml: %w(.toml),
|
89
|
+
ini: %w(.ini .cnf .conf .cfg .cf)
|
90
|
+
}.freeze
|
91
|
+
|
33
92
|
# A collection of config paths
|
34
93
|
# @api public
|
35
94
|
attr_reader :location_paths
|
@@ -50,14 +109,25 @@ module TTY
|
|
50
109
|
# @api public
|
51
110
|
attr_reader :validators
|
52
111
|
|
53
|
-
|
54
|
-
|
112
|
+
# The prefix used for searching ENV variables
|
113
|
+
# @api public
|
114
|
+
attr_accessor :env_prefix
|
115
|
+
|
116
|
+
# Create a configuration instance
|
117
|
+
#
|
118
|
+
# @api public
|
119
|
+
def initialize(**settings)
|
55
120
|
@settings = settings
|
121
|
+
@location_paths = []
|
56
122
|
@validators = {}
|
57
123
|
@filename = 'config'
|
58
124
|
@extname = '.yml'
|
59
|
-
@extensions =
|
125
|
+
@extensions = EXTENSIONS.values.flatten << ''
|
60
126
|
@key_delim = '.'
|
127
|
+
@envs = {}
|
128
|
+
@env_prefix = ''
|
129
|
+
@autoload_env = false
|
130
|
+
@aliases = {}
|
61
131
|
|
62
132
|
yield(self) if block_given?
|
63
133
|
end
|
@@ -88,6 +158,20 @@ module TTY
|
|
88
158
|
@location_paths.unshift(path)
|
89
159
|
end
|
90
160
|
|
161
|
+
# Check if env variables are auto loaded
|
162
|
+
#
|
163
|
+
# @api public
|
164
|
+
def autoload_env?
|
165
|
+
@autoload_env == true
|
166
|
+
end
|
167
|
+
|
168
|
+
# Auto load env variables
|
169
|
+
#
|
170
|
+
# @api public
|
171
|
+
def autoload_env
|
172
|
+
@autoload_env = true
|
173
|
+
end
|
174
|
+
|
91
175
|
# Set a value for a composite key and overrides any existing keys.
|
92
176
|
# Keys are case-insensitive
|
93
177
|
#
|
@@ -123,6 +207,34 @@ module TTY
|
|
123
207
|
block ? set(*keys, &block) : set(*keys, value: value)
|
124
208
|
end
|
125
209
|
|
210
|
+
# Bind a key to ENV variable
|
211
|
+
#
|
212
|
+
# @example
|
213
|
+
# set_from_env(:host)
|
214
|
+
# set_from_env(:foo, :bar) { 'HOST' }
|
215
|
+
#
|
216
|
+
# @param [Array[String]] keys
|
217
|
+
# the keys to bind to ENV variables
|
218
|
+
#
|
219
|
+
# @api public
|
220
|
+
def set_from_env(*keys, &block)
|
221
|
+
assert_keys_with_block(convert_to_keys(keys), block)
|
222
|
+
key = flatten_keys(keys)
|
223
|
+
env_key = block.nil? ? key : block.()
|
224
|
+
env_key = to_env_key(env_key)
|
225
|
+
@envs[key.to_s.downcase] = env_key
|
226
|
+
end
|
227
|
+
|
228
|
+
# Convert config key to standard ENV var name
|
229
|
+
#
|
230
|
+
# @param [String] key
|
231
|
+
#
|
232
|
+
# @api private
|
233
|
+
def to_env_key(key)
|
234
|
+
env_key = key.to_s.upcase
|
235
|
+
@env_prefix == '' ? env_key : "#{@env_prefix.to_s.upcase}_#{env_key}"
|
236
|
+
end
|
237
|
+
|
126
238
|
# Fetch value under a composite key
|
127
239
|
#
|
128
240
|
# @param [Array[String|Symbol]] keys
|
@@ -131,9 +243,21 @@ module TTY
|
|
131
243
|
#
|
132
244
|
# @api public
|
133
245
|
def fetch(*keys, default: nil, &block)
|
246
|
+
# check alias
|
247
|
+
real_key = @aliases[flatten_keys(keys)]
|
248
|
+
keys = real_key.split(key_delim) if real_key
|
249
|
+
|
134
250
|
keys = convert_to_keys(keys)
|
251
|
+
env_key = autoload_env? ? to_env_key(keys[0]) : @envs[flatten_keys(keys)]
|
252
|
+
# first try settings
|
135
253
|
value = deep_fetch(@settings, *keys)
|
254
|
+
# then try ENV var
|
255
|
+
if value.nil? && env_key
|
256
|
+
value = ENV[env_key]
|
257
|
+
end
|
258
|
+
# then try default
|
136
259
|
value = block || default if value.nil?
|
260
|
+
|
137
261
|
while callable_without_params?(value)
|
138
262
|
value = value.call
|
139
263
|
end
|
@@ -182,7 +306,38 @@ module TTY
|
|
182
306
|
deep_delete(*keys, @settings)
|
183
307
|
end
|
184
308
|
|
185
|
-
#
|
309
|
+
# Define an alias to a nested key
|
310
|
+
#
|
311
|
+
# @example
|
312
|
+
# alias_setting(:foo, to: :bar)
|
313
|
+
#
|
314
|
+
# @param [Array[String]] keys
|
315
|
+
# the alias key
|
316
|
+
#
|
317
|
+
# @api public
|
318
|
+
def alias_setting(*keys, to: nil)
|
319
|
+
flat_setting = flatten_keys(keys)
|
320
|
+
alias_keys = Array(to)
|
321
|
+
alias_key = flatten_keys(alias_keys)
|
322
|
+
|
323
|
+
if alias_key == flat_setting
|
324
|
+
raise ArgumentError, 'Alias matches setting key'
|
325
|
+
end
|
326
|
+
|
327
|
+
if fetch(alias_key)
|
328
|
+
raise ArgumentError, 'Setting already exists with an alias ' \
|
329
|
+
"'#{alias_keys.map(&:inspect).join(', ')}'"
|
330
|
+
end
|
331
|
+
|
332
|
+
@aliases[alias_key] = flat_setting
|
333
|
+
end
|
334
|
+
|
335
|
+
# Register a validation rule for a nested key
|
336
|
+
#
|
337
|
+
# @param [Array[String]] keys
|
338
|
+
# a deep nested keys
|
339
|
+
# @param [Proc] validator
|
340
|
+
# the logic to use to validate given nested key
|
186
341
|
#
|
187
342
|
# @api public
|
188
343
|
def validate(*keys, &validator)
|
@@ -192,26 +347,8 @@ module TTY
|
|
192
347
|
validators[key] = values
|
193
348
|
end
|
194
349
|
|
195
|
-
#
|
350
|
+
# Find configuration file matching filename and extension
|
196
351
|
#
|
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
|
205
|
-
#
|
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
352
|
# @api private
|
216
353
|
def find_file
|
217
354
|
@location_paths.each do |location_path|
|
@@ -227,9 +364,10 @@ module TTY
|
|
227
364
|
# @return [Boolean]
|
228
365
|
#
|
229
366
|
# @api public
|
230
|
-
def
|
367
|
+
def exist?
|
231
368
|
!find_file.nil?
|
232
369
|
end
|
370
|
+
alias persisted? exist?
|
233
371
|
|
234
372
|
# Find and read a configuration file.
|
235
373
|
#
|
@@ -239,17 +377,20 @@ module TTY
|
|
239
377
|
# @param [String] file
|
240
378
|
# the path to the configuration file to be read
|
241
379
|
#
|
380
|
+
# @param [String] format
|
381
|
+
# the format to read configuration in
|
382
|
+
#
|
242
383
|
# @raise [TTY::Config::ReadError]
|
243
384
|
#
|
244
385
|
# @api public
|
245
|
-
def read(file = find_file)
|
386
|
+
def read(file = find_file, format: :auto)
|
246
387
|
if file.nil?
|
247
|
-
raise ReadError,
|
388
|
+
raise ReadError, 'No file found to read configuration from!'
|
248
389
|
elsif !::File.exist?(file)
|
249
390
|
raise ReadError, "Configuration file `#{file}` does not exist!"
|
250
391
|
end
|
251
392
|
|
252
|
-
merge(unmarshal(file))
|
393
|
+
merge(unmarshal(file, format: format))
|
253
394
|
end
|
254
395
|
|
255
396
|
# Write current configuration to a file.
|
@@ -258,11 +399,11 @@ module TTY
|
|
258
399
|
# the path to a file
|
259
400
|
#
|
260
401
|
# @api public
|
261
|
-
def write(file = find_file, force: false)
|
402
|
+
def write(file = find_file, force: false, format: :auto)
|
262
403
|
if file && ::File.exist?(file)
|
263
404
|
if !force
|
264
405
|
raise WriteError, "File `#{file}` already exists. " \
|
265
|
-
|
406
|
+
'Use :force option to overwrite.'
|
266
407
|
elsif !::File.writable?(file)
|
267
408
|
raise WriteError, "Cannot write to #{file}."
|
268
409
|
end
|
@@ -273,7 +414,8 @@ module TTY
|
|
273
414
|
file = ::File.join(dir, "#{filename}#{@extname}")
|
274
415
|
end
|
275
416
|
|
276
|
-
marshal(file, @settings)
|
417
|
+
content = marshal(file, @settings, format: format)
|
418
|
+
::File.write(file, content)
|
277
419
|
end
|
278
420
|
|
279
421
|
# Current configuration
|
@@ -286,16 +428,57 @@ module TTY
|
|
286
428
|
|
287
429
|
private
|
288
430
|
|
431
|
+
# Ensure that value is set either through parameter or block
|
432
|
+
#
|
433
|
+
# @api private
|
434
|
+
def assert_either_value_or_block(value, block)
|
435
|
+
if value.nil? && block.nil?
|
436
|
+
raise ArgumentError, 'Need to set either value or block'
|
437
|
+
elsif !(value.nil? || block.nil?)
|
438
|
+
raise ArgumentError, "Can't set both value and block"
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
# Check if object is a proc with no arguments
|
443
|
+
#
|
444
|
+
# @return [Boolean]
|
445
|
+
#
|
446
|
+
# @api private
|
289
447
|
def callable_without_params?(object)
|
290
448
|
object.respond_to?(:call) &&
|
291
449
|
(!object.respond_to?(:arity) || object.arity.zero?)
|
292
450
|
end
|
293
451
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
452
|
+
# Wrap callback in a proc object that includes validation
|
453
|
+
# that will be performed at point when a new proc is invoked.
|
454
|
+
#
|
455
|
+
# @param [String] key
|
456
|
+
# @param [Proc] callback
|
457
|
+
#
|
458
|
+
# @api private
|
459
|
+
def delay_validation(key, callback)
|
460
|
+
-> do
|
461
|
+
val = callback.()
|
462
|
+
assert_valid(key, val)
|
463
|
+
val
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
# Check if key passes all registered validations for a key
|
468
|
+
#
|
469
|
+
# @param [String] key
|
470
|
+
# @param [Object] value
|
471
|
+
#
|
472
|
+
# @api private
|
473
|
+
def assert_valid(key, value)
|
474
|
+
validators[key].each do |validator|
|
475
|
+
validator.call(key, value)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
def assert_keys_with_block(keys, block)
|
480
|
+
if keys.size > 1 && block.nil?
|
481
|
+
raise ArgumentError, 'Need to set env var in block'
|
299
482
|
end
|
300
483
|
end
|
301
484
|
|
@@ -407,61 +590,80 @@ module TTY
|
|
407
590
|
end
|
408
591
|
|
409
592
|
# @api private
|
410
|
-
def unmarshal(file)
|
411
|
-
|
412
|
-
|
413
|
-
self.
|
414
|
-
|
593
|
+
def unmarshal(file, format: :auto)
|
594
|
+
file_ext = ::File.extname(file)
|
595
|
+
ext = (format == :auto ? file_ext : ".#{format}")
|
596
|
+
self.extname = file_ext
|
597
|
+
self.filename = ::File.basename(file, file_ext)
|
415
598
|
|
416
599
|
case ext
|
417
|
-
when
|
418
|
-
|
600
|
+
when *EXTENSIONS[:yaml]
|
601
|
+
load_read_dep('yaml', ext)
|
419
602
|
if YAML.respond_to?(:safe_load)
|
420
603
|
YAML.safe_load(File.read(file))
|
421
604
|
else
|
422
605
|
YAML.load(File.read(file))
|
423
606
|
end
|
424
|
-
when
|
425
|
-
|
607
|
+
when *EXTENSIONS[:json]
|
608
|
+
load_read_dep('json', ext)
|
426
609
|
JSON.parse(File.read(file))
|
427
|
-
when
|
428
|
-
|
429
|
-
require 'toml'
|
610
|
+
when *EXTENSIONS[:toml]
|
611
|
+
load_read_dep('toml', ext)
|
430
612
|
TOML.load(::File.read(file))
|
613
|
+
when *EXTENSIONS[:ini]
|
614
|
+
load_read_dep('inifile', ext)
|
615
|
+
ini = IniFile.load(file).to_h
|
616
|
+
global = ini.delete('global')
|
617
|
+
ini.merge!(global)
|
431
618
|
else
|
432
619
|
raise ReadError, "Config file format `#{ext}` is not supported."
|
433
620
|
end
|
621
|
+
end
|
622
|
+
|
623
|
+
# Try loading read dependency
|
624
|
+
# @api private
|
625
|
+
def load_read_dep(gem_name, format)
|
626
|
+
require gem_name
|
434
627
|
rescue LoadError
|
435
|
-
puts "Please install `#{gem_name}`"
|
436
628
|
raise ReadError, "Gem `#{gem_name}` is missing. Please install it " \
|
437
|
-
"to read #{
|
629
|
+
"to read #{format} configuration format."
|
438
630
|
end
|
439
631
|
|
632
|
+
# Marshal data hash into a configuration file content
|
633
|
+
#
|
634
|
+
# @return [String]
|
635
|
+
#
|
440
636
|
# @api private
|
441
|
-
def marshal(file, data)
|
442
|
-
|
443
|
-
|
444
|
-
self.
|
445
|
-
|
637
|
+
def marshal(file, data, format: :auto)
|
638
|
+
file_ext = ::File.extname(file)
|
639
|
+
ext = (format == :auto ? file_ext : ".#{format}")
|
640
|
+
self.extname = file_ext
|
641
|
+
self.filename = ::File.basename(file, file_ext)
|
446
642
|
|
447
643
|
case ext
|
448
|
-
when
|
449
|
-
|
450
|
-
|
451
|
-
when
|
452
|
-
|
453
|
-
|
454
|
-
when
|
455
|
-
|
456
|
-
|
457
|
-
|
644
|
+
when *EXTENSIONS[:yaml]
|
645
|
+
load_write_dep('yaml', ext)
|
646
|
+
YAML.dump(self.class.normalize_hash(data, :to_s))
|
647
|
+
when *EXTENSIONS[:json]
|
648
|
+
load_write_dep('json', ext)
|
649
|
+
JSON.pretty_generate(data)
|
650
|
+
when *EXTENSIONS[:toml]
|
651
|
+
load_write_dep('toml', ext)
|
652
|
+
TOML::Generator.new(data).body
|
653
|
+
when *EXTENSIONS[:ini]
|
654
|
+
Config.generate(data)
|
458
655
|
else
|
459
656
|
raise WriteError, "Config file format `#{ext}` is not supported."
|
460
657
|
end
|
658
|
+
end
|
659
|
+
|
660
|
+
# Try loading write depedency
|
661
|
+
# @api private
|
662
|
+
def load_write_dep(gem_name, format)
|
663
|
+
require gem_name
|
461
664
|
rescue LoadError
|
462
|
-
|
463
|
-
|
464
|
-
"to read #{ext} configuration format."
|
665
|
+
raise WriteError, "Gem `#{gem_name}` is missing. Please install it " \
|
666
|
+
"to read #{format} configuration format."
|
465
667
|
end
|
466
668
|
end # Config
|
467
669
|
end # TTY
|