File katana-0.4.0-git.patch of Package katana
diff --git a/README.md b/README.md
index a93d94b..d7cd4cb 100644
--- a/README.md
+++ b/README.md
@@ -16,9 +16,6 @@ To sum up this is a grab bag of useful functions (ideally).
# Contact Us
-For **questions** or **general comments** regarding the use of this library,
-please use our public [hipchat room](https://inaka.net/hipchat).
-
If you find any **bugs** or have a **problem** while using this library, please
[open an issue](https://github.com/inaka/erlang-katana/issues/new) in this repo
(or a pull request :)).
@@ -37,7 +34,6 @@ Check out as well [katana-code](https://github.com/inaka/katana-code) and [katan
* `ktn_date`: functions useful for handling dates and time values.
* `ktn_debug`: functions useful for debugging.
-* `ktn_fsm`: a _nice_ wrapper on top of `gen_fsm`
* `ktn_json`: functions useful for processing & creating JSON.
* `ktn_maps`: functions useful for handling maps.
* `ktn_numbers`: functions useful for processing numeric values.
@@ -46,6 +42,28 @@ Check out as well [katana-code](https://github.com/inaka/katana-code) and [katan
* `ktn_task`: functions useful for managing asyncronous tasks.
* `ktn_user_default`: useful functions for your erlang shell.
+### `ktn_date`
+
+This module contains functions to manipulate date and time values.
+
+#### `shift_days`
+
+With `shift_days(Datetime :: calendar:datetime(), N :: integer())` you can move the date expressed in `DateTime`, `N` days to the future or to the past.
+
+#### `shift_months`
+
+With `shift_months(Date :: calendar:date(), N :: integer())` you can move the date expressed in `Date`, `N` months to the future or to the past.
+
+Examples:
+```erlang
+$> ktn_date:shift_months({2018, 8, 15}, 2).
+{2018, 10, 15}
+$> ktn_date:shift_months({2018, 8, 15}, -2).
+{2018, 6, 15}
+$> ktn_date:shift_months({2018, 8, 15}, 6).
+{2019, 2, 15}
+```
+
### `ktn_user_default`
This module contains functions that are nice to have in your user default
@@ -142,12 +160,12 @@ all cases.
A better way to structure the code would be preferable, one that makes the
flow obvious and separates responsibilities into appropriate code segments.
One way is a finite state machine. OTP even has a behaviour for exactly this
-purpose. However, gen_fsm does not exactly fit our needs:
-To begin with, gen_fsms run in their own process, which may not be what is
-needed. Second, logic flow is not immediately obvious because the fsm's
-state depends on the result of each state function. Finally, a gen_fsm
+purpose. However, gen_statem does not exactly fit our needs:
+To begin with, gen_statems run in their own process, which may not be what is
+needed. Second, logic flow is not immediately obvious because the FSM's
+state depends on the result of each state function. Finally, a gen_statem
transitions only on receiving input, whereas we are looking for something
-that runs like normal code, "on its own". So, our fsm will be defined by
+that runs like normal code, "on its own". So, our FSM will be defined by
something like a transition table, a single place you can look at and know
how it executes. This specification will "drive" the recipe.
diff --git a/rebar.config b/rebar.config
index 3e0d5eb..fdddd2a 100644
--- a/rebar.config
+++ b/rebar.config
@@ -23,11 +23,6 @@
{profiles, [
{test, [
{deps, [{xref_runner, "1.0.0"}]}
- ]},
- {shell, [
- {deps, [
- {sync, {git, "https://github.com/rustyio/sync.git", {ref, "9c78e7b"}}}
- ]}
]}
]}.
@@ -59,7 +54,7 @@
%% == Dependencies ==
-{deps, [{elvis, "0.3.5", {pkg, elvis_core}}]}.
+{deps, [{elvis, "0.3.9", {pkg, elvis_core}}]}.
%% == Dialyzer ==
@@ -72,7 +67,3 @@
, {plt_location, local}
, {base_plt_apps, [stdlib, kernel]}
, {base_plt_location, global}]}.
-
-%% == Shell ==
-
-{shell, [{apps, [sync]}]}.
\ No newline at end of file
diff --git a/rebar.lock b/rebar.lock
index f6c20bf..0f74d44 100644
--- a/rebar.lock
+++ b/rebar.lock
@@ -1,16 +1,12 @@
{"1.1.0",
[{<<"aleppo">>,{pkg,<<"inaka_aleppo">>,<<"1.0.0">>},2},
- {<<"elvis">>,{pkg,<<"elvis_core">>,<<"0.3.5">>},0},
- {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},2},
+ {<<"elvis">>,{pkg,<<"elvis_core">>,<<"0.3.9">>},0},
{<<"katana_code">>,{pkg,<<"katana_code">>,<<"0.1.0">>},1},
- {<<"lager">>,{pkg,<<"lager">>,<<"3.2.4">>},1},
{<<"zipper">>,{pkg,<<"zipper">>,<<"1.0.1">>},1}]}.
[
{pkg_hash,[
{<<"aleppo">>, <<"8DB14CF16BB8C263C14FF4C3F69F64D7C849D40888944F4204D2CA74F1114CEB">>},
- {<<"elvis">>, <<"9C6DE2DA5317081D12512CA34EC9CAC858D3A169E6882E86BFAC97FA47962C4D">>},
- {<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>},
+ {<<"elvis">>, <<"6B3C18E1E1CC9BC5684282F59A95B6B42898C1A27E0E2F3CFE9B384AF76A7629">>},
{<<"katana_code">>, <<"C34F3926A258D6BEACD8D21F140F3D47D175501936431C460B144339D5271A0B">>},
- {<<"lager">>, <<"A6DEB74DAE7927F46BD13255268308EF03EB206EC784A94EAF7C1C0F3B811615">>},
{<<"zipper">>, <<"3CCB4F14B97C06B2749B93D8B6C204A1ECB6FAFC6050CACC3B93B9870C05952A">>}]}
].
diff --git a/src/ktn_date.erl b/src/ktn_date.erl
index 688ba59..701e67e 100644
--- a/src/ktn_date.erl
+++ b/src/ktn_date.erl
@@ -3,8 +3,12 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-module(ktn_date).
+-define(SECONDS_IN_A_DAY, 86400).
+
-export([
- now_human_readable/0
+ now_human_readable/0,
+ shift_days/2,
+ shift_months/2
]).
-type date() :: {date, {non_neg_integer(), 1..12, 1..31}}.
@@ -27,3 +31,39 @@ now_human_readable() ->
DateList = io_lib:format("~p-~2..0B-~2..0BT~p:~p:~p.~6..0wZ",
[Year, Month, Day, Hour, Minute, Second, Micro]),
list_to_binary(DateList).
+
+%% @doc Moves the received datetime `N' days to the future (or to the past)
+-spec shift_days(calendar:datetime(), integer()) -> calendar:datetime().
+shift_days(Datetime, N) ->
+ Shift = ?SECONDS_IN_A_DAY * N,
+ Secs = calendar:datetime_to_gregorian_seconds(Datetime),
+ calendar:gregorian_seconds_to_datetime(Secs + Shift).
+
+%% @doc Moves the received date `N' months to the future (or to the past)
+-spec shift_months(calendar:date(), integer()) -> calendar:date().
+shift_months({Y, M, D}, N) ->
+ %% in order for the modular arithmetic to work, months in this function
+ %% range from 0 to 11 (January to December)
+ TotalMonths = 12*Y + M-1 + N,
+
+ case TotalMonths >= 0 of
+ true ->
+ Month = TotalMonths rem 12,
+ Year = (TotalMonths - Month) div 12,
+
+ %% add one back to the month to fix our tricky mod 12
+ find_valid_date({Year, Month+1, D});
+ false ->
+ error(out_of_bounds)
+ end.
+
+%% @doc Returns `Date' if valid. Otherwise, returns `Date' replacing `Day`
+%% with the last day of the month.
+find_valid_date(Date) ->
+ case calendar:valid_date(Date) of
+ true ->
+ Date;
+ false ->
+ {Y, M, _} = Date,
+ {Y, M, calendar:last_day_of_the_month(Y, M)}
+ end.
\ No newline at end of file
diff --git a/src/ktn_fsm.erl b/src/ktn_fsm.erl
deleted file mode 100644
index f11ea62..0000000
--- a/src/ktn_fsm.erl
+++ /dev/null
@@ -1,67 +0,0 @@
-%%% @doc "Nicer" interfact to gen_fsm.
-%%% With this module you can:
-%%% - use ktn_fsm:call instead of gen_fsm:sync_send_event
-%%% - use ktn_fsm:call_through instead of gen_fsm:sync_send_all_state_event
-%%% - use ktn_fsm:cast instead of gen_fsm:send_event
-%%% - use ktn_fsm:cast_through instead of gen_fsm:sync_send_event
-%%% - use ktn_fsm:state to retrieve the current gen_fsm state
-%%% @todo implement the inner functional changes:
-%%% - leave some of the callbacks undefined. If a callback function is
-%%% undefined, the caller will throw an exception, but the fsm will keep
-%%% looping. This way you don't have to have catch-all clauses there.
--module(ktn_fsm).
-
--ignore_xref([start/3]).
-
--export(
- [ state/1
- , call/2
- , call/3
- , call_through/2
- , call_through/3
- , cast/2
- , cast_through/2
- ]).
-
--export(['$handle_undefined_function'/2]).
-
--spec state(sys:name()) -> atom().
-state(Fsm) ->
- {StateName, _State} = sys:get_state(Fsm),
- StateName.
-
--spec call(sys:name(), term()) -> term().
-call(Fsm, Call) ->
- gen_fsm:sync_send_event(Fsm, Call).
-
--spec call(sys:name(), term(), timeout()) -> term().
-call(Fsm, Call, Timeout) ->
- gen_fsm:sync_send_event(Fsm, Call, Timeout).
-
--spec call_through(sys:name(), term()) -> term().
-call_through(Fsm, Call) ->
- gen_fsm:sync_send_all_state_event(Fsm, Call).
-
--spec call_through(sys:name(), term(), timeout()) -> term().
-call_through(Fsm, Call, Timeout) ->
- gen_fsm:sync_send_all_state_event(Fsm, Call, Timeout).
-
--spec cast(sys:name(), term()) -> ok.
-cast(Fsm, Cast) ->
- gen_fsm:send_event(Fsm, Cast).
-
--spec cast_through(sys:name(), term()) -> ok.
-cast_through(Fsm, Cast) ->
- gen_fsm:send_all_state_event(Fsm, Cast).
-
--spec '$handle_undefined_function'(atom(), [any()]) -> any().
-'$handle_undefined_function'(Func, Args) ->
- {module, gen_fsm} =
- case code:is_loaded(gen_fsm) of
- false -> code:load_file(gen_fsm);
- _ -> {module, gen_fsm}
- end,
- case erlang:function_exported(gen_fsm, Func, length(Args)) of
- true -> erlang:apply(gen_fsm, Func, Args);
- false -> error_handler:raise_undef_exception(?MODULE, Func, Args)
- end.
diff --git a/src/ktn_os.erl b/src/ktn_os.erl
index 777604a..c6e6618 100644
--- a/src/ktn_os.erl
+++ b/src/ktn_os.erl
@@ -3,7 +3,10 @@
-export([command/1, command/2]).
--type opts() :: #{log_fun => fun((iodata()) -> any()), timeout => integer()}.
+-type opts() :: #{ log_fun => fun((iodata()) -> any())
+ , timeout => integer()
+ , monitor => reference()
+ }.
-type exit_status() :: integer().
-spec command(iodata()) -> {exit_status(), string()}.
@@ -13,15 +16,19 @@ command(Cmd) ->
-spec command(iodata(), opts()) -> {exit_status(), string()}.
command(Cmd, Opts) ->
- PortOpts = [stream, exit_status, eof],
+ PortOpts = [hide, stream, exit_status, eof, stderr_to_stdout],
Port = open_port({spawn, shell_cmd()}, PortOpts),
- erlang:port_command(Port, make_cmd(Cmd)),
- get_data(Port, Opts, []).
+ MonRef = erlang:monitor(port, Port),
+ true = erlang:port_command(Port, make_cmd(Cmd)),
+ Result = get_data(Port, Opts#{monitor => MonRef}, []),
+ _ = demonitor(MonRef, [flush]),
+ Result.
-spec get_data(port(), opts(), [string()]) -> {exit_status(), string()}.
get_data(Port, Opts, Data) ->
%% Get timeout option or an hour if undefined.
Timeout = maps:get(timeout, Opts, 600000),
+ MonRef = maps:get(monitor, Opts),
receive
{Port, {data, NewData}} ->
case maps:get(log_fun, Opts, undefined) of
@@ -30,13 +37,17 @@ get_data(Port, Opts, Data) ->
end,
get_data(Port, Opts, [NewData | Data]);
{Port, eof} ->
- port_close(Port),
+ catch port_close(Port),
+ flush_until_down(Port, MonRef),
receive
{Port, {exit_status, ExitStatus}} ->
{ExitStatus, lists:flatten(lists:reverse(Data))}
- end
+ end;
+ {'DOWN', MonRef, _, _, Reason} ->
+ flush_exit(Port),
+ exit({error, Reason, lists:flatten(lists:reverse(Data))})
after
- Timeout -> throw(timeout)
+ Timeout -> exit(timeout)
end.
-spec make_cmd(string()) -> iodata().
@@ -46,4 +57,27 @@ make_cmd(Cmd) ->
[$(, unicode:characters_to_binary(Cmd), "\n) </dev/null; exit\n"].
-spec shell_cmd() -> string().
-shell_cmd() -> "sh -s unix:cmd 2>&1".
+shell_cmd() -> "/bin/sh -s unix:cmd".
+
+%% When port_close returns we know that all the
+%% messages sent have been sent and that the
+%% DOWN message is after them all.
+flush_until_down(Port, MonRef) ->
+ receive
+ {Port, {data, Bytes}} ->
+ flush_until_down(Port, MonRef);
+ {'DOWN', MonRef, _, _, _} ->
+ flush_exit(Port)
+ end.
+
+%% The exit signal is always delivered before
+%% the down signal, so we can be sure that if there
+%% was an exit message sent, it will be in the
+%% mailbox now.
+flush_exit(Port) ->
+ receive
+ {'EXIT', Port, _} ->
+ ok
+ after 0 ->
+ ok
+ end.
diff --git a/test/cover.spec b/test/cover.spec
index 767df06..49614fa 100644
--- a/test/cover.spec
+++ b/test/cover.spec
@@ -4,7 +4,6 @@
[ktn_binary,
ktn_date,
ktn_debug,
- ktn_fsm,
ktn_json,
ktn_lists,
ktn_maps,
diff --git a/test/ktn_date_SUITE.erl b/test/ktn_date_SUITE.erl
new file mode 100644
index 0000000..bf52405
--- /dev/null
+++ b/test/ktn_date_SUITE.erl
@@ -0,0 +1,50 @@
+-module(ktn_date_SUITE).
+
+-export([all/0]).
+
+-export([
+ shift_days/1,
+ shift_months/1
+ ]).
+
+-type config() :: [{atom(), term()}].
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Common test
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+-spec all() -> [atom()].
+all() ->
+ [
+ shift_days,
+ shift_months
+ ].
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Test Cases
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+-spec shift_days(config()) -> ok.
+shift_days(_Config) ->
+ Datetime = {{2018, 8, 15}, {12, 12, 12}},
+ {{2018, 8, 16}, {12, 12, 12}} = ktn_date:shift_days(Datetime, 1),
+ {{2018, 8, 14}, {12, 12, 12}} = ktn_date:shift_days(Datetime, -1),
+ {{2018, 9, 4}, {12, 12, 12}} = ktn_date:shift_days(Datetime, 20),
+ ok.
+
+-spec shift_months(config()) -> ok.
+shift_months(_Config) ->
+ {2018, 10, 15} = ktn_date:shift_months({2018, 8, 15}, 2),
+ {2018, 6, 15} = ktn_date:shift_months({2018, 8, 15}, -2),
+ {2019, 2, 15} = ktn_date:shift_months({2018, 8, 15}, 6),
+
+ {2018, 10, 31} = ktn_date:shift_months({2018, 8, 31}, 2),
+ {2018, 6, 30} = ktn_date:shift_months({2018, 8, 31}, -2),
+ {2019, 2, 28} = ktn_date:shift_months({2018, 8, 31}, 6),
+
+ ok = try
+ _ = ktn_date:shift_months({0, 8, 31}, -9),
+ out_of_bounds_expected
+ catch
+ error:out_of_bounds -> ok
+ end.
\ No newline at end of file
diff --git a/test/ktn_fsm_SUITE.erl b/test/ktn_fsm_SUITE.erl
deleted file mode 100644
index 59e4b37..0000000
--- a/test/ktn_fsm_SUITE.erl
+++ /dev/null
@@ -1,109 +0,0 @@
--module(ktn_fsm_SUITE).
-
--export([all/0]).
-
--export([ secure_vault/1
- ]).
-
--type config() :: [{atom(), term()}].
-
-all() -> [secure_vault].
-
--spec secure_vault(config()) -> {comment, []}.
-secure_vault(_Config) ->
- ct:comment("We start the vault with a master password and 3 attempts limit"),
- {ok, V} = secure_vault:start(masterpwd, 3),
-
- ct:comment("While it's unlocked, we can push, pop and check contents"),
- unlocked = secure_vault:state(V),
- [] = secure_vault:contents(V),
- 0 = secure_vault:count(V),
- ok = secure_vault:push(V, a),
- ok = secure_vault:push(V, b),
- [b, a] = secure_vault:contents(V),
- 2 = secure_vault:count(V),
- b = secure_vault:pop(V),
- [a] = secure_vault:contents(V),
- 1 = secure_vault:count(V),
- V ! ignored,
-
- ct:comment("We lock it with password 'pwd'"),
- ok = secure_vault:lock(V, pwd),
- locked = secure_vault:state(V),
- V ! ignored,
-
- ct:comment("While it's locked we can't do anything besides count & unlock"),
- 1 = secure_vault:count(V),
- badstate = fails(fun() -> secure_vault:lock(V, pwd2) end),
- badstate = fails(fun() -> secure_vault:lock(V, pwd) end),
- badstate = fails(fun() -> secure_vault:pop(V) end),
- ok = secure_vault:push(V, notpushed),
- 1 = secure_vault:count(V),
- badstate = fails(fun() -> secure_vault:contents(V) end),
- V ! ignored,
-
- ct:comment("We try to unlock it with a wrong password"),
- badpwd = fails(fun() -> secure_vault:unlock(V, notpwd) end),
- locked = secure_vault:state(V),
- V ! ignored,
-
- ct:comment("We unlock it with the right password"),
- ok = secure_vault:unlock(V, pwd),
- V ! ignored,
-
- ct:comment("While it's unlocked, we can push, pop and check contents again"),
- [a] = secure_vault:contents(V),
- 1 = secure_vault:count(V),
- a = secure_vault:pop(V),
- [] = secure_vault:contents(V),
- 0 = secure_vault:count(V),
- ok = secure_vault:push(V, x),
- V ! ignored,
-
- ct:comment("While it's unlocked, we can't unlock it again"),
- badstate = fails(fun() -> secure_vault:unlock(V, pwd2) end),
- badstate = fails(fun() -> secure_vault:unlock(V, pwd) end),
-
- ct:comment("We lock it again"),
- ok = secure_vault:lock(V, pwd3),
-
- ct:comment("After 3 invalid pwd attempts, the vault is closed"),
- badpwd = fails(fun() -> secure_vault:unlock(V, notpwd3) end),
- badpwd = fails(fun() -> secure_vault:unlock(V, notpwd3) end),
- closed = fails(fun() -> secure_vault:unlock(V, notpwd3) end),
- closed = secure_vault:state(V),
-
- ct:comment("When a vault is closed, there is nothing we can do..."),
- badstate = fails(fun() -> secure_vault:unlock(V, pwd3) end),
- badstate = fails(fun() -> secure_vault:lock(V, pwd3) end),
- badstate = fails(fun() -> secure_vault:lock(V, pwd) end),
- badstate = fails(fun() -> secure_vault:pop(V) end),
- ok = secure_vault:push(V, notpushed),
- 1 = secure_vault:count(V),
- badstate = fails(fun() -> secure_vault:contents(V) end),
-
- ct:comment("...except stopping it with the master password"),
- 1 = secure_vault:count(V),
- ok = secure_vault:stop(V, notmasterpwd),
- 1 = secure_vault:count(V),
- badstate = fails(fun() -> secure_vault:contents(V) end),
- V ! ignored,
-
- ok = secure_vault:stop(V, masterpwd),
- noproc =
- ktn_task:wait_for(
- fun() ->
- fails(fun() -> secure_vault:contents(V) end)
- end, noproc, 500, 10),
-
- {comment, ""}.
-
-fails(Fun) ->
- try Fun() of
- Response ->
- ct:fail("Unexpected response from ~p: ~p", [Fun, Response])
- catch
- _:{noproc, _} -> noproc;
- _:Error -> Error
- end.
-
diff --git a/test/ktn_os_SUITE.erl b/test/ktn_os_SUITE.erl
index 4b69ef1..eb8013a 100644
--- a/test/ktn_os_SUITE.erl
+++ b/test/ktn_os_SUITE.erl
@@ -68,9 +68,45 @@ command(_Config) ->
YesFun = fun() ->
case ktn_os:command("yes > /dev/null") of
{ExitStatus, _} when ExitStatus =/= 0 -> Self ! ok;
- _ -> Self ! error
+ UnexpectedResult -> Self ! {error, UnexpectedResult}
end
end,
erlang:spawn_link(YesFun),
[] = os:cmd("pkill yes"),
- ok = receive X -> X after 1000 -> error end.
+ ok = receive X -> X after 1000 -> timeout end,
+
+ ct:comment("Check result when port is closed"),
+ Yes2Fun =
+ fun() ->
+ process_flag(trap_exit, true),
+ try ktn_os:command("yes > /dev/null") of
+ UnexpectedResult -> Self ! {error, UnexpectedResult}
+ catch
+ exit:{error, _, _} -> Self ! ok
+ end
+ end,
+ FindPort =
+ fun(Proc) ->
+ fun() ->
+ {links, Links} = erlang:process_info(Proc, links),
+ [_] = [P || P <- Links, is_port(P)]
+ end
+ end,
+ Pid = erlang:spawn(Yes2Fun),
+ try
+ [Port] = ktn_task:wait_for_success(FindPort(Pid)),
+ port_close(Port),
+ ok = receive X2 -> X2 after 1000 -> timeout end
+ after
+ exit(Pid, kill)
+ end,
+
+ ct:comment("Check result when port is killed"),
+ Pid2 = erlang:spawn(Yes2Fun),
+ try
+ [Port2] = ktn_task:wait_for_success(FindPort(Pid2)),
+ exit(Port2, kill),
+ ok = receive X3 -> X3 after 1000 -> timeout end
+ after
+ exit(Pid2, kill)
+ end.
diff --git a/test/secure_vault.erl b/test/secure_vault.erl
deleted file mode 100644
index bc368fa..0000000
--- a/test/secure_vault.erl
+++ /dev/null
@@ -1,136 +0,0 @@
--module(secure_vault).
-
--behaviour(gen_fsm).
-
--ignore_xref([{ktn_fsm, start, 3}]).
-
--export([ start/2
- , state/1
- , contents/1
- , count/1
- , push/2
- , pop/1
- , lock/2
- , unlock/2
- , stop/2
- ]).
-
--export([ init/1
- , handle_event/3
- , handle_sync_event/4
- , handle_info/3
- , terminate/3
- , code_change/4
- ]).
-
--export([ unlocked/3
- , unlocked/2
- , locked/3
- , locked/2
- , closed/3
- , closed/2
- ]).
-
--dialyzer({no_missing_calls, [start/2]}).
-
-start(MasterPassword, MaxAttempts) ->
- ktn_fsm:start(
- ?MODULE,
- {MasterPassword, MaxAttempts},
- [{debug, [log, statistics, trace]}]).
-
-state(Vault) ->
- ktn_fsm:state(Vault).
-
-contents(Vault) ->
- call(Vault, contents).
-
-count(Vault) ->
- call(Vault, count).
-
-push(Vault, Content) ->
- ktn_fsm:cast(Vault, {push, Content}).
-
-pop(Vault) ->
- call(Vault, pop).
-
-lock(Vault, Password) ->
- call(Vault, {lock, Password}).
-
-unlock(Vault, Password) ->
- call(Vault, {unlock, Password}).
-
-stop(Vault, Password) ->
- ktn_fsm:cast_through(Vault, {stop, Password}).
-
-init({MasterPassword, Attempts}) ->
- StateData =
- #{ masterpwd => MasterPassword
- , max_attempts => Attempts
- , contents => []
- },
- {ok, unlocked, StateData}.
-
-unlocked({lock, Password}, _From, StateData) ->
- #{max_attempts := Attempts} = StateData,
- {reply, ok, locked, StateData#{password => Password, attempts => Attempts}};
-unlocked(pop, _From, #{contents := []} = StateData) ->
- {reply, {ok, undefined}, unlocked, StateData};
-unlocked(pop, _From, StateData) ->
- #{contents := [Content|Contents]} = StateData,
- {reply, {ok, Content}, unlocked, StateData#{contents := Contents}};
-unlocked(contents, _From, StateData) ->
- #{contents := Contents} = StateData,
- {reply, {ok, Contents}, unlocked, StateData};
-unlocked(_, _From, StateData) ->
- {reply, {error, badstate}, unlocked, StateData}.
-
-unlocked({push, Content}, StateData) ->
- #{contents := Contents} = StateData,
- {next_state, unlocked, StateData#{contents := [Content|Contents]}}.
-
-locked({unlock, Password}, _From, #{password := Password} = StateData) ->
- {reply, ok, unlocked, maps:remove(password, StateData)};
-locked({unlock, _NotPassword}, _From, #{attempts := 1} = StateData) ->
- {reply, {error, closed}, closed, StateData};
-locked({unlock, _NotPassword}, _From, StateData) ->
- #{attempts := Attempts} = StateData,
- {reply, {error, badpwd}, locked, StateData#{attempts := Attempts - 1}};
-locked(_, _From, StateData) ->
- {reply, {error, badstate}, locked, StateData}.
-
-locked(_, StateData) ->
- {next_state, locked, StateData}.
-
-closed(_, _From, StateData) ->
- {reply, {error, badstate}, closed, StateData}.
-
-closed(_, StateData) ->
- {next_state, closed, StateData}.
-
-handle_sync_event(count, _From, StateName, StateData) ->
- #{contents := Contents} = StateData,
- {reply, {ok, length(Contents)}, StateName, StateData}.
-
-handle_event({stop, Pwd}, _StateName, #{masterpwd := Pwd} = StateData) ->
- {stop, normal, StateData};
-handle_event(_, StateName, StateData) ->
- {next_state, StateName, StateData}.
-
-handle_info(_, StateName, StateData) ->
- {next_state, StateName, StateData}.
-
-terminate(_Reason, _StateName, _StateData) -> ok.
-
-code_change(_OldVsn, StateName, StateData, _Extra) ->
- {ok, StateName, StateData}.
-
-call(Vault, count) -> call(Vault, count, fun ktn_fsm:call_through/2);
-call(Vault, Call) -> call(Vault, Call, fun ktn_fsm:call/2).
-
-call(Vault, Call, Fun) ->
- case Fun(Vault, Call) of
- ok -> ok;
- {ok, Result} -> Result;
- {error, Error} -> throw(Error)
- end.