defmodule Kuddle.Config do
@moduledoc """
Utility module for handling various sources of configuration using Kuddle.
"""
alias Kuddle.Node
alias Kuddle.Value
@doc """
Load a config map from a given kdl blob.
Usage:
{:ok, config} = Kuddle.Config.load_config_blob(\"\"\"
my_application {
key "value"
key2 subkey="subvalue"
}
\"\"\")
"""
@spec load_config_blob(String.t(), Keyword.t()) :: {:ok, Keyword.t()} | {:error, term()}
def load_config_blob(blob, config \\ []) do
with {:ok, document, []} <- Kuddle.decode(blob),
{:ok, config2} <- load_config_document(document) do
{:ok, Config.Reader.merge(config, config2)}
end
end
@doc """
Load a KDL config file
Usage:
{:ok, config} = Kuddle.Config.load_config_file("/path/to/kdl/config/file")
"""
@spec load_config_file(Path.t(), Keyword.t()) :: {:ok, Keyword.t()} | {:error, term()}
def load_config_file(filename, config \\ []) do
with {:ok, blob} <- File.read(filename) do
load_config_blob(blob, config)
end
end
@doc """
Load config from a directory, the extensions of the config files must be supplied as well.
Usage:
{:ok, config} =
Kuddle.Config.load_config_directory("/path/to/kdl/config/directory", [".kdl", ".kuddle"])
"""
def load_config_directory(directory_path, extensions, config \\ []) do
Enum.reduce(extensions, config, fn extname, config ->
wildcard_path = Path.join([directory_path, "**/*#{extname}"])
Enum.reduce(Path.wildcard(wildcard_path), config, fn filename, config ->
case load_config_file(filename, config) do
{:ok, config2} ->
Config.Reader.merge(config, config2)
{:error, reason} ->
raise Kuddle.ConfigError, message: "config failed to load", reason: reason
end
end)
end)
end
@doc """
Load config from a given kuddle document.
Usage:
{:ok, config} =
Kuddle.Config.load_config_document(document)
"""
@spec load_config_document(Kuddle.document(), Keyword.t()) ::
{:ok, Keyword.t()} | {:error, term()}
def load_config_document(document, acc \\ [])
def load_config_document([node | rest], acc) do
case config_from_node(node) do
{:ok, pair} ->
load_config_document(rest, [pair | acc])
{:error, _} = err ->
err
end
end
def load_config_document([], acc) do
{:ok, Enum.reverse(acc)}
end
defp config_from_value(%Value{annotations: [], value: value}) do
{:ok, value}
end
defp config_from_value(%Value{annotations: [type], value: value}) do
Kuddle.Config.Types.cast(type, value)
end
defp config_from_attributes(attributes, acc \\ [])
defp config_from_attributes([], acc) do
{:ok, Enum.reverse(acc)}
end
defp config_from_attributes([{%Value{value: key}, %Value{} = value} = pair | rest], acc) do
case config_from_value(value) do
{:ok, value} ->
config_from_attributes(rest, [{String.to_atom(key), value} | acc])
:error ->
{:error, {:attribute_error, pair}}
end
end
defp config_from_attributes([%Value{} = value | rest], acc) do
case config_from_value(value) do
{:ok, value} ->
config_from_attributes(rest, [value | acc])
:error ->
{:error, {:attribute_error, value}}
end
end
defp config_from_node(%Node{name: name, attributes: attributes, annotations: annotations, children: nil}) do
case config_from_attributes(attributes) do
{:ok, config} ->
case annotations do
[type] ->
case Kuddle.Config.Types.cast(type, config) do
{:ok, value} ->
{:ok, {String.to_atom(name), value}}
{:error, _} = err ->
err
end
[] ->
{:ok, {String.to_atom(name), maybe_single(config)}}
end
{:error, _} = err ->
err
end
end
defp config_from_node(%Node{name: name, attributes: attributes, children: children}) do
with {:ok, config} <- config_from_attributes(attributes),
{:ok, config2} <- load_config_document(children) do
config = Config.Reader.merge(config, config2)
{:ok, {String.to_atom(name), config}}
end
end
defp maybe_single([a]) do
a
end
defp maybe_single(a) do
a
end
end