tty-config 0.2.0 → 0.3.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 +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
         |