Kuddle Config

An Elixir Config.Provider using kuddle, also general configuration helpers using Kuddle.

Installation

To add kuddle_config to your project:

def deps do
[
{:kuddle_config, "~> 0.3.0"}
]
end

Usage

Kuddle provides 2 different config providers:

Single File Providers

The default Config.Provider which will load a kdl file as config

Kuddle.Config.Provider

Used for elixir releases:

# Format
defp releases do
[
application: [
config_providers: [
{Kuddle.Config.Provider, [
path: config_path()
]}
]
]
]
end
# Example
defp releases do
[
application: [
config_providers: [
{Kuddle.Config.Provider, [
path: {:system, "PATH_TO_CONFIG", "/default/path/to/kdl/config"}
]}
]
]
]
end

Kuddle.Config.Distillery.Provider

Used for distillery releases

# Format
release :my_app do
set config_providers: [
{Kuddle.Config.Distillery.Provider, [
path: config_path()
]}
]
end
# Example
release :my_app do
set config_providers: [
{Kuddle.Config.Distillery.Provider, [
path: {:system, "PATH_TO_CONFIG", "/default/path/to/kdl/config"}
]}
]
end

Directory Loaders

Kuddle.Config.DirectoryProvider

# A special config provider that will load every kdl file in a directory as config
# Format
defp releases do
[
application: [
config_providers: [
{Kuddle.Config.DirectoryProvider, [
paths: [config_path()],
extensions: [String.t()]
]}
]
]
]
end
# Example
defp releases do
[
application: [
config_providers: [
{Kuddle.Config.DirectoryProvider, [
paths: [
{:system, "PATH_TO_CONFIG", "/default/path/to/kdl/config"}
],
extensions: [".kdl", ".kuddle"]
]}
]
]
]
end

Kuddle.Config.Distillery.DirectoryProvider

# Format
release :my_app do
set config_providers: [
{Kuddle.Config.Distillery.DirectoryProvider, [
paths: [config_path()],
extensions: [String.t()]
]}
]
end
# Example
release :my_app do
set config_providers: [
{Kuddle.Config.Distillery.DirectoryProvider, [
paths: [
{:system, "PATH_TO_CONFIG", "/default/path/to/kdl/config"}
],
extensions: [".kdl", ".kuddle"]
]}
]
end

Other Use Cases

Despite the original purpose of this library being the config providers, the config module itself is quite useful even without the providers:

# Have a KDL blob you wish to load?
{:ok, config} =
Kuddle.Config.load_config_blob("""
application {
node "value"
}
""")
[
application: [
node: "value"
]
] = config
# Have a KDL file you'd like to load?
{:ok, config} = Kuddle.Config.load_config_file("my_kdl_config.kdl")
[
application: [
node2: "some_other_value"
]
] = config
# Have a directory filled with KDL files you'd like to load into one config?
{:ok, config} = Kuddle.Config.load_config_directory("/my/kdl/configs", [".kdl", ".kuddle"])
[
data: [
{MyRepo, [
database: "database",
host: "127.0.0.1",
port: 5432,
]},
],
web: [
http: [
port: 4000
]
],
workers: [
amqp: [
host: "127.0.0.1",
port: 5732,
virtual_host: "my_workers",
]
]
] = config
# Have the kuddle document already?
{:ok, config} = Kuddle.Config.load_config_document(document)
[
logger: [
console: [
level: :debug
]
]
] = config

Config Format

Root nodes are application level config, while subsequent sub nodes will be one level deeper config

application {
key "value"
key2 {
subkey "value"
}
}

Is equivalent to:

config :application,
key: "value",
key2: [
subkey: "value"
]

Config is extracted from both attributes and sub nodes:

application {
food {
bacon "1"
eggs "2"
}
}

Is equivalent to:

application {
food bacon="1" eggs="2"
}

Either can be mixed and matched to achieve a comfortable format:

application {
food bacon="1" {
eggs "2"
}
}

There is one tiny gotcha in regards to lists:

application {
node "value"
}

Would evaluate to:

[
application: [
node: "value"
]
]

As one would expect, however:

application {
node "value" "value2"
}

Would evaluate to:

[
application: [
node: ["value", "value2"]
]
]

Assuming the configuration requires a list, it can be coerced using the (list) annotation on the node:

application {
(list)node "value"
}
[
application: [
node: ["value"]
]
]

Sometimes it is useful to set a tuple, which is something most available configuration languages struggle with for elixir:

application {
(tuple)thing "left" "right"
}
[
application: [
thing: {"left", "right"}
]
]

You can also cast values into atoms:

logger {
console {
level (atom)"debug"
}
}
[
logger: [
console: [
level: :debug
]
]
]

A list of all available types and more information can be found in the Kuddle.Config.Types module, or the table below.

Type AnnotationExample
datestart_date (date)"2021-09-14"
utc_datetimeinserted_at (utc_datetime)"2021-09-14T18:00:00.000000Z"
naive_datetimedeleted_at (naive_datetime)"2021-09-14T18:00:00.000000Z"
timestart_time (time)"18:00:23"
decimalcost (decimal)"0.002500"
atomlevel (atom)"debug"
booleanenable_polling (boolean)"YES"
tuple(tuple)call_pair "New York" "12003004000"
list(list)allow_list "117.27.222.122"

Additional types can be registered using kuddle_config:types config:

config :kuddle_config,
types: [
geopoint: {MyGeoPoint, :cast},
]
defmodule MyGeoPoint do
def cast(value) do
{:ok, String.split(value, ",", parts: 2) |> Enum.map(&Decimal.new/1) |> List.to_tuple()}
end
end
application {
point (geopoint)"15.27,265.27"
}
[
application: [
point: {%Decimal{coef: "1527", exp: -2, sign: 1}, %Decimal{coef: "26527", exp: -2, sign: 1}}
]
]