ucfg
Load YAML config, merge overrides, expand environment variables, and validate the result before your application boots.
ucfg is a Ruby take on
elastic/go-ucfg.
Typical Use
Most apps need some version of this:
-
config.ymlfor defaults -
config/development.ymlorconfig/production.ymlfor overrides - environment variables for secrets and deploy-time values
- a schema for required keys and expected types
Load and validate config at boot:
config = Ucfg.load(
"config/app.yml",
"config/#{ENV.fetch('APP_ENV', 'development')}.yml",
schema: "config/schema.yml",
env: true,
)
App.start(config)Invalid config fails before App.start, with errors tied to config properties.
Installation
ucfg requires Ruby 3.1 or newer.
Add ucfg to your Gemfile:
gem "ucfg"Then run:
bundle installA Complete Example
Start with a base config:
# config/app.yml
service:
name: billing-api
host: 0.0.0.0
port: 3000
database:
url: ${DATABASE_URL}
pool: ${DATABASE_POOL:5}
features:
invoices: trueAdd an environment override:
# config/production.yml
service:
port: ${PORT:8080}
database:
pool: 20Describe what valid config looks like:
# config/schema.yml
type: object
required:
- service
- database
properties:
service:
type: object
required:
- name
- host
- port
properties:
name:
type: string
minLength: 1
host:
type: string
port:
type: integer
minimum: 1
maximum: 65535
database:
type: object
required:
- url
- pool
properties:
url:
type: string
minLength: 1
pool:
type: integer
minimum: 1
features:
type: object
additionalProperties:
type: booleanLoad and validate it:
config = Ucfg.load(
"config/app.yml",
"config/production.yml",
schema: "config/schema.yml",
env: true,
)
config.fetch("service").fetch("port")
# => 8080Semantics:
- later files override earlier files
- nested objects are merged
- arrays and scalar values are replaced
-
${NAME}reads from the environment -
${NAME:default}uses a default when the environment variable is missing or empty -
$$escapes to a literal$ -
env_parsers: { "NAME" => :csv }parses a whole environment value as a comma-separated list - schema errors are reported before the loaded config is returned
Loading API
Use Ucfg.load when invalid config should stop boot:
config = Ucfg.load(
"config/app.yml",
"config/production.yml",
schema: "config/schema.yml",
env: true,
)Ucfg.load! is an alias for Ucfg.load.
Use Ucfg.load_result when you want to handle errors yourself:
result = Ucfg.load_result(
"config/app.yml",
"config/production.yml",
schema: "config/schema.yml",
env: true,
)
if result.valid?
App.start(result.config)
else
warn result.errors.join("\n")
exit 1
endresult.errors returns human-readable strings. result.error_details returns
structured error objects:
result.error_details.first.to_h
# => {
# :message => "Property `service.port` must be of type `integer` ...",
# :path => ["service", "port"],
# :keyword => "type",
# :type => :validation,
# }Error type is :validation for config validation errors, :schema for
invalid schema shapes, and :load for file loading or parsing errors.
schema: accepts:
-
nilfor no validation - a schema
Hash - a string or path-like object pointing to a schema file
Raw YAML schema strings are not accepted as schema: values. Use
Ucfg.load_yaml first if you already have the schema source in memory.
env_parsers: can be used with Ucfg.load, Ucfg.load_result,
Ucfg.load_file, and Ucfg.load_yaml.
Lower-Level APIs
You can load and validate YAML strings directly:
schema = {
"type" => "object",
"required" => ["service"],
"properties" => {
"service" => {
"type" => "object",
"required" => ["name"],
"properties" => {
"name" => { "type" => "string" },
},
},
},
}
result = Ucfg.validate_yaml(<<~YAML, schema)
service:
name: billing-api
YAML
result.valid?
# => trueOr compose the lower-level pieces manually:
base = Ucfg.load_file("config/app.yml", env: true)
override = Ucfg.load_file("config/production.yml", env: true)
config = Ucfg::ConfigMerger.merge(base, override)
schema = Ucfg.load_file("config/schema.yml")
result = Ucfg.validate(config, schema)
unless result.valid?
abort result.errors.join("\n")
endEnvironment Expansion
Environment expansion is opt-in:
host: ${HOST:localhost}
port: ${PORT:3000}
debug: ${DEBUG:false}
hosts: ${HOSTS:localhost}When a whole YAML value is an environment expression, ucfg preserves useful
types:
ENV["PORT"] = "3000"
ENV["DEBUG"] = "false"
ENV["HOSTS"] = "api-1,api-2"
Ucfg.load_yaml("port: ${PORT}\ndebug: ${DEBUG}\nhosts: ${HOSTS}\n", env: true)
# => {
# "port" => 3000,
# "debug" => false,
# "hosts" => "api-1,api-2",
# }Comma-separated values are strings by default. Use env_parsers when a specific
environment variable should be parsed as a list:
ENV["HOSTS"] = "api-1,api-2"
Ucfg.load_yaml(
"hosts: ${HOSTS}\n",
env: true,
env_parsers: { "HOSTS" => :csv },
)
# => { "hosts" => ["api-1", "api-2"] }Embedded environment expressions are rendered as strings:
url: postgres://${DATABASE_HOST:localhost}:5432/appWrite $$ for a literal $, including when a value should keep a ${...}
sequence instead of expanding it:
price: $$5
literal: $${NOT_EXPANDED}# => { "price" => "$5", "literal" => "${NOT_EXPANDED}" }A $ that is not part of $$ or ${ is left alone, so a$b needs no
escaping.
ERB Templates
ERB rendering is also opt-in:
config = Ucfg.load_file("config/app.yml", erb: true)Use ERB only for trusted configuration files. ERB executes Ruby code while the config is being loaded.
Environment expansion and ERB rendering are intentionally separate modes. Prefer
${NAME} expansion for secrets and deployment values, and reserve ERB for cases
that truly need Ruby logic.
Supported Schema Keywords
ucfg implements a practical subset of JSON Schema for configuration files:
typerequiredpropertiesadditionalPropertiespatternPropertiesitemsenumconst-
minimum,maximum,exclusiveMinimum,exclusiveMaximum - legacy
minandmax -
minLength,maxLength,pattern -
minItems,maxItems,uniqueItems -
anyOf,oneOf,allOf
Unsupported Schema Keywords
ucfg is not a full JSON Schema implementation. These commonly used JSON Schema
features are not currently supported:
-
$schema,$id,$ref,$defs, anddefinitions -
default,title,description,examples,deprecated, andreadOnly formatmultipleOfnot-
if,then, andelse -
dependentRequired,dependentSchemas, and legacydependencies propertyNames-
minPropertiesandmaxProperties - tuple-style
items,prefixItems,contains,minContains, andmaxContains -
unevaluatedPropertiesandunevaluatedItems patternRequired-
contentEncoding,contentMediaType, andcontentSchema
Unsupported keywords are ignored unless ucfg has explicit schema-shape checks
for that keyword. Keep schemas small and focused on the supported validation
rules above.
YAML Rules
ucfg accepts a restricted YAML shape:
- one YAML document per file
- no anchors or aliases
- no merge keys
- no explicit tags
- no block scalars
- no flow-style objects or arrays
Dotted keys are expanded into nested objects:
service.name: billing-api
service.port: 3000is equivalent to:
service:
name: billing-api
port: 3000Every segment of a dotted key must be non-empty, so .name, service..name,
and service. are all rejected.
Roadmap
Before a stable v1, add a small CLI for checking config in CI, for example:
ucfg check config/app.yml --schema config/schema.ymlDevelopment
After checking out the repository, install dependencies, then run the test suite and style checks:
bin/setup
bundle exec rakeTo experiment locally:
bin/consoleBug reports and pull requests are welcome at https://github.com/orhantoy/ucfg.