Current section
Files
Jump to
Current section
Files
lib/loader.ex
defmodule Casconf.Loader do
@callback load(term) :: :not_found | {:found, term} | {:error, reason :: term}
@callback describe_load(term) :: String.t
@callback source_description(term) :: String.t
@default_loaders [
file: Casconf.Loader.File,
env: Casconf.Loader.Env,
default: Casconf.Loader.Default
]
@not_found_return {:error, :notfound}
@doc """
Loads a specific configuration item
"""
@spec load_raw(Casconf.Config.t) :: {:found, key :: term, value :: term, loader_name :: atom} | {:error, reason :: term}
def load_raw(%Casconf.Config{} = config) do
Enum.reduce_while(loaders(), @not_found_return, fn({name, loader}, _acc) ->
case Keyword.has_key?(config.sources, name) do
true ->
key = Keyword.get(config.sources, name)
case loader.load(key) do
{:found, value} ->
{:halt, {:found, key, value, name}}
:notfound ->
{:cont, @not_found_return}
{:error, _} = e ->
{:halt, e}
end
false ->
{:cont, @not_found_return}
end
end)
end
@doc """
Loads and casts a specific configuration item
It will raise the following errors:
* Casconf.CastError (in case the supplied value couldn't be converted to the given data type)
* Casconf.NoValueError (in case none of the loaders have found a value)
"""
@spec load(Casconf.Config.t) :: {:ok, value :: term} | {:error, reason :: term}
def load(%Casconf.Config{} = config) do
case load_raw(config) do
{:found, key, value, loader} ->
case Ecto.Type.cast(config.type, value) do
:error ->
raise Casconf.CastError,
loader: Keyword.get(loaders(), loader),
loader_opts: key,
value: value,
type: config.type
{:ok, value} ->
{:ok, value}
end
{:error, :notfound} ->
raise Casconf.NoValueError, sources: config.sources
other ->
other
end
end
import Casconf.Config, only: [value: 2]
@doc """
Finds Casconf.Config structs in the given term (lists, keyword lists and maps supported)
and
"""
@spec load_in(term) :: term
def load_in(term)
def load_in({:casconf, sources}) do
load_in(value(:string, sources))
end
def load_in({:casconf, type, sources}) do
load_in(value(type, sources))
end
# :case will get a key from the 2nd element and then get the value from the 3rd element using it as a keyword list
def load_in({:case, key, values}) do
key = load_in(key)
case Keyword.get(values, key) do
nil -> raise Casconf.NoValueError, []
value -> load_in(value)
end
end
# :cascade will try to load the first config, if that raises, the second, and so on
def load_in({:cascade, configs}) when is_list(configs) do
result = Enum.reduce_while(configs, @not_found_return, fn config, _acc ->
try do
{:halt, load_in(config)}
rescue
Casconf.NoValueError -> {:cont, @not_found_return}
Casconf.CastError -> {:cont, @not_found_return}
end
end)
case result do
{:error, :notfound} -> raise Casconf.NoValueError, []
value -> value
end
end
def load_in(%Casconf.Config{} = config) do
case load(config) do
{:ok, value} -> value
{:error, _} = e -> raise e #TODO better error handling
end
end
def load_in(list) when is_list(list) do
Enum.map(list, fn
{key, value} -> {key, load_in(value)}
value -> load_in(value)
end)
end
def load_in(%_{} = struct) do
struct
end
def load_in(map) when is_map(map) do
map
|> Enum.to_list()
|> load_in()
|> Enum.into(%{})
end
def load_in(other) do
# give up
other
end
@doc """
Returns the configured loaders in order of precedence
"""
def loaders do
config = Application.get_env(:casconf, __MODULE__) || []
Keyword.get(config, :loaders, @default_loaders)
end
end