defmodule Confx do
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
@doc """
Returns the configuration specified in the given file
## Options
The accepted options are:
* `defaults`: If some key could not be found, the default will be assumed
"""
@spec load(term(), Keyword.t()) ::
{:ok, map()}
| {:error, :file_not_found}
| {:error, :file_format_not_found}
| {:error, atom()}
def load(path, opts \\ []) do
defaults = opts[:defaults] || []
with {:ok, format} <- file_format(path),
{:ok, content} <- read_file(path),
{:ok, config} <- parse_file(content, format) do
defaults = keyword_to_map(defaults)
conf =
config
|> keys_to_atom()
|> then(&merge(defaults, &1))
{:ok, conf}
else
{:error, %{}} ->
{:error, :parsing_error}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Same as `load/2` but returns the config map directly, or raises an exception if
an error is returned
"""
@spec load!(term(), Keyword.t()) :: map()
def load!(path, opts \\ []) do
case load(path, opts) do
{:ok, config} ->
config
{:error, :file_format_not_found} ->
raise Confx.FileFormatNotFoundError, path
{:error, :file_not_found} ->
raise Confx.FileNotFoundError, path
end
end
defp parse_file(content, format) do
case format do
:json ->
Jason.decode(content)
:yaml ->
YamlElixir.read_from_string(content)
end
end
defp file_format(path) do
case Path.extname(path) do
".json" ->
{:ok, :json}
ext when ext in [".yml", ".yaml"] ->
{:ok, :yaml}
_ ->
{:error, :file_format_not_found}
end
end
defp read_file(path) do
case File.read(path) do
{:ok, content} ->
{:ok, content}
{:error, _} ->
{:error, :file_not_found}
end
end
defp merge(left, right) do
Map.merge(left, right, &merge_resolve/3)
end
defp merge_resolve(_key, %{} = left, %{} = right), do: merge(left, right)
defp merge_resolve(_key, _left, right), do: right
defp keys_to_atom(str_key_map) when is_map(str_key_map) do
for {key, val} <- str_key_map, into: %{}, do: {String.to_atom(key), keys_to_atom(val)}
end
defp keys_to_atom(value), do: value
defp keyword_to_map([]), do: %{}
defp keyword_to_map(keyword_list) when is_list(keyword_list) do
for {key, val} <- keyword_list, into: %{}, do: {key, keyword_to_map(val)}
end
defp keyword_to_map(val), do: val
end