src/exodus.erl

-module(exodus).

-export([run/1, list/1, revert/1, redo/1, config/2, config/3, timestamp/0, cleanup/1]).

-spec config(list() | map(), module()) -> map().
config(Migrations, Driver) ->
    config(Migrations, Driver, null).

-spec config(list() | map(), module(), any()) -> map().
config(#{} = Migrations, Driver, Conn) ->
    config(maps:to_list(Migrations), Driver, Conn);
config(Migrations, Driver, Conn) ->
    #{migrations => Migrations,
      driver => Driver,
      conn => Conn}.

-spec run(map()) -> ok | {error, any()}.
run(#{driver := Driver,
      conn := Conn,
      migrations := Migrations}) ->
    case Driver:up(Conn) of
        {error, _Err} = E ->
            E;
        ok ->
            case Driver:list(Conn) of
                {error, _Err} = E ->
                    E;
                {ok, AppliedMigrations} ->
                    NewMigrations = filter_applied_migrations(Migrations, AppliedMigrations),
                    run_migrations(Driver, Conn, sort_migrations(NewMigrations))
            end
    end;
run(_) ->
    {error, invalid_config}.

-spec run_migrations(module(), any(), list()) -> ok | {error, any()}.
run_migrations(Driver, Conn, [{Migration, Timestamp} | T]) ->
    case Driver:exec(Conn, Migration:up()) of
        {error, _Err} = E ->
            E;
        ok ->
            case Driver:add(Conn, atom_to_binary(Migration), Timestamp) of
                {error, _Err} = E ->
                    E;
                ok ->
                    run_migrations(Driver, Conn, T)
            end
    end;
run_migrations(_Driver, _Query, []) ->
    ok.

-spec filter_applied_migrations(list(), list()) -> list().
filter_applied_migrations(Migrations, AppliedMigrations) ->
    FormatAppliedMigrations =
        lists:map(fun({_Id, Migration, Timestamp}) -> {Migration, Timestamp} end,
                  AppliedMigrations),
    AppliedMigrationsMap = maps:from_list(FormatAppliedMigrations),
    MigrationsMap = maps:from_list(Migrations),
    maps:to_list(
        maps:without(
            maps:keys(AppliedMigrationsMap), MigrationsMap)).

-spec sort_migrations(list()) -> list().
sort_migrations(Migrations) ->
    lists:sort(fun({_, A}, {_, B}) -> A =< B end, Migrations).

-spec list(map()) -> {ok, list()} | {error, any()}.
list(#{driver := Driver, conn := Conn}) ->
    Driver:list(Conn).

-spec revert(map()) -> ok | {error, any()}.
revert(#{driver := Driver, conn := Conn}) ->
    case Driver:last(Conn) of
        {error, _Err} = E ->
            E;
        {ok, {Id, Migration, _Timestamp}} ->
            case Driver:exec(Conn, Migration:down()) of
                {error, _Err} = E ->
                    E;
                ok ->
                    case Driver:delete(Conn, Id) of
                        {error, _Err} = E ->
                            E;
                        ok ->
                            ok
                    end
            end
    end.

-spec redo(map()) -> ok | {error, any()}.
redo(Config) ->
    case revert(Config) of
        {error, _Err} = E ->
            E;
        ok ->
            run(Config)
    end.

-spec cleanup(map()) -> ok | {error, any()}.
cleanup(#{driver := Driver,
          conn := Conn}) ->
    case Driver:list(Conn) of
        {error, _Err} = E ->
            E;
        {ok, AppliedMigrations} ->
            case cleanup_migrations(Driver, Conn, lists:reverse(AppliedMigrations)) of
                {error, _Err} = E ->
                    E;
                ok ->
                    Driver:down(Conn)
            end
    end.

-spec cleanup_migrations(module(), any(), list()) -> ok | {error, any()}.
cleanup_migrations(Driver, Conn, [{Id, _Name, _Timestamp} | T]) ->
    case Driver:delete(Conn, Id) of
        {error, _Err} = E ->
            E;
        ok ->
            cleanup_migrations(Driver, Conn, T)
    end;
cleanup_migrations(_Driver, _Conn, []) ->
    ok.

-spec timestamp() -> integer().
timestamp() ->
    {A, B, C} = erlang:timestamp(),
    Bin = list_to_binary([integer_to_list(A), integer_to_list(B), integer_to_list(C)]),
    binary_to_integer(Bin).

-ifdef(TEST).

-include_lib("eunit/include/eunit.hrl").

filter_applied_migrations_test() ->
    Migrations = [{create_todos, 12345}, {create_tags, 123456}],
    Migrations2 = [{1, create_todos, 12345}],
    ?assertEqual([{create_tags, 123456}], filter_applied_migrations(Migrations, Migrations2)).

sort_migrations_test() ->
    Migrations = [{create_todos, 12345}, {create_tags, 123456}, {create_stuff, 123}],
    ?assertEqual([{create_stuff, 123}, {create_todos, 12345}, {create_tags, 123456}],
                 sort_migrations(Migrations)).

timestamp_test() ->
    T = timestamp(),
    ?assert(is_integer(T)).

-endif.