src/conf.erl

%%%-------------------------------------------------------------------
%%% @author Evgeny Khramtsov <xramtsov@gmail.com>
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.
%%%
%%%-------------------------------------------------------------------
-module(conf).

-behaviour(application).

%% API
-export([start/0]).
-export([stop/0]).
-export([load_file/1]).
-export([reload_file/0]).
-export([reload_file/1]).
-export([load/1]).
-export([reload/1]).
-export([get_path/0]).
-export([format_error/1]).
%% Application callbacks
-export([start/2, stop/1, config_change/3]).
-export_type([error_reason/0, apps_config/0]).

-type backend() :: conf_yaml_backend.
-type error_reason() :: {bad_env, conf_env:error_reason()} |
                        {bad_ref, conf_file:ref(), conf_file:error_reason()} |
                        {backend(), term()}.
-type apps_config() :: [{atom(), #{atom() => term()} | {atom(), term()}}].
-callback validator() -> yval:validator().

%%%===================================================================
%%% API
%%%===================================================================
-spec load_file(file:filename_all()) -> ok | {error, error_reason()}.
load_file(Path0) ->
    case prep_path(Path0) of
        {ok, Path} -> read_and_load_file(Path, false);
        error -> erlang:error(badarg, [Path0])
    end.

-spec reload_file() -> ok | {error, error_reason()}.
reload_file() ->
    case conf_env:file() of
        {ok, Path} ->
            reload_file(Path);
        {error, Reason} ->
            {error, {bad_env, Reason}}
    end.

-spec reload_file(file:filename_all()) -> ok | {error, error_reason()}.
reload_file(Path0) ->
    case prep_path(Path0) of
        {ok, Path} -> read_and_load_file(Path, true);
        error -> erlang:error(badarg, [Path0])
    end.

-spec load(term()) -> ok | {error, error_reason()}.
load(Y) ->
    load(Y, false).

-spec reload(term()) -> ok | {error, error_reason()}.
reload(Y) ->
    load(Y, true).

-spec get_path() -> {ok, binary()} | {error, error_reason()}.
get_path() ->
    case conf_env:file() of
        {ok, _} = OK -> OK;
        {error, Reason} -> {error, {bad_env, Reason}}
    end.

-spec format_error(error_reason()) -> string().
format_error({bad_env, Reason}) ->
    conf_env:format_error(Reason);
format_error({bad_ref, _Ref, Reason}) ->
    conf_file:format_error(Reason);
format_error({Module, Reason}) ->
    Module:format_error(Reason).

-spec start() -> ok | {error, term()}.
start() ->
    case application:ensure_all_started(?MODULE) of
        {ok, _} -> ok;
        {error, _} = Err -> Err
    end.

-spec stop() -> ok | {error, term()}.
stop() ->
    application:stop(?MODULE).

%%%===================================================================
%%% Application callbacks
%%%===================================================================
-spec start(normal | {takeover, node()} | {failover, node()}, term()) ->
          {ok, pid()} | {error, term()}.
start(_StartType, _StartArgs) ->
    case conf_env:file() of
        {ok, Path} ->
            case read_and_load_file(Path, false) of
                ok ->
                    conf_sup:start_link();
                {error, Reason} = Err ->
                    logger:critical(
                      "Failed to load configuration from ~ts: ~ts",
                      [conf_file:format_ref(Path), format_error(Reason)]),
                    do_stop(Err)
            end;
        {error, {undefined_env, _}} ->
            conf_sup:start_link();
        {error, Reason} = Err ->
            logger:critical("~s", [conf_env:format_error(Reason)]),
            do_stop(Err)
    end.

-spec stop(term()) -> ok.
stop(_State) ->
    ok.

-spec config_change(Changed :: [{atom(), term()}],
                    New :: [{atom(), term()}],
                    Removed :: [atom()]) -> ok.
config_change(_Changed, _New, _Removed) ->
    ok.

%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec read_and_load_file(binary(), boolean()) -> ok | {error, error_reason()}.
read_and_load_file(Path, Reload) ->
    case conf_file:path_to_ref(Path) of
        {error, Reason} -> {error, {bad_ref, Path, Reason}};
        {ok, Ref} ->
            Mimes = conf_yaml_backend:mime_types(),
            case conf_file:read(Ref, Mimes) of
                {error, Reason} ->
                    {error, {bad_ref, Ref, Reason}};
                {ok, Data} ->
                    case conf_yaml_backend:decode(Data) of
                        {ok, Y} ->
                            load(Y, Reload);
                        {error, Reason} ->
                            {error, {conf_yaml_backend, Reason}}
                    end
            end
    end.

-spec load(term(), boolean()) -> ok | {error, error_reason()}.
load(Y, Reload) ->
    case conf_yaml_backend:validate(Y) of
        {ok, Config} ->
            case conf_env:load(Config, Reload) of
                ok -> ok;
                {error, Errors} ->
                    report_config_change_errors(Errors)
            end;
        {error, Reason} ->
            {error, {conf_yaml_backend, Reason}}
    end.

-spec report_config_change_errors(term()) -> ok.
report_config_change_errors(Errors) when is_list(Errors) ->
    Errors1 = lists:filter(
                fun({module_not_defined, _}) -> false;
                   ({application_not_found, _}) -> false;
                   (_) -> true
                end, Errors),
    lists:foreach(
      fun(Error) ->
              logger:warning(
                "Failed to change configuration of Erlang application: ~p",
                [Error])
      end, Errors1);
report_config_change_errors(Error) ->
    logger:warning(
      "Failed to change configuration of Erlang applications: ~p",
      [Error]).

-spec do_stop({error, term()}) -> {error, term()}.
do_stop({error, Reason} = Err) ->
    case conf_env:on_fail() of
        stop ->
            Err;
        OnFail ->
            flush_logger(),
            Status = case OnFail of
                         crash -> format_error(Reason);
                         _Halt -> 1
                     end,
            halt(Status)
    end.

-spec flush_logger() -> ok.
flush_logger() ->
    lists:foreach(
      fun(#{id := Name, module := Mod}) ->
              case conf_misc:try_load(Mod, filesync, 1) of
                  {ok, _} -> Mod:filesync(Name);
                  _ -> ok
              end
      end, logger:get_handler_config()).

-spec prep_path(file:filename_all()) -> {ok, binary()} | error.
prep_path(Path) when is_binary(Path), Path /= <<>> ->
    {ok, Path};
prep_path(Path) when is_list(Path), Path /= [] ->
    try unicode:characters_to_binary(Path) of
        Bin when is_binary(Bin) -> {ok, Bin};
        _ -> error
    catch _:_ ->
            error
    end;
prep_path(_) ->
    error.