src/dotenv_config_parser.erl

-module(dotenv_config_parser).

-include("dotenv_config.hrl").

-callback get_parser() -> parser().

-ifdef(EUNIT).
-compile(export_all).
-endif.

-export([parse_file/1, parse_config/2]).
-export_type([parsed_config/0, parsed_config_raw/0]).

-spec parse_file(file:name_all()) -> {ok, parsed_config_raw()} | {error, any()}.
parse_file(FileName) ->
    case file:read_file(FileName) of
        {ok, FileContent} ->
            parse_file_content(FileContent);
        {error, Reason} ->
            {error, Reason}
    end.

-spec parse_file_content(binary()) -> {ok, parsed_config_raw()} | {error, any()}.
parse_file_content(FileContent) ->
    Lines = binary:split(FileContent, <<"\n">>, [global, trim_all]),
    {_, ParsedConfigFileRaw} = lists:foldl(
        fun
            (Line, {{multi_line, {ConfigItemName, ParsedHead}}, ParsedLines}) ->
                case parse_multi_line(Line, ParsedHead) of
                    {ok, end_multi_line} ->
                        {single_line, maps:put(ConfigItemName, ParsedHead, ParsedLines)};
                    {ok, ParsedHead1} ->
                        {{multi_line, {ConfigItemName, ParsedHead1}}, ParsedLines}
                end;
            (Line, {single_line, ParsedLines}) ->
                case parse_single_line(Line) of
                    {ok, {ConfigItemName, multi_line_start}} ->
                        {{multi_line, {ConfigItemName, <<>>}}, ParsedLines};
                    {ok, {ConfigItemName, ConfigItemRawValue}} ->
                        {single_line, maps:put(ConfigItemName, ConfigItemRawValue, ParsedLines)};
                    error ->
                        exit(<<"Can't parse this line from doteenv config: ", Line/binary>>)
                end
        end,
        {single_line, #{}},
        Lines
    ),
    {ok, ParsedConfigFileRaw}.

-spec parse_multi_line(binary(), config_item_raw_value()) ->
    {ok, config_item_raw_value()} | {ok, end_multi_line}.
parse_multi_line(Line, ParsedHead) ->
    case {ParsedHead, string:trim(Line)} of
        {_, <<"\"\"\"">>} ->
            {ok, end_multi_line};
        {<<>>, _} ->
            {ok, <<Line/binary>>};
        {ParsedHead, _} ->
            {ok, <<ParsedHead/binary, "\n", Line/binary>>}
    end.

-spec parse_single_line(binary()) ->
    {ok, {config_item_name(), config_item_raw_value()}}
    | {ok, {config_item_name(), multi_line_start}}
    | error.
parse_single_line(Line) ->
    case binary:split(Line, <<"=">>, [trim_all]) of
        [Key, <<"\"\"\"">>] ->
            {ok, {string:trim(Key), multi_line_start}};
        [Key, Value] ->
            {ok, {string:trim(Key), string:trim(Value)}};
        _ ->
            error
    end.

-spec parse_config(parsed_config_raw(), module()) -> {ok, parsed_config()}.
parse_config(Config, Module) ->
    ConfigItemsParsers = Module:get_parser(),

    ParsedConfig = lists:foldl(
        fun({ConfigItemName, Parser}, ParsedConfigAcc) ->
            DefaultValue = get_default_value(Config, ConfigItemName),

            case get_environment_variable(ConfigItemName, DefaultValue) of
                not_found ->
                    exit(<<"Can't find config item: ", ConfigItemName/binary>>);
                already_stored ->
                    ParsedConfigAcc;
                ConfigItemRawValue ->
                    ConfigItemRawValue1 = maybe_trim_quotes(ConfigItemRawValue),
                    case parse_config_item(ConfigItemRawValue1, Parser) of
                        {ok, ConfigItemValue} ->
                            [{ConfigItemName, ConfigItemValue} | ParsedConfigAcc];
                        error ->
                            BinParser = dotenv_config_error:to_binary(Parser),
                            exit(
                                <<"Can't parse config item: ", ConfigItemName/binary, " of value: ",
                                    ConfigItemRawValue1/binary, " to type: ", BinParser/binary>>
                            )
                    end
            end
        end,
        [],
        ConfigItemsParsers
    ),
    {ok, ParsedConfig}.

-spec maybe_trim_quotes(config_item_raw_value()) -> config_item_raw_value().
maybe_trim_quotes(ConfigItemRawValue) ->
    LastByte = byte_size(ConfigItemRawValue) - 1,
    case {binary:at(ConfigItemRawValue, 0), binary:at(ConfigItemRawValue, LastByte)} of
        % double quotes
        {34, 34} ->
            string:trim(ConfigItemRawValue, both, "\"");
        % single quotes
        {39, 39} ->
            string:trim(ConfigItemRawValue, both, "\'");
        _ ->
            ConfigItemRawValue
    end.

-spec get_default_value(parsed_config_raw(), config_item_name()) ->
    config_item_raw_value() | not_found | already_stored.
get_default_value(Config, ConfigItemName) ->
    MaybeValueFromConfig = maps:get(ConfigItemName, Config, not_found),
    IsAlreadyStored = is_value_already_stored(ConfigItemName),
    case {MaybeValueFromConfig, IsAlreadyStored} of
        {not_found, false} ->
            not_found;
        {not_found, true} ->
            already_stored;
        {ValueFromConfig, _} ->
            ValueFromConfig
    end.

-spec get_environment_variable(
    config_item_name(), config_item_raw_value() | not_found | already_stored
) -> config_item_raw_value() | not_found | already_stored.
get_environment_variable(ConfigItemName, Default) ->
    case os:getenv(binary_to_list(ConfigItemName)) of
        false ->
            Default;
        EnvironmentValue ->
            list_to_binary(EnvironmentValue)
    end.

-spec is_value_already_stored(config_item_name()) -> boolean().
is_value_already_stored(ConfigItemName) ->
    case dotenv_config_storage:get(ConfigItemName) of
        {error, not_found} ->
            false;
        {ok, _StoredValue} ->
            true
    end.

-spec parse_config_item(config_item_raw_value(), config_item_type()) ->
    {ok, config_item_value()} | error.
parse_config_item(ConfigItemRawValue, str) ->
    {ok, ConfigItemRawValue};
parse_config_item(ConfigItemRawValue, int) ->
    case string:to_integer(ConfigItemRawValue) of
        {Value, _Rest} when is_integer(Value) ->
            {ok, Value};
        _ ->
            error
    end;
parse_config_item(ConfigItemRawValue, bool) ->
    try binary_to_existing_atom(ConfigItemRawValue) of
        true -> {ok, true};
        false -> {ok, false}
    catch
        _:_ ->
            error
    end;
parse_config_item(ConfigItemRawValue, json) ->
    try jiffy:decode(ConfigItemRawValue, [return_maps]) of
        JsonValue ->
            {ok, JsonValue}
    catch
        _:_ ->
            error
    end;
parse_config_item(ConfigItemRawValue, atom) ->
    try binary_to_atom(ConfigItemRawValue) of
        AtomValue ->
            {ok, AtomValue}
    catch
        _:_ ->
            error
    end;
parse_config_item(ConfigItemRawValue, module) ->
    try binary_to_existing_atom(ConfigItemRawValue) of
        ModuleValue ->
            case code:ensure_loaded(ModuleValue) of
                {module, ModuleValue} ->
                    {ok, ModuleValue};
                _ ->
                    error
            end
    catch
        _:_ ->
            error
    end;
parse_config_item(ConfigItemRawValue, charlist) ->
    {ok, binary_to_list(ConfigItemRawValue)};
parse_config_item(ConfigItemRawValue, [Type | Rest]) ->
    case Type of
        {exact, ExactValue} when ExactValue =:= ConfigItemRawValue ->
            {ok, ExactValue};
        SingleType when is_atom(SingleType) ->
            parse_config_item(ConfigItemRawValue, SingleType);
        {exact, _} ->
            parse_config_item(ConfigItemRawValue, Rest)
    end;
parse_config_item(ConfigItemRawValue, ConvertFunction) when is_function(ConvertFunction) ->
    try ConvertFunction(ConfigItemRawValue) of
        ConfigItemValue ->
            {ok, ConfigItemValue}
    catch
        _:_ ->
            error
    end;
parse_config_item(_, _) ->
    error.