tty-config 0.3.1 → 0.5.1
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 +4 -4
- data/CHANGELOG.md +50 -1
- data/LICENSE.txt +1 -1
- data/README.md +305 -153
- 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/hcl_marshaller.rb +30 -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/marshallers.rb +35 -0
- data/lib/tty/config/version.rb +3 -3
- data/lib/tty/config.rb +374 -190
- data/lib/tty-config.rb +1 -1
- metadata +66 -57
- data/Rakefile +0 -8
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/spec/spec_helper.rb +0 -54
- data/spec/unit/alias_setting_spec.rb +0 -72
- data/spec/unit/append_spec.rb +0 -26
- data/spec/unit/autoload_env_spec.rb +0 -62
- data/spec/unit/delete_spec.rb +0 -22
- data/spec/unit/exist_spec.rb +0 -24
- data/spec/unit/fetch_spec.rb +0 -45
- data/spec/unit/generate_spec.rb +0 -70
- data/spec/unit/merge_spec.rb +0 -13
- data/spec/unit/new_spec.rb +0 -6
- data/spec/unit/normalize_hash_spec.rb +0 -21
- data/spec/unit/read_spec.rb +0 -109
- data/spec/unit/remove_spec.rb +0 -16
- data/spec/unit/set_from_env_spec.rb +0 -78
- data/spec/unit/set_if_empty_spec.rb +0 -26
- data/spec/unit/set_spec.rb +0 -62
- data/spec/unit/validate_spec.rb +0 -76
- data/spec/unit/write_spec.rb +0 -197
- data/tasks/console.rake +0 -11
- data/tasks/coverage.rake +0 -11
- data/tasks/spec.rake +0 -29
- data/tty-config.gemspec +0 -30
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,80 +29,33 @@ 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
|
-
end
|
31
|
-
end
|
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
56
|
end
|
80
|
-
content.join("\n")
|
81
57
|
end
|
82
58
|
|
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
|
-
|
92
59
|
# A collection of config paths
|
93
60
|
# @api public
|
94
61
|
attr_reader :location_paths
|
@@ -113,22 +80,33 @@ module TTY
|
|
113
80
|
# @api public
|
114
81
|
attr_accessor :env_prefix
|
115
82
|
|
83
|
+
# The string used to separate parts in ENV variable name
|
84
|
+
# @api public
|
85
|
+
attr_accessor :env_separator
|
86
|
+
|
116
87
|
# Create a configuration instance
|
117
88
|
#
|
118
89
|
# @api public
|
119
|
-
def initialize(
|
90
|
+
def initialize(settings = {})
|
120
91
|
@settings = settings
|
121
92
|
@location_paths = []
|
122
93
|
@validators = {}
|
123
|
-
@filename =
|
124
|
-
@extname =
|
125
|
-
@
|
126
|
-
@key_delim = '.'
|
94
|
+
@filename = "config"
|
95
|
+
@extname = ".yml"
|
96
|
+
@key_delim = "."
|
127
97
|
@envs = {}
|
128
|
-
@env_prefix =
|
98
|
+
@env_prefix = ""
|
99
|
+
@env_separator = "_"
|
129
100
|
@autoload_env = false
|
130
101
|
@aliases = {}
|
131
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
|
109
|
+
|
132
110
|
yield(self) if block_given?
|
133
111
|
end
|
134
112
|
|
@@ -138,14 +116,23 @@ module TTY
|
|
138
116
|
#
|
139
117
|
# api public
|
140
118
|
def extname=(name)
|
141
|
-
unless
|
119
|
+
unless extensions.include?(name)
|
142
120
|
raise UnsupportedExtError, "Config file format `#{name}` is not supported."
|
143
121
|
end
|
122
|
+
|
144
123
|
@extname = name
|
145
124
|
end
|
146
125
|
|
147
126
|
# Add path to locations to search in
|
148
127
|
#
|
128
|
+
# @example
|
129
|
+
# append_path(Dir.pwd)
|
130
|
+
#
|
131
|
+
# @param [String] path
|
132
|
+
# the path to append
|
133
|
+
#
|
134
|
+
# @return [Array<String>]
|
135
|
+
#
|
149
136
|
# @api public
|
150
137
|
def append_path(path)
|
151
138
|
@location_paths << path
|
@@ -153,6 +140,14 @@ module TTY
|
|
153
140
|
|
154
141
|
# Insert location path at the begining
|
155
142
|
#
|
143
|
+
# @example
|
144
|
+
# prepend_path(Dir.pwd)
|
145
|
+
#
|
146
|
+
# @param [String] path
|
147
|
+
# the path to prepend
|
148
|
+
#
|
149
|
+
# @return [Array<String>]
|
150
|
+
#
|
156
151
|
# @api public
|
157
152
|
def prepend_path(path)
|
158
153
|
@location_paths.unshift(path)
|
@@ -160,6 +155,8 @@ module TTY
|
|
160
155
|
|
161
156
|
# Check if env variables are auto loaded
|
162
157
|
#
|
158
|
+
# @return [Boolean]
|
159
|
+
#
|
163
160
|
# @api public
|
164
161
|
def autoload_env?
|
165
162
|
@autoload_env == true
|
@@ -172,9 +169,26 @@ module TTY
|
|
172
169
|
@autoload_env = true
|
173
170
|
end
|
174
171
|
|
175
|
-
# 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
|
176
173
|
# Keys are case-insensitive
|
177
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
|
+
#
|
178
192
|
# @api public
|
179
193
|
def set(*keys, value: nil, &block)
|
180
194
|
assert_either_value_or_block(value, block)
|
@@ -198,12 +212,22 @@ module TTY
|
|
198
212
|
|
199
213
|
# Set a value for a composite key if not present already
|
200
214
|
#
|
201
|
-
# @
|
215
|
+
# @example
|
216
|
+
# set_if_empty(:foo, :bar, :baz, value: 2)
|
217
|
+
#
|
218
|
+
# @param [Array<String, Symbol>] keys
|
202
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
|
203
225
|
#
|
204
226
|
# @api public
|
205
227
|
def set_if_empty(*keys, value: nil, &block)
|
206
|
-
|
228
|
+
keys = convert_to_keys(keys)
|
229
|
+
return unless deep_fetch(@settings, *keys).nil?
|
230
|
+
|
207
231
|
block ? set(*keys, &block) : set(*keys, value: value)
|
208
232
|
end
|
209
233
|
|
@@ -213,12 +237,11 @@ module TTY
|
|
213
237
|
# set_from_env(:host)
|
214
238
|
# set_from_env(:foo, :bar) { 'HOST' }
|
215
239
|
#
|
216
|
-
# @param [Array
|
240
|
+
# @param [Array<String>] keys
|
217
241
|
# the keys to bind to ENV variables
|
218
242
|
#
|
219
243
|
# @api public
|
220
244
|
def set_from_env(*keys, &block)
|
221
|
-
assert_keys_with_block(convert_to_keys(keys), block)
|
222
245
|
key = flatten_keys(keys)
|
223
246
|
env_key = block.nil? ? key : block.()
|
224
247
|
env_key = to_env_key(env_key)
|
@@ -229,17 +252,32 @@ module TTY
|
|
229
252
|
#
|
230
253
|
# @param [String] key
|
231
254
|
#
|
255
|
+
# @return [String]
|
256
|
+
#
|
232
257
|
# @api private
|
233
258
|
def to_env_key(key)
|
234
|
-
env_key = key.to_s.upcase
|
235
|
-
@env_prefix ==
|
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
|
236
265
|
end
|
237
266
|
|
238
267
|
# Fetch value under a composite key
|
239
268
|
#
|
240
|
-
# @
|
269
|
+
# @example
|
270
|
+
# fetch(:foo, :bar, :baz)
|
271
|
+
#
|
272
|
+
# @example
|
273
|
+
# fetch("foo.bar.baz")
|
274
|
+
#
|
275
|
+
# @param [Array<String, Symbol>, String] keys
|
241
276
|
# the keys to get value at
|
242
277
|
# @param [Object] default
|
278
|
+
# the default value
|
279
|
+
#
|
280
|
+
# @return [Object]
|
243
281
|
#
|
244
282
|
# @api public
|
245
283
|
def fetch(*keys, default: nil, &block)
|
@@ -259,24 +297,37 @@ module TTY
|
|
259
297
|
value = block || default if value.nil?
|
260
298
|
|
261
299
|
while callable_without_params?(value)
|
262
|
-
value = value.
|
300
|
+
value = value.()
|
263
301
|
end
|
264
302
|
value
|
265
303
|
end
|
266
304
|
|
267
305
|
# Merge in other configuration settings
|
268
306
|
#
|
269
|
-
# @param [Hash
|
307
|
+
# @param [Hash{Symbol => Object]] other_settings
|
308
|
+
#
|
309
|
+
# @return [Hash, nil]
|
310
|
+
# the combined settings or nil
|
270
311
|
#
|
271
312
|
# @api public
|
272
313
|
def merge(other_settings)
|
314
|
+
return unless other_settings.respond_to?(:to_hash)
|
315
|
+
|
273
316
|
@settings = deep_merge(@settings, other_settings)
|
274
317
|
end
|
275
318
|
|
276
319
|
# Append values to an already existing nested key
|
277
320
|
#
|
278
|
-
# @
|
321
|
+
# @example
|
322
|
+
# append(1, 2, to: %i[foo bar])
|
323
|
+
#
|
324
|
+
# @param [Array<Object>] values
|
279
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
|
280
331
|
#
|
281
332
|
# @api public
|
282
333
|
def append(*values, to: nil)
|
@@ -286,24 +337,45 @@ module TTY
|
|
286
337
|
|
287
338
|
# Remove a set of values from a nested key
|
288
339
|
#
|
289
|
-
# @
|
290
|
-
#
|
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
|
291
350
|
#
|
292
351
|
# @api public
|
293
352
|
def remove(*values, from: nil)
|
294
353
|
keys = Array(from)
|
354
|
+
raise ArgumentError, "Need to set key to remove from" if keys.empty?
|
355
|
+
|
295
356
|
set(*keys, value: Array(fetch(*keys)) - values)
|
296
357
|
end
|
297
358
|
|
298
359
|
# Delete a value from a nested key
|
299
360
|
#
|
300
|
-
# @
|
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
|
301
368
|
# the keys for a value deletion
|
302
369
|
#
|
370
|
+
# @yield [key] Invoke the block with a missing key
|
371
|
+
#
|
372
|
+
# @return [Object]
|
373
|
+
# the deleted value(s)
|
374
|
+
#
|
303
375
|
# @api public
|
304
|
-
def delete(*keys)
|
376
|
+
def delete(*keys, &default)
|
305
377
|
keys = convert_to_keys(keys)
|
306
|
-
deep_delete(*keys, @settings)
|
378
|
+
deep_delete(*keys, @settings, &default)
|
307
379
|
end
|
308
380
|
|
309
381
|
# Define an alias to a nested key
|
@@ -311,7 +383,7 @@ module TTY
|
|
311
383
|
# @example
|
312
384
|
# alias_setting(:foo, to: :bar)
|
313
385
|
#
|
314
|
-
# @param [Array
|
386
|
+
# @param [Array<String>] keys
|
315
387
|
# the alias key
|
316
388
|
#
|
317
389
|
# @api public
|
@@ -321,11 +393,11 @@ module TTY
|
|
321
393
|
alias_key = flatten_keys(alias_keys)
|
322
394
|
|
323
395
|
if alias_key == flat_setting
|
324
|
-
raise ArgumentError,
|
396
|
+
raise ArgumentError, "Alias matches setting key"
|
325
397
|
end
|
326
398
|
|
327
399
|
if fetch(alias_key)
|
328
|
-
raise ArgumentError,
|
400
|
+
raise ArgumentError, "Setting already exists with an alias " \
|
329
401
|
"'#{alias_keys.map(&:inspect).join(', ')}'"
|
330
402
|
end
|
331
403
|
|
@@ -334,7 +406,7 @@ module TTY
|
|
334
406
|
|
335
407
|
# Register a validation rule for a nested key
|
336
408
|
#
|
337
|
-
# @param [Array
|
409
|
+
# @param [Array<String>] keys
|
338
410
|
# a deep nested keys
|
339
411
|
# @param [Proc] validator
|
340
412
|
# the logic to use to validate given nested key
|
@@ -385,37 +457,64 @@ module TTY
|
|
385
457
|
# @api public
|
386
458
|
def read(file = find_file, format: :auto)
|
387
459
|
if file.nil?
|
388
|
-
raise ReadError,
|
460
|
+
raise ReadError, "No file found to read configuration from!"
|
389
461
|
elsif !::File.exist?(file)
|
390
462
|
raise ReadError, "Configuration file `#{file}` does not exist!"
|
391
463
|
end
|
392
464
|
|
393
|
-
|
465
|
+
set_file_metadata(file)
|
466
|
+
|
467
|
+
ext = (format == :auto ? extname : ".#{format}")
|
468
|
+
content = ::File.read(file)
|
469
|
+
|
470
|
+
merge(unmarshal(content, ext: ext))
|
394
471
|
end
|
395
472
|
|
396
473
|
# Write current configuration to a file.
|
397
474
|
#
|
475
|
+
# @example
|
476
|
+
# write(force: true, create: true)
|
477
|
+
#
|
398
478
|
# @param [String] file
|
399
|
-
# 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]
|
400
490
|
#
|
401
491
|
# @api public
|
402
|
-
def write(file = find_file, force: false, format: :auto
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
'Use :force option to overwrite.'
|
407
|
-
elsif !::File.writable?(file)
|
408
|
-
raise WriteError, "Cannot write to #{file}."
|
409
|
-
end
|
410
|
-
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)
|
411
496
|
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
497
|
+
set_file_metadata(file)
|
498
|
+
ext = (format == :auto ? extname : ".#{format}")
|
499
|
+
content = marshal(@settings, ext: ext)
|
500
|
+
filepath = Pathname.new(file)
|
416
501
|
|
417
|
-
|
418
|
-
::File.write(
|
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)
|
419
518
|
end
|
420
519
|
|
421
520
|
# Current configuration
|
@@ -433,7 +532,7 @@ module TTY
|
|
433
532
|
# @api private
|
434
533
|
def assert_either_value_or_block(value, block)
|
435
534
|
if value.nil? && block.nil?
|
436
|
-
raise ArgumentError,
|
535
|
+
raise ArgumentError, "Need to set either value or block"
|
437
536
|
elsif !(value.nil? || block.nil?)
|
438
537
|
raise ArgumentError, "Can't set both value and block"
|
439
538
|
end
|
@@ -453,7 +552,11 @@ module TTY
|
|
453
552
|
# that will be performed at point when a new proc is invoked.
|
454
553
|
#
|
455
554
|
# @param [String] key
|
555
|
+
# the key to set validation for
|
456
556
|
# @param [Proc] callback
|
557
|
+
# the callback to wrap
|
558
|
+
#
|
559
|
+
# @return [Proc]
|
457
560
|
#
|
458
561
|
# @api private
|
459
562
|
def delay_validation(key, callback)
|
@@ -467,18 +570,14 @@ module TTY
|
|
467
570
|
# Check if key passes all registered validations for a key
|
468
571
|
#
|
469
572
|
# @param [String] key
|
573
|
+
# the key to validate a value for
|
470
574
|
# @param [Object] value
|
575
|
+
# the value to check
|
471
576
|
#
|
472
577
|
# @api private
|
473
578
|
def assert_valid(key, value)
|
474
579
|
validators[key].each do |validator|
|
475
|
-
validator.
|
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'
|
580
|
+
validator.(key, value)
|
482
581
|
end
|
483
582
|
end
|
484
583
|
|
@@ -490,12 +589,16 @@ module TTY
|
|
490
589
|
#
|
491
590
|
# @param [Hash] settings
|
492
591
|
#
|
493
|
-
# @param [Array
|
592
|
+
# @param [Array<Object>] keys
|
494
593
|
# the keys to nest
|
495
594
|
#
|
595
|
+
# @return [Hash]
|
596
|
+
# the nested setting
|
597
|
+
#
|
496
598
|
# @api private
|
497
599
|
def deep_set(settings, *keys)
|
498
600
|
return settings if keys.empty?
|
601
|
+
|
499
602
|
key, *rest = *keys
|
500
603
|
value = settings[key]
|
501
604
|
|
@@ -510,15 +613,13 @@ module TTY
|
|
510
613
|
end
|
511
614
|
end
|
512
615
|
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
end
|
521
|
-
|
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
|
522
623
|
def convert_to_keys(keys)
|
523
624
|
first_key = keys[0]
|
524
625
|
if first_key.to_s.include?(key_delim)
|
@@ -528,6 +629,18 @@ module TTY
|
|
528
629
|
end
|
529
630
|
end
|
530
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
|
531
644
|
def flatten_keys(keys)
|
532
645
|
first_key = keys[0]
|
533
646
|
if first_key.to_s.include?(key_delim)
|
@@ -540,8 +653,12 @@ module TTY
|
|
540
653
|
# Fetch value under deeply nested keys with indiffernt key access
|
541
654
|
#
|
542
655
|
# @param [Hash] settings
|
656
|
+
# the settings to search
|
657
|
+
# @param [Array<Object>] keys
|
658
|
+
# the nested key to look up
|
543
659
|
#
|
544
|
-
# @
|
660
|
+
# @return [Object, nil]
|
661
|
+
# the value or nil
|
545
662
|
#
|
546
663
|
# @api private
|
547
664
|
def deep_fetch(settings, *keys)
|
@@ -554,6 +671,14 @@ module TTY
|
|
554
671
|
end
|
555
672
|
end
|
556
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
|
+
#
|
557
682
|
# @api private
|
558
683
|
def deep_merge(this_hash, other_hash, &block)
|
559
684
|
this_hash.merge(other_hash) do |key, this_val, other_val|
|
@@ -567,21 +692,41 @@ module TTY
|
|
567
692
|
end
|
568
693
|
end
|
569
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
|
+
#
|
570
705
|
# @api private
|
571
|
-
def deep_delete(*keys, settings)
|
706
|
+
def deep_delete(*keys, settings, &default)
|
572
707
|
key, *rest = keys
|
573
708
|
value = settings[key]
|
574
|
-
if !
|
575
|
-
deep_delete(*rest, value)
|
709
|
+
if !rest.empty? && value.is_a?(::Hash)
|
710
|
+
deep_delete(*rest, value, &default)
|
576
711
|
elsif !value.nil?
|
577
712
|
settings.delete(key)
|
713
|
+
elsif default
|
714
|
+
default.(key)
|
578
715
|
end
|
579
716
|
end
|
580
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
|
+
#
|
581
726
|
# @api private
|
582
727
|
def search_in_path(path)
|
583
728
|
path = Pathname.new(path)
|
584
|
-
|
729
|
+
extensions.each do |ext|
|
585
730
|
if ::File.exist?(path.join("#{filename}#{ext}").to_s)
|
586
731
|
return path.join("#{filename}#{ext}").to_s
|
587
732
|
end
|
@@ -589,81 +734,120 @@ module TTY
|
|
589
734
|
nil
|
590
735
|
end
|
591
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
|
+
#
|
592
770
|
# @api private
|
593
|
-
def
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
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."
|
618
798
|
else
|
619
|
-
|
799
|
+
filepath.dirname.mkpath
|
620
800
|
end
|
621
801
|
end
|
622
802
|
|
623
|
-
#
|
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
|
+
#
|
624
810
|
# @api private
|
625
|
-
def
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
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
|
630
817
|
end
|
631
818
|
|
632
|
-
#
|
819
|
+
# Unmarshal content into a hash object
|
633
820
|
#
|
634
|
-
# @
|
821
|
+
# @param [String] content
|
822
|
+
# the content to convert into a hash object
|
823
|
+
#
|
824
|
+
# @return [Hash{String => Object}]
|
635
825
|
#
|
636
826
|
# @api private
|
637
|
-
def
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
self.filename = ::File.basename(file, file_ext)
|
642
|
-
|
643
|
-
case ext
|
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)
|
827
|
+
def unmarshal(content, ext: nil)
|
828
|
+
ext ||= extname
|
829
|
+
if marshaller = create_marshaller(ext)
|
830
|
+
marshaller.unmarshal(content)
|
655
831
|
else
|
656
|
-
raise
|
832
|
+
raise ReadError, "Config file format `#{ext}` is not supported."
|
657
833
|
end
|
658
834
|
end
|
659
835
|
|
660
|
-
#
|
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
|
+
#
|
661
843
|
# @api private
|
662
|
-
def
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
844
|
+
def marshal(object, ext: nil)
|
845
|
+
ext ||= extname
|
846
|
+
if marshaller = create_marshaller(ext)
|
847
|
+
marshaller.marshal(object)
|
848
|
+
else
|
849
|
+
raise WriteError, "Config file format `#{ext}` is not supported."
|
850
|
+
end
|
667
851
|
end
|
668
852
|
end # Config
|
669
853
|
end # TTY
|