Configuration files
The file
A config file is a hash with comments. YAML is the usual choice because a person edits it, and the shape here is one section named after the program:
app:
name: demo
port: 3000
debug: false
The section is not decoration: a file that also holds a database: block can be shared between programs, and each one reads its own key.
The script
Three layers in order: the defaults in code, the file over them, the environment over the file. Hash#merge is the layering, and the validation is the last line before the program starts:
require "yaml"
DEFAULTS = { "name" => "app", "port" => 3000, "debug" => false }.freeze
i = ARGV.index("--config") or abort("usage: ruby app.rb --config FILE")
path = ARGV[i + 1] or abort("usage: ruby app.rb --config FILE")
raw = YAML.safe_load_file(path) or abort("#{path}: empty config")
settings = DEFAULTS.merge(raw.fetch("app") { abort("#{path}: missing app section") })
settings["port"] = Integer(ENV.fetch("APP_PORT", settings["port"]))
abort("port must be positive") unless settings["port"].positive?
puts "#{settings["name"]} on #{settings["port"]} (debug: #{settings["debug"]})"
safe_load_file opens and parses in one call, and the same safe rules as the YAML lesson apply. Integer() is the strict conversion: a value that is not a number raises instead of becoming zero.
Running it
ruby app.rb --config config.yml
demo on 3000 (debug: false)
The environment layer is a real override, not a second file:
APP_PORT=4000 ruby app.rb --config config.yml
demo on 4000 (debug: false)
A container sets APP_PORT and mounts the same config, and the code does not change between the laptop and the host. The precedence is documented in the README, which is the part that keeps the next reader from guessing.
Fail fast
A config error must stop the program before it opens a socket or writes a row. The abort calls above print to stderr and exit non-zero, which a supervisor and a CI job both read:
ruby app.rb --config broken.yml
broken.yml: missing app section
APP_PORT=-1 ruby app.rb --config config.yml
port must be positive
The message names the file or the setting, so the fix is a one-line edit. A program that starts with a bad config and fails on the first request wastes the time between the two.
What to watch for
Never commit the file that holds a password. Commit a
config.example.yml with empty values, read the
real one from a path in the environment, and the secret stays out of the
repository.
A key that a config does not mention should come from the defaults, but a
key that a config sets to null is a deliberate
empty value. merge keeps the null and the default
is gone; decide which one the setting means and validate it.
Next: HTTP clients
Article author: Arthur Isaev