Packages

A type-safe configuration loader for Gleam that supports JSON, YAML, and TOML with automatic format detection, environment variable resolution, and profile-based configuration.

Current section

Files

Jump to
yodel src yodel.erl
Raw

src/yodel.erl

-module(yodel).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-export([get_string/2, get_string_or/3, parse_string/2, get_int/2, get_int_or/3, parse_int/2, get_float/2, get_float_or/3, parse_float/2, get_bool/2, get_bool_or/3, parse_bool/2, default_options/0, with_format/2, with_resolve_enabled/2, enable_resolve/1, disable_resolve/1, with_resolve_mode/2, get_format/1, is_resolve_enabled/1, get_resolve_mode/1, with_strict_resolve/1, with_lenient_resolve/1, auto_detect_format/1, as_json/1, as_toml/1, as_yaml/1, load_with_options/2, load/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Yodel is a type-safe configuration loader for Gleam that handles JSON,\n"
" YAML, and TOML configs with automatic format detection, environment variable\n"
" resolution, and an intuitive dot-notation API for accessing your config\n"
" values.\n"
"\n"
" ```gleam\n"
" import yodel\n"
"\n"
" let ctx = yodel.load(\"config.toml\")\n"
" yodel.get_string(ctx, \"foo.bar\") // \"fooey\"\n"
" ```\n"
"\n"
" Yodel can resolve placeholders in the configuration content, using environment variables.\n"
" - Placeholders are defined as `${foo}` where `foo` is the placeholder key.\n"
" - Placeholders can have default values like `${foo:bar}` where `bar` is the default value.\n"
" - Placeholders can be nested like `${foo:${bar}}` where `bar` is another placeholder key.\n"
"\n"
" ```bash\n"
" # system environment variables\n"
" echo $FOO # \"fooey\"\n"
" echo $BAR # <empty>\n"
" ```\n"
"\n"
" ```toml\n"
" # config.toml\n"
" foo = \"${FOO}\"\n"
" bar = \"${BAR:default}\"\n"
" ```\n"
" ```gleam\n"
" import yodel\n"
"\n"
" let ctx = case yodel.load(\"config.toml\") {\n"
" Ok(ctx) -> ctx\n"
" Error(e) -> Error(e) // check your config!\n"
" }\n"
"\n"
" yodel.get_string(ctx, \"foo\") // \"fooey\"\n"
" yodel.get_string(ctx, \"bar\") // \"default\"\n"
" ```\n"
"\n"
" Yodel makes it easy to access configuration values in your Gleam code.\n"
" - Access values from your configuration using dot-notation.\n"
" - Get string, integer, float, and boolean values from the configuration.\n"
" - Optional return default values if the key is not found.\n"
).
-file("src/yodel.gleam", 154).
?DOC(
" Get a string value from the configuration.\n"
" If the value is not a string, an error will be returned.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" case yodel.get_string(ctx, \"foo\") {\n"
" Ok(value) -> value // \"bar\"\n"
" Error(e) -> Error(e)\n"
" }\n"
" ```\n"
).
-spec get_string(yodel@context:context(), binary()) -> {ok, binary()} |
{error, yodel@properties:properties_error()}.
get_string(Ctx, Key) ->
yodel@context:get_string(Ctx, Key).
-file("src/yodel.gleam", 165).
?DOC(
" Get a string value from the configuration, or a default value if the key is not found.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let value = yodel.get_string_or(ctx, \"foo\", \"default\")\n"
" ```\n"
).
-spec get_string_or(yodel@context:context(), binary(), binary()) -> binary().
get_string_or(Ctx, Key, Default) ->
yodel@context:get_string_or(Ctx, Key, Default).
-file("src/yodel.gleam", 182).
?DOC(
" Parse a string value from the configuration.\n"
"\n"
" If the value is not a string, it will be converted to a string.\n"
" An error will be returned if the value is not a string or cannot be\n"
" converted to a string.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" case yodel.parse_string(ctx, \"foo\") {\n"
" Ok(value) -> value // \"42\"\n"
" Error(e) -> Error(e)\n"
" }\n"
).
-spec parse_string(yodel@context:context(), binary()) -> {ok, binary()} |
{error, yodel@properties:properties_error()}.
parse_string(Ctx, Key) ->
yodel@context:parse_string(Ctx, Key).
-file("src/yodel.gleam", 200).
?DOC(
" Get an integer value from the configuration.\n"
" If the value is not an integer, an error will be returned.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" case yodel.get_int(ctx, \"foo\") {\n"
" Ok(value) -> value // 42\n"
" Error(e) -> Error(e)\n"
" }\n"
" ```\n"
).
-spec get_int(yodel@context:context(), binary()) -> {ok, integer()} |
{error, yodel@properties:properties_error()}.
get_int(Ctx, Key) ->
yodel@context:get_int(Ctx, Key).
-file("src/yodel.gleam", 211).
?DOC(
" Get an integer value from the configuration, or a default value if the key is not found.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let value = yodel.get_int_or(ctx, \"foo\", 42)\n"
" ```\n"
).
-spec get_int_or(yodel@context:context(), binary(), integer()) -> integer().
get_int_or(Ctx, Key, Default) ->
yodel@context:get_int_or(Ctx, Key, Default).
-file("src/yodel.gleam", 229).
?DOC(
" Parse an integer value from the configuration.\n"
"\n"
" If the value is not an integer, it will be converted to an integer.\n"
" An error will be returned if the value is not an integer or cannot be\n"
" converted to an integer.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" case yodel.parse_int(ctx, \"foo\") {\n"
" Ok(value) -> value // 42\n"
" Error(e) -> Error(e)\n"
" }\n"
" ```\n"
).
-spec parse_int(yodel@context:context(), binary()) -> {ok, integer()} |
{error, yodel@properties:properties_error()}.
parse_int(Ctx, Key) ->
yodel@context:parse_int(Ctx, Key).
-file("src/yodel.gleam", 243).
?DOC(
" Get a float value from the configuration.\n"
" If the value is not a float, an error will be returned.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" case yodel.get_float(ctx, \"foo\") {\n"
" Ok(value) -> value // 42.0\n"
" Error(e) -> Error(e)\n"
" }\n"
).
-spec get_float(yodel@context:context(), binary()) -> {ok, float()} |
{error, yodel@properties:properties_error()}.
get_float(Ctx, Key) ->
yodel@context:get_float(Ctx, Key).
-file("src/yodel.gleam", 254).
?DOC(
" Get a float value from the configuration, or a default value if the key is not found.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let value = yodel.get_float_or(ctx, \"foo\", 42.0)\n"
" ```\n"
).
-spec get_float_or(yodel@context:context(), binary(), float()) -> float().
get_float_or(Ctx, Key, Default) ->
yodel@context:get_float_or(Ctx, Key, Default).
-file("src/yodel.gleam", 272).
?DOC(
" Parse a float value from the configuration.\n"
"\n"
" If the value is not a float, it will be converted to a float.\n"
" An error will be returned if the value is not a float or cannot be\n"
" converted to a float.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" case yodel.parse_float(ctx, \"foo\") {\n"
" Ok(value) -> value // 99.999\n"
" Error(e) -> Error(e)\n"
" }\n"
" ```\n"
).
-spec parse_float(yodel@context:context(), binary()) -> {ok, float()} |
{error, yodel@properties:properties_error()}.
parse_float(Ctx, Key) ->
yodel@context:parse_float(Ctx, Key).
-file("src/yodel.gleam", 286).
?DOC(
" Get a boolean value from the configuration.\n"
" If the value is not a boolean, an error will be returned.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" case yodel.get_bool(ctx, \"foo\") {\n"
" Ok(value) -> value // True\n"
" Error(e) -> Error(e)\n"
" }\n"
).
-spec get_bool(yodel@context:context(), binary()) -> {ok, boolean()} |
{error, yodel@properties:properties_error()}.
get_bool(Ctx, Key) ->
yodel@context:get_bool(Ctx, Key).
-file("src/yodel.gleam", 297).
?DOC(
" Get a boolean value from the configuration, or a default value if the key is not found.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let value = yodel.get_bool_or(ctx, \"foo\", False)\n"
" ```\n"
).
-spec get_bool_or(yodel@context:context(), binary(), boolean()) -> boolean().
get_bool_or(Ctx, Key, Default) ->
yodel@context:get_bool_or(Ctx, Key, Default).
-file("src/yodel.gleam", 315).
?DOC(
" Parse a bool value from the configuration.\n"
"\n"
" If the value is not a bool, it will be converted to a bool.\n"
" An error will be returned if the value is not a bool or cannot be\n"
" converted to a bool.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" case yodel.parse_bool(ctx, \"foo\") {\n"
" Ok(value) -> value // True\n"
" Error(e) -> Error(e)\n"
" }\n"
" ```\n"
).
-spec parse_bool(yodel@context:context(), binary()) -> {ok, boolean()} |
{error, yodel@properties:properties_error()}.
parse_bool(Ctx, Key) ->
yodel@context:parse_bool(Ctx, Key).
-file("src/yodel.gleam", 334).
?DOC(
" The default options for loading a configuration file.\n"
"\n"
" Default Options:\n"
"\n"
" - Format: `auto_format`\n"
" - Resolve Enabled: `True`\n"
" - Resolve Mode: `lenient_resolve`\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let ctx =\n"
" yodel.default_options()\n"
" |> yodel.load_with_options(\"config.toml\")\n"
" ```\n"
).
-spec default_options() -> yodel@options:options().
default_options() ->
yodel@options:default().
-file("src/yodel.gleam", 348).
?DOC(
" Set the format of the configuration file.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let ctx =\n"
" yodel.default_options()\n"
" |> yodel.with_format(yodel.json_format)\n"
" |> yodel.load_with_options(\"config.json\")\n"
" ```\n"
).
-spec with_format(yodel@options:options(), yodel@options:format()) -> yodel@options:options().
with_format(Options, Format) ->
yodel@options:with_format(Options, Format).
-file("src/yodel.gleam", 426).
?DOC(
" Enable or disable placeholder resolution.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let ctx =\n"
" yodel.default_options()\n"
" |> yodel.with_resolve_enabled(False)\n"
" |> yodel.load_with_options(\"config.yaml\")\n"
" ```\n"
).
-spec with_resolve_enabled(yodel@options:options(), boolean()) -> yodel@options:options().
with_resolve_enabled(Options, Enabled) ->
yodel@options:with_resolve_enabled(Options, Enabled).
-file("src/yodel.gleam", 443).
?DOC(
" Enable placeholder resolution.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let ctx =\n"
" yodel.default_options()\n"
" |> yodel.enable_resolve()\n"
" |> yodel.load_with_options(\"config.yaml\")\n"
" ```\n"
).
-spec enable_resolve(yodel@options:options()) -> yodel@options:options().
enable_resolve(Options) ->
with_resolve_enabled(Options, true).
-file("src/yodel.gleam", 457).
?DOC(
" Disable placeholder resolution.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let ctx =\n"
" yodel.default_options()\n"
" |> yodel.disable_resolve()\n"
" |> yodel.load_with_options(\"config.yaml\")\n"
" ```\n"
).
-spec disable_resolve(yodel@options:options()) -> yodel@options:options().
disable_resolve(Options) ->
with_resolve_enabled(Options, false).
-file("src/yodel.gleam", 471).
?DOC(
" Set the resolve mode.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let ctx =\n"
" yodel.default_options()\n"
" |> yodel.with_resolve_mode(yodel.strict_resolve)\n"
" |> yodel.load_with_options(\"config.json\")\n"
" ```\n"
).
-spec with_resolve_mode(yodel@options:options(), yodel@options:resolve_mode()) -> yodel@options:options().
with_resolve_mode(Options, Mode) ->
yodel@options:with_resolve_mode(Options, Mode).
-file("src/yodel.gleam", 511).
?DOC(
" Get the format of the configuration file.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let format = yodel.get_format(options)\n"
" ```\n"
).
-spec get_format(yodel@options:options()) -> yodel@options:format().
get_format(Options) ->
yodel@options:get_format(Options).
-file("src/yodel.gleam", 525).
?DOC(
" Check if placeholder resolution is enabled.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" case yodel.is_resolve_enabled(options) {\n"
" True -> \"Resolution is enabled\"\n"
" False -> \"Resolution is disabled\"\n"
" }\n"
" ```\n"
).
-spec is_resolve_enabled(yodel@options:options()) -> boolean().
is_resolve_enabled(Options) ->
yodel@options:is_resolve_enabled(Options).
-file("src/yodel.gleam", 536).
?DOC(
" Get the resolve mode.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let mode = yodel.get_resolve_mode(options)\n"
" ```\n"
).
-spec get_resolve_mode(yodel@options:options()) -> yodel@options:resolve_mode().
get_resolve_mode(Options) ->
yodel@options:get_resolve_mode(Options).
-file("src/yodel.gleam", 540).
-spec parse(
binary(),
yodel@options:format(),
fun((yodel@properties:properties()) -> {ok, yodel@context:context()} |
{error, yodel@errors:config_error()})
) -> {ok, yodel@context:context()} | {error, yodel@errors:config_error()}.
parse(Input, Format, Next) ->
_pipe = yodel@parser:parse(Input, Format),
gleam@result:then(_pipe, Next).
-file("src/yodel.gleam", 549).
-spec validate(
yodel@properties:properties(),
fun((yodel@properties:properties()) -> {ok, yodel@context:context()} |
{error, yodel@errors:config_error()})
) -> {ok, yodel@context:context()} | {error, yodel@errors:config_error()}.
validate(Props, Handler) ->
_pipe = yodel@validator:validate_properties(Props),
gleam@result:then(_pipe, Handler).
-file("src/yodel.gleam", 557).
-spec resolve(
binary(),
yodel@options:options(),
fun((binary()) -> {ok, yodel@context:context()} |
{error, yodel@errors:config_error()})
) -> {ok, yodel@context:context()} | {error, yodel@errors:config_error()}.
resolve(Input, Options, Handler) ->
_pipe@1 = case yodel@options:is_resolve_enabled(Options) of
true ->
yodel@resolver:resolve_placeholders(Input, Options);
false ->
_pipe = Input,
{ok, _pipe}
end,
gleam@result:then(_pipe@1, Handler).
-file("src/yodel.gleam", 569).
-spec read(
binary(),
fun((binary()) -> {ok, yodel@context:context()} |
{error, yodel@errors:config_error()})
) -> {ok, yodel@context:context()} | {error, yodel@errors:config_error()}.
read(Input, Handler) ->
_pipe = case yodel@input:get_content(Input) of
{ok, Content} ->
{ok, Content};
{error, E} ->
{error, E}
end,
gleam@result:then(_pipe, Handler).
-file("src/yodel.gleam", 487).
?DOC(
" Set the resolve mode to strict.\n"
"\n"
" Example:\n"
" ```gleam\n"
" let ctx =\n"
" yodel.default_options()\n"
" |> yodel.with_strict_resolve()\n"
" |> yodel.load_with_options(my_config)\n"
" ```\n"
).
-spec with_strict_resolve(yodel@options:options()) -> yodel@options:options().
with_strict_resolve(Options) ->
with_resolve_mode(Options, strict).
-file("src/yodel.gleam", 500).
?DOC(
" Set the resolve mode to lenient.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let ctx =\n"
" yodel.default_options()\n"
" |> yodel.with_lenient_resolve()\n"
" |> yodel.load_with_options(my_config)\n"
).
-spec with_lenient_resolve(yodel@options:options()) -> yodel@options:options().
with_lenient_resolve(Options) ->
with_resolve_mode(Options, lenient).
-file("src/yodel.gleam", 412).
?DOC(
" Attempt to automatically detect the format of the configuration file.\n"
"\n"
" If the input is a file, we first try to detect the format from the file extension.\n"
" If that fails, we try to detect the format from the content of the file.\n"
"\n"
" If the input is a string, we try to detect the format from the content.\n"
"\n"
" If Auto Detection fails, an error will be returned because we can't safely proceed.\n"
" If this happens, try specifying the format using `as_json`, `as_toml`, `as_yaml`, or `with_format`.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let ctx =\n"
" yodel.default_options()\n"
" |> yodel.auto_detect_format()\n"
" |> yodel.load_with_options(my_config)\n"
" ```\n"
).
-spec auto_detect_format(yodel@options:options()) -> yodel@options:options().
auto_detect_format(Options) ->
with_format(Options, auto).
-file("src/yodel.gleam", 362).
?DOC(
" Set the format of the configuration file to JSON.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let ctx =\n"
" yodel.default_options()\n"
" |> yodel.as_json()\n"
" |> yodel.load_with_options(my_config)\n"
" ```\n"
).
-spec as_json(yodel@options:options()) -> yodel@options:options().
as_json(Options) ->
with_format(Options, json).
-file("src/yodel.gleam", 376).
?DOC(
" Set the format of the configuration file to TOML.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let ctx =\n"
" yodel.default_options()\n"
" |> yodel.as_toml()\n"
" |> yodel.load_with_options(my_config)\n"
" ```\n"
).
-spec as_toml(yodel@options:options()) -> yodel@options:options().
as_toml(Options) ->
with_format(Options, toml).
-file("src/yodel.gleam", 390).
?DOC(
" Set the format of the configuration file to YAML.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let ctx =\n"
" yodel.default_options()\n"
" |> yodel.as_yaml()\n"
" |> yodel.load_with_options(my_config)\n"
" ```\n"
).
-spec as_yaml(yodel@options:options()) -> yodel@options:options().
as_yaml(Options) ->
with_format(Options, yaml).
-file("src/yodel.gleam", 580).
-spec select(
binary(),
binary(),
yodel@options:options(),
fun((yodel@options:format()) -> {ok, yodel@context:context()} |
{error, yodel@errors:config_error()})
) -> {ok, yodel@context:context()} | {error, yodel@errors:config_error()}.
select(Input, Content, Options, Handler) ->
Formats = [{format_detector,
<<"toml"/utf8>>,
fun yodel@parsers@toml:detect/1},
{format_detector, <<"json/yaml"/utf8>>, fun yodel@parsers@yaml:detect/1}],
_pipe = case yodel@format:get_format(Input, Content, Options, Formats) of
json ->
json;
toml ->
toml;
yaml ->
yaml;
auto ->
auto
end,
_pipe@1 = {ok, _pipe},
gleam@result:then(_pipe@1, Handler).
-file("src/yodel.gleam", 131).
?DOC(
" Load a configuration file with options.\n"
"\n"
" This function will use the provided options to read and parse the config content,\n"
" returning a `Context` if successful.\n"
).
-spec load_with_options(yodel@options:options(), binary()) -> {ok,
yodel@context:context()} |
{error, yodel@errors:config_error()}.
load_with_options(Options, Input) ->
read(
Input,
fun(Content) ->
select(
Input,
Content,
Options,
fun(Format) ->
resolve(
Content,
Options,
fun(Resolved) ->
parse(
Resolved,
Format,
fun(Parsed) ->
validate(
Parsed,
fun(Validated) ->
{ok, yodel@context:new(Validated)}
end
)
end
)
end
)
end
)
end
).
-file("src/yodel.gleam", 123).
?DOC(
" Load a configuration file.\n"
"\n"
" This function will read the config content, detect the format,\n"
" resolve the placeholders, parse the config content, returning a `Context` if successful.\n"
"\n"
" `input` can be a file path or a string containing the configuration content.\n"
"\n"
" Example:\n"
"\n"
" ```gleam\n"
" let ctx = yodel.load(\"config.toml\")\n"
"\n"
" let content = \"foo: bar\" // yaml content\n"
" let ctx = yodel.load(content)\n"
" ```\n"
).
-spec load(binary()) -> {ok, yodel@context:context()} |
{error, yodel@errors:config_error()}.
load(Input) ->
load_with_options(default_options(), Input).