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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +65 -1
- data/LICENSE.txt +1 -1
- data/README.md +547 -101
- data/lib/tty-config.rb +1 -1
- data/lib/tty/config.rb +512 -126
- data/lib/tty/config/dependency_loader.rb +55 -0
- data/lib/tty/config/generator.rb +57 -0
- data/lib/tty/config/marshaller.rb +66 -0
- data/lib/tty/config/marshaller_registry.rb +45 -0
- data/lib/tty/config/marshallers.rb +35 -0
- data/lib/tty/config/marshallers/hcl_marshaller.rb +28 -0
- data/lib/tty/config/marshallers/ini_marshaller.rb +31 -0
- data/lib/tty/config/marshallers/java_props_marshaller.rb +28 -0
- data/lib/tty/config/marshallers/json_marshaller.rb +28 -0
- data/lib/tty/config/marshallers/toml_marshaller.rb +28 -0
- data/lib/tty/config/marshallers/yaml_marshaller.rb +32 -0
- data/lib/tty/config/version.rb +5 -3
- metadata +75 -40
- 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/Rakefile +0 -8
- data/appveyor.yml +0 -23
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/tasks/console.rake +0 -11
- data/tasks/coverage.rake +0 -11
- data/tasks/spec.rake +0 -29
- data/tty-config.gemspec +0 -29
data/lib/tty-config.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require_relative
|
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
|
3
|
+
require "pathname"
|
4
4
|
|
5
|
-
require_relative
|
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.
|
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 =
|
58
|
-
@extname =
|
59
|
-
@
|
60
|
-
@
|
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
|
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
|
-
#
|
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
|
-
# @
|
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
|
-
|
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
|
-
# @
|
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.
|
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
|
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
|
-
# @
|
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
|
-
# @
|
166
|
-
#
|
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
|
-
# @
|
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
|
-
#
|
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
|
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
|
-
|
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
|
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
|
-
|
263
|
-
|
264
|
-
|
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
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
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
|
-
|
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
|
-
|
290
|
-
|
291
|
-
|
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
|
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
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
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
|
-
# @
|
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 !
|
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
|
-
|
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
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
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(
|
442
|
-
ext
|
443
|
-
|
444
|
-
|
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
|