Packages

Generate Gleam modules based on static assets and environment variables.

Current section

Files

Jump to
embeds src embeds@env.erl
Raw

src/embeds@env.erl

-module(embeds@env).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/embeds/env.gleam").
-export([string/1, generate/1, get_variables/3, list/2, default_parse/2, typed_parse/1, typed_parse_fallback/2, int/1, float/1, bool/1, main/0]).
-export_type([options/0]).
-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(
" `embeds/env` generates a Gleam module containing constants and zero-argument\n"
" functions based on environment variables.\n"
"\n"
" This module is intended to be run as a CLI tool during your build process:\n"
" ```sh\n"
" gleam run -m embeds/env -- --help\n"
" ```\n"
"\n"
" The functions exposed in this module can also be used directly to support\n"
" advanced use cases inside custom pre-build scripts.\n"
).
-type options() :: {options,
binary(),
binary(),
fun((binary(), binary()) -> embeds@generate:code()),
binary(),
gleam@option:option(gleam@regexp:regexp())}.
-file("src/embeds/env.gleam", 402).
-spec chop_prefix(binary(), binary()) -> {ok, binary()} | {error, nil}.
chop_prefix(String, Prefix) ->
case {gleam_stdlib:string_pop_grapheme(String),
gleam_stdlib:string_pop_grapheme(Prefix)} of
{{ok, {Ch1, String@1}}, {ok, {Ch2, Prefix@1}}} when Ch1 =:= Ch2 ->
chop_prefix(String@1, Prefix@1);
{_, {error, nil}} ->
{ok, String};
{_, _} ->
{error, nil}
end.
-file("src/embeds/env.gleam", 296).
?DOC(" A string parser, just passing through the value\n").
-spec string(binary()) -> embeds@generate:code().
string(Value) ->
{constant, gleam@string:inspect(Value)}.
-file("src/embeds/env.gleam", 85).
?DOC(
" Generate Gleam modules, containing constants for all found variables.\n"
" Exported for easier integration with other build tools.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import embeds/env\n"
" import embeds/generate\n"
" import gleam/option\n"
" import gleam/io\n"
"\n"
" pub fn main() {\n"
" let result = env.generate(env.Options(\n"
" module: \"build_env\",\n"
" header: generate.default_header,\n"
" parse: env.default_parse,\n"
" prefix: \"BUILD_\",\n"
" regex: option.None,\n"
" ))\n"
"\n"
" case result {\n"
" Ok(Nil) -> Nil\n"
" Error(errs) -> io.println_error(generate.describe_errors(errs))\n"
" }\n"
" }\n"
" ```\n"
).
-spec generate(options()) -> {ok, nil} | {error, list(embeds@generate:error())}.
generate(Options) ->
embeds@generate:guard_gleam_project(
fun() ->
{options, Module, Header, Parse, Prefix, Regex} = Options,
Defs = get_variables(Module, Prefix, Regex),
Wrapped_parse = fun(Name, Code) -> case Code of
{text, Text} ->
Parse(Name, Text);
{binary, _} ->
skip
end end,
embeds@generate:generate(Defs, Header, Wrapped_parse, true)
end
).
-file("src/embeds/env.gleam", 367).
?DOC(
" Get all variables from the environment matching a prefix and a regex,\n"
" and convert them into definitions.\n"
).
-spec get_variables(
binary(),
binary(),
gleam@option:option(gleam@regexp:regexp())
) -> list(embeds@generate:definition()).
get_variables(Module, Prefix, Regex) ->
gleam@list:filter_map(
maps:to_list(envoy_ffi:all()),
fun(_use0) ->
{Key, Value} = _use0,
gleam@result:'try'(
chop_prefix(Key, Prefix),
fun(Var_name) -> gleam@result:'try'(case Regex of
{some, Regex@1} ->
case gleam@regexp:scan(Regex@1, Var_name) of
[{match, _, [{some, Matched} | _]} | _] ->
{ok, Matched};
[_ | _] ->
{ok, Var_name};
[] ->
{error, nil}
end;
none ->
{ok, Var_name}
end, fun(Var_name@1) ->
Var_name@2 = embeds@generate:to_gleam_identifier(
Var_name@1
),
{ok,
{definition,
Key,
fun() -> {ok, {text, Value}} end,
0,
Module,
Var_name@2}}
end) end
)
end
).
-file("src/embeds/env.gleam", 335).
?DOC(
" A parser for lists.\n"
" Common separators might be `:` or `,`\n"
).
-spec list(fun((binary()) -> embeds@generate:code()), binary()) -> fun((binary()) -> embeds@generate:code()).
list(Item_parser, Separator) ->
fun(Value) ->
List = gleam@string:split(Value, Separator),
Result = begin
gleam@list:try_fold(
List,
{[], true},
fun(_use0, Value@1) ->
{Parts, Is_const} = _use0,
case Item_parser(gleam@string:trim(Value@1)) of
skip ->
{ok, {Parts, Is_const}};
{generate_error, Error} ->
{error, Error};
{constant, Code} ->
{ok, {[Code | Parts], Is_const}};
{function_body, Code@1} ->
{ok, {[Code@1 | Parts], false}}
end
end
)
end,
case Result of
{ok, {Parts@1, Is_const@1}} ->
Code@2 = gleam@string:inspect(lists:reverse(Parts@1)),
case Is_const@1 of
true ->
{constant, Code@2};
false ->
{function_body, Code@2}
end;
{error, Err} ->
{generate_error, Err}
end
end.
-file("src/embeds/env.gleam", 55).
?DOC(
" A default parse function, converting environment variables into\n"
" string constants.\n"
).
-spec default_parse(binary(), binary()) -> embeds@generate:code().
default_parse(_, Value) ->
{constant, gleam@string:inspect(Value)}.
-file("src/embeds/env.gleam", 267).
?DOC(
" A function taking a list of `(key, parser)` pairs, and turning them\n"
" into a parse function to be used in `Options`.\n"
" \n"
" This allows you to generate other types of variables, instead of just strings.\n"
" \n"
" ## Example\n"
" \n"
" ```gleam\n"
" let options = env.Options(\n"
" module: \"build_env\",\n"
" header: generate.default_header,\n"
" prefix: \"BUILD_\",\n"
" regex: option.None,\n"
" parse: env.typed_parse([\n"
" #(\"BUILD_ENV\", string),\n"
" #(\"BUILD_REVISION\", int),\n"
" #(\"BUILD_ENABLE_AWESOMENESS\", bool)\n"
" ]),\n"
" )\n"
" ```\n"
).
-spec typed_parse(list({binary(), fun((binary()) -> embeds@generate:code())})) -> fun((binary(), binary()) -> embeds@generate:code()).
typed_parse(Definitions) ->
fun(Key, Value) -> case gleam@list:key_find(Definitions, Key) of
{ok, Parser} ->
Parser(gleam@string:trim(Value));
{error, nil} ->
skip
end end.
-file("src/embeds/env.gleam", 283).
?DOC(
" A variant of `typed_parse` that takes an additional fallback function\n"
" used when the variable is not found inside of `definitions`.\n"
" \n"
" Using `default_parse` as the fallback is useful to include all\n"
" variables not found in `definitions` as simple string constants.\n"
).
-spec typed_parse_fallback(
list({binary(), fun((binary()) -> embeds@generate:code())}),
fun((binary(), binary()) -> embeds@generate:code())
) -> fun((binary(), binary()) -> embeds@generate:code()).
typed_parse_fallback(Definitions, Fallback) ->
fun(Key, Value) -> case gleam@list:key_find(Definitions, Key) of
{ok, Parser} ->
Parser(gleam@string:trim(Value));
{error, nil} ->
Fallback(Key, Value)
end end.
-file("src/embeds/env.gleam", 301).
?DOC(" A parser for integers literals\n").
-spec int(binary()) -> embeds@generate:code().
int(Value) ->
embeds@generate:result_to_const(
begin
gleam@result:map(
begin
_pipe = gleam_stdlib:parse_int(Value),
gleam@result:replace_error(
_pipe,
<<"Invalid integer: "/utf8, Value/binary>>
)
end,
fun(N) -> gleam@string:inspect(N) end
)
end
).
-file("src/embeds/env.gleam", 313).
?DOC(" A parser for floating-point literals\n").
-spec float(binary()) -> embeds@generate:code().
float(Value) ->
embeds@generate:result_to_const(
begin
gleam@result:map(
begin
_pipe = gleam_stdlib:parse_float(Value),
gleam@result:replace_error(
_pipe,
<<"Invalid number: "/utf8, Value/binary>>
)
end,
fun(N) -> gleam@string:inspect(N) end
)
end
).
-file("src/embeds/env.gleam", 324).
?DOC(" A parser for boolean flags\n").
-spec bool(binary()) -> embeds@generate:code().
bool(Value) ->
Sanitized = gleam@string:trim(string:lowercase(Value)),
Bool = case Sanitized of
<<"true"/utf8>> ->
true;
<<"yes"/utf8>> ->
true;
<<"on"/utf8>> ->
true;
<<"1"/utf8>> ->
true;
_ ->
false
end,
{constant, gleam@string:inspect(Bool)}.
-file("src/embeds/env.gleam", 191).
-spec parse_definitions(list(binary())) -> {ok,
list({binary(), fun((binary()) -> embeds@generate:code())})} |
{error, list(embeds@generate:error())}.
parse_definitions(Args) ->
embeds@internal:traverse_map(
Args,
fun(Arg) -> case gleam@string:split(Arg, <<":"/utf8>>) of
[Name, Type_name] ->
Name@1 = gleam@string:trim(Name),
case gleam@string:trim(string:lowercase(Type_name)) of
<<"bool"/utf8>> ->
{ok, {Name@1, fun bool/1}};
<<"int"/utf8>> ->
{ok, {Name@1, fun int/1}};
<<"float"/utf8>> ->
{ok, {Name@1, fun float/1}};
<<"string"/utf8>> ->
{ok, {Name@1, fun string/1}};
_ ->
{error,
[{invalid_argument,
<<""/utf8>>,
Arg,
<<"Invalid type name: "/utf8,
Type_name/binary>>}]}
end;
_ ->
{error,
[{invalid_argument,
<<""/utf8>>,
Arg,
<<"Invalid type definition"/utf8>>}]}
end end
).
-file("src/embeds/env.gleam", 120).
-spec generate_cmd() -> glint:command(nil).
generate_cmd() ->
glint:command_help(
<<<<<<<<"Generate Gleam modules containing String constants of whitelisted environment variables.\n\n\n"/utf8,
"By default, environment variables starting with `BUILD_' will be included, and a module called build_env will be generated.\n\n"/utf8>>/binary,
"The generted constants have this prefix stripped.\n\n\n"/utf8>>/binary,
"You can optionally provide a list of NAME:TYPE pairs to generate constants of other types instead.\n\n"/utf8>>/binary,
"Valid types names are: int, float, bool, string"/utf8>>,
fun() ->
glint:flag(
begin
_pipe = glint:string_flag(<<"module"/utf8>>),
_pipe@1 = glint:flag_default(_pipe, <<"build_env"/utf8>>),
glint:flag_help(
_pipe@1,
<<"Name of the generated Gleam module (default: build_env)"/utf8>>
)
end,
fun(Module_name) ->
glint:flag(
begin
_pipe@2 = glint:string_flag(<<"prefix"/utf8>>),
_pipe@3 = glint:flag_default(
_pipe@2,
<<"BUILD_"/utf8>>
),
glint:flag_help(
_pipe@3,
<<"Only environment variables matching this prefix will be included, and the prefix will be stripped from their name. (default: BUILD_)"/utf8>>
)
end,
fun(Prefix) ->
glint:flag(
begin
_pipe@4 = glint:string_flag(
<<"filter"/utf8>>
),
glint:flag_help(
_pipe@4,
<<"Use a regular expression to match environment variables. If a capture group exists, it will be used as the variable name. If both prefix and regex are given, the prefix will be stripped first."/utf8>>
)
end,
fun(Filter) ->
glint:command(
fun(_, Args, Flags) ->
Module@1 = case Module_name(Flags) of
{ok, Module} -> Module;
_assert_fail ->
erlang:error(
#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"embeds/env"/utf8>>,
function => <<"generate_cmd"/utf8>>,
line => 154,
value => _assert_fail,
start => 4797,
'end' => 4839,
pattern_start => 4808,
pattern_end => 4818}
)
end,
Prefix@2 = case Prefix(Flags) of
{ok, Prefix@1} -> Prefix@1;
_assert_fail@1 ->
erlang:error(
#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"embeds/env"/utf8>>,
function => <<"generate_cmd"/utf8>>,
line => 155,
value => _assert_fail@1,
start => 4842,
'end' => 4879,
pattern_start => 4853,
pattern_end => 4863}
)
end,
Filter@1 = begin
_pipe@5 = Filter(Flags),
gleam@option:from_result(
_pipe@5
)
end,
Result = begin
gleam@result:'try'(
case Filter@1 of
{some, Filter@2} ->
case gleam@regexp:from_string(
Filter@2
) of
{ok, Regex} ->
{ok,
{some,
Regex}};
{error,
{compile_error,
Error,
_}} ->
{error,
[{invalid_argument,
<<"filter"/utf8>>,
Filter@2,
Error}]}
end;
none ->
{ok, none}
end,
fun(Regex@1) ->
gleam@result:'try'(
parse_definitions(
Args
),
fun(Definitions) ->
Options = {options,
embeds@generate:to_gleam_module_name(
Module@1
),
<<"//// Generated by `embeds`, manual edits are futile"/utf8>>,
typed_parse_fallback(
Definitions,
fun default_parse/2
),
Prefix@2,
Regex@1},
generate(
Options
)
end
)
end
)
end,
case Result of
{ok, nil} ->
nil;
{error, Errs} ->
gleam_stdlib:println_error(
embeds@generate:describe_errors(
Errs
)
)
end,
nil
end
)
end
)
end
)
end
)
end
).
-file("src/embeds/env.gleam", 113).
?DOC(" runs generate using command line arguments to fill out the arguments.\n").
-spec main() -> nil.
main() ->
_pipe = glint:new(),
_pipe@1 = glint:pretty_help(_pipe, glint:default_pretty_help()),
_pipe@2 = glint:add(_pipe@1, [], generate_cmd()),
glint:run(_pipe@2, erlang:element(4, argv:load())).