Skip to content

Instantly share code, notes, and snippets.

@niamtokik
Last active February 14, 2018 19:01
Show Gist options
  • Select an option

  • Save niamtokik/9b9ea703f797c70a5eeba4e9f3801984 to your computer and use it in GitHub Desktop.

Select an option

Save niamtokik/9b9ea703f797c70a5eeba4e9f3801984 to your computer and use it in GitHub Desktop.
Simple File Representation In Erlang/OTP with gen_statem Behavior
%%%===================================================================
%%% @doc Editor2 State Machine simulate a file with different action
%%% on it (append, delete, read).
%%% @end
%%% @todo add something to abstract state data-structure.
%%% @todo add dynamic callback functions (editor_test currently).
%%% @todo add replace function support.
%%% @todo add map function support.
%%%===================================================================
-module(editor2).
-behavior(gen_statem).
% standard start/stop functions
-export([start_link/0, start_link/1, start_link/2]).
-export([start/0, start/1, start/2]).
-export([stop/1]).
% required functions for gen_statem
-export([callback_mode/0, init/1, code_change/4, terminate/3]).
-export([readwrite/3, readonly/3, writeonly/3]).
% external api
-export([append/2, append/3]).
-export([delete/1, delete/2, delete/3]).
-export([read/1]).
-export([state/2]).
% extra informational info
-export([capability/0, capability/1]).
-define(CALL, editor_test).
-type state() :: bitstring().
%%====================================================================
%% Internal gen_statem
%%====================================================================
%%--------------------------------------------------------------------
%% @doc start_link/0, start_link/1, start_link/2 start editor2
%% with link to current process.
%% @end
%%--------------------------------------------------------------------
-spec start_link() -> {ok, pid()}.
start_link() ->
start_link([]).
-spec start_link(list()) -> {ok, pid()}.
start_link(Args) ->
gen_statem:start_link(?MODULE, Args, []).
-spec start_link(term(), list()) -> {ok, pid()}.
start_link(Name, Args) ->
gen_statem:start_link({local, Name}, ?MODULE, Args, []).
%%--------------------------------------------------------------------
%% @doc start/0, start/1, start/2 start editor2 as actor without
%% links.
%% @end
%%--------------------------------------------------------------------
-spec start() -> {ok, pid()}.
start() ->
gen_statem:start([]).
-spec start(list()) -> {ok, pid()}.
start(Args) ->
gen_statem:start(?MODULE, Args, []).
-spec start(atom(), list()) -> {ok, pid()}.
start(Name, Args) ->
gen_statem:start({local, Name}, ?MODULE, Args, []).
%%--------------------------------------------------------------------
%% @doc stop/1 stop an editor process.
%% @end
%%--------------------------------------------------------------------
-spec stop(pid()|atom()) -> ok.
stop(Pid) ->
gen_statem:stop(Pid).
%%====================================================================
%% start/stop/code_change default API
%%====================================================================
%%--------------------------------------------------------------------
%% @doc callback_mode/0 function return state_functions atom. We
%% uses a mixed calling convention from state_functions and
%% handle_event_functions.
%% @end
%%--------------------------------------------------------------------
-spec callback_mode() -> atom().
callback_mode() ->
state_functions.
%%--------------------------------------------------------------------
%% @doc init/1 function initialize editor2 process. By default,
%% this realease start in readwrite mode.
%% @end
%%--------------------------------------------------------------------
init(_Args) ->
{ok, readwrite, <<>>}.
%%--------------------------------------------------------------------
%% @doc code_change/4 is currently unsupported.
%% @end
%%--------------------------------------------------------------------
code_change(_,_,_,_) ->
ok.
%%--------------------------------------------------------------------
%% @doc terminate/3 is not currently supported.
%% @end
%%--------------------------------------------------------------------
terminate(_,_,_) ->
ok.
%%====================================================================
%% Main State Functions Definition
%%====================================================================
%%--------------------------------------------------------------------
%% @doc readwrite/3 allow to read and write into our data structure.
%% @end
%%--------------------------------------------------------------------
readwrite({call, From}, Event, Data) ->
handle_call(readwrite, From, Event, Data);
readwrite(cast, Event, Data) ->
handle_cast(readwrite, Event, Data);
readwrite(info, Event, Data) ->
handle_info(Event, Data);
readwrite(_Request, _Event, Data) ->
{keep_state, Data}.
%%--------------------------------------------------------------------
%% @doc readonly/3 allow read data structure only.
%% @end
%%--------------------------------------------------------------------
readonly({call, From}, Event, Data) ->
handle_call(readonly, From, Event, Data);
readonly(cast, Event, Data) ->
handle_cast(readonly, Event, Data);
readonly(info, Event, Data) ->
handle_info(Event, Data);
readonly(_Request, _Event, Data) ->
{keep_state, Data}.
%%--------------------------------------------------------------------
%% @doc writeonly/3 allow to only write on this write (can't delete).
%% @end
%%--------------------------------------------------------------------
writeonly({call, From}, Event, Data) ->
handle_call(writeonly, From, Event, Data);
writeonly(cast, Event, Data) ->
handle_cast(writeonly, Event, Data);
writeonly(info, Event, Data) ->
handle_info(Event, Data);
writeonly(_Request, _Event, Data) ->
{keep_state, Data}.
%%====================================================================
%% Handle Functions
%%====================================================================
%%--------------------------------------------------------------------
%% @doc handle_call/3
%% @end
%%--------------------------------------------------------------------
-spec handle_call(pid(), term(), term()) -> {keep_state, term()}.
handle_call(_From, _Event, Data) ->
{keep_state, Data}.
%%--------------------------------------------------------------------
%% @doc handle_call/4
%% @end
%%--------------------------------------------------------------------
-spec handle_call(atom(), pid(), term(), term())
-> {keep_state, term()}.
handle_call(readonly, From, read, Data) ->
handle_read(From, Data);
handle_call(readwrite, From, read, Data) ->
handle_read(From, Data);
handle_call(_, From, Event, Data) ->
handle_call(From, Event, Data).
%%--------------------------------------------------------------------
%% @doc handle_cast/2
%% @end
%%--------------------------------------------------------------------
-spec handle_cast({state, atom()}, state())
-> {next_state, atom(), state()}.
handle_cast({state, writeonly}, Data) ->
handle_state(writeonly, Data);
handle_cast({state, readonly}, Data) ->
handle_state(readonly, Data);
handle_cast({state, readwrite}, Data) ->
handle_state(readwrite, Data);
handle_cast(Event, Data) ->
io:format("global cast: ~p~n", [Event]),
{keep_state, Data}.
%%--------------------------------------------------------------------
%% @doc handle_cast/3
%% @end
%%--------------------------------------------------------------------
-spec handle_cast(atom(), term(), state())
-> {keep_state, state()}.
handle_cast(writeonly, {append, Text}, Data) ->
handle_append(Text, Data);
handle_cast(writeonly, {append, Text, Position}, Data) ->
handle_append(Data, Text, Position);
handle_cast(readwrite, {append, Text}, Data) ->
handle_append(Data, Text);
handle_cast(readwrite, {append, Text, Position}, Data) ->
handle_append(Data, Text, Position);
handle_cast(readwrite, delete, Data) ->
handle_delete(Data);
handle_cast(readwrite, {delete, Position}, Data) ->
handle_delete(Data, Position);
handle_cast(readwrite, {delete, Start, Stop}, Data) ->
handle_delete(Data, Start, Stop);
handle_cast(_, Event, Data) ->
handle_cast(Event, Data).
%%--------------------------------------------------------------------
%% @doc handle_state/2 change current editor2 state.
%% @end
%%--------------------------------------------------------------------
-spec handle_state(atom(), state())
-> {next_state, atom(), state()}.
handle_state(State, Data) ->
{next_state, State, Data}.
%%--------------------------------------------------------------------
%% @doc handle_read/2 return file content to the caller.
%% @end
%%--------------------------------------------------------------------
-spec handle_read(pid(), state())
-> {keep_state, state(), list()}.
handle_read(From, Data) ->
{keep_state,Data, [{reply, From, Data}]}.
%%--------------------------------------------------------------------
%% @doc handle_delete/1, handle_delete/2, handle_delete/3 delete
%% content of the file based on argument given by the caller.
%% @end
%%--------------------------------------------------------------------
-spec handle_delete(term()) -> {keep_state, term()}.
handle_delete(Data) ->
{keep_state, ?CALL:delete(Data)}.
-spec handle_delete(state(), non_neg_integer())
-> {keep_state, state()}.
handle_delete(Data, Position) ->
{keep_state, ?CALL:delete(Data, Position)}.
-spec handle_delete(state(), non_neg_integer(), non_neg_integer())
-> {keep_state, state()}.
handle_delete(Data, Start, End) ->
{keep_state, ?CALL:delete(Data, Start, End)}.
%%--------------------------------------------------------------------
%% @doc handle_append/2, handle_append/3 add data to data-structure
%% based on informations given by the caller.
%% @end
%%--------------------------------------------------------------------
-spec handle_append(state(), state())
-> {keep_state, state()}.
handle_append(Data, Text) ->
{keep_state, ?CALL:append(Data, Text)}.
-spec handle_append(state(), state(), non_neg_integer())
-> {keep_state, state()}.
handle_append(Data, Text, Position) ->
{keep_state, ?CALL:append(Data, Text, Position)}.
%%--------------------------------------------------------------------
%% @doc handle_info/2 currently do nothing (currently). Only here
%% to get external call from non-standard OTP application.
%% @end
%%--------------------------------------------------------------------
-spec handle_info(term(), state())
-> {keep_state, state()}.
handle_info(Event, Data) ->
io:format("global info: ~p~n", [Event]),
{keep_state, Data}.
%%====================================================================
%% external API
%%====================================================================
%%--------------------------------------------------------------------
%% @doc append/2, append/3 give access to append function to external
%% client/process.
%% @end
%%--------------------------------------------------------------------
append(Pid, Text) ->
gen_statem:cast(Pid, {append, Text}).
append(Pid, Text, Position) ->
gen_statem:cast(Pid, {append, Text, Position}).
%%--------------------------------------------------------------------
%% @doc delete/1, delete/2, delete/3 give access to delete function to
%% external client/process.
%% @end
%%--------------------------------------------------------------------
delete(Pid) ->
gen_statem:cast(Pid, delete).
delete(Pid, Position) ->
gen_statem:cast(Pid, {delete, Position}).
delete(Pid, Start, Stop) ->
gen_statem:cast(Pid, {delete, Start, Stop}).
%%--------------------------------------------------------------------
%% @doc read/1 give access to data-structure content from external
%% process/client.
%% @end
%%--------------------------------------------------------------------
read(Pid) ->
gen_statem:call(Pid, read, 1000).
%%--------------------------------------------------------------------
%% @doc state/2 allow to change state.
%% @end
%%--------------------------------------------------------------------
state(Pid, State) ->
gen_statem:cast(Pid, {state, State}).
%%====================================================================
%% Capability Section
%%====================================================================
capability() ->
[{readwrite, capability(readwrite)}
,{readonly, capability(readonly)}
,{writeonly, capability(writeonly)}
].
capability(readwrite) ->
[fun append/2
,fun append/3
,fun delete/1
,fun delete/2
,fun delete/3
% ,fun replace/2
% ,fun replace/3
,fun read/1
];
capability(readonly) ->
[fun read/1
];
capability(writeonly) ->
[fun append/2
,fun append/3
].
-module(editor_SUITE).
-compile(export_all).
-include_lib("common_test/include/ct.hrl").
groups() ->
[].
suite() ->
[].
all() ->
[testcase1, testcase2].
init_per_suite(Config) ->
{ok, Pid} = editor2:start_link(),
io:format("debug: ~p~n", [Pid]),
[{editor, Pid}].
end_per_suite(Config) ->
Pid = ?config(editor, Config),
editor2:stop(Pid),
ok.
init_per_group(Config) ->
[].
end_per_group(Config) ->
ok.
init_per_testcase(TestCase, Config) ->
{ok, Pid} = editor2:start_link(),
[{editor, Pid}].
end_per_testcase(TestCase, Config) ->
ok.
testcase1() ->
[].
testcase1(Config) ->
Pid = ?config(editor, Config),
editor2:append(Pid, <<"test">>),
case editor2:read(Pid) of
<<"test">> -> ok;
_Else -> ct:fail({error, bad_result, _Else})
end.
testcase2(Config) ->
Pid = ?config(editor, Config),
editor2:append(Pid, <<"test2">>),
editor2:append(Pid, <<"abcd">>, 0),
case editor2:read(Pid) of
<<"abcdtest2">> -> ok;
_Else -> ct:fail({error, bad_result, _Else})
end.
%%%===================================================================
%%%
%%%===================================================================
-module(editor_test).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
delete(<<"">>) ->
<<"">>;
delete(Bitstring) ->
Length = erlang:bit_size(Bitstring)-8,
<<Return:Length/bitstring, _:8>> = Bitstring,
Return.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
delete(<<"">>, 0) ->
<<"">>;
delete(Bitstring, 0) ->
<<_:8, Rest/bitstring>> = Bitstring,
Rest;
delete(Bitstring, Position)
when Position*8 >= bit_size(Bitstring) ->
Bitstring;
delete(Bitstring, Position)
when Position > 0 ->
Shift = Position*8,
<<Head:Shift/bitstring, _:8, Rest/bitstring>> = Bitstring,
<<Head/bitstring, Rest/bitstring>>.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
delete(<<"">>,0,0) ->
<<"">>;
delete(Bitstring, 0, End)
when End > 0 ->
Shift = End*8,
case Shift > erlang:bit_size(Bitstring) of
true -> <<"">>;
false ->
<<_:Shift/bitstring, Rest/bitstring>> = Bitstring,
Rest
end;
delete(Bitstring, Start, End)
when Start =< End ->
Length = erlang:bit_size(Bitstring),
ShiftStart = Start*8,
ShiftEnd = End*8,
case ShiftEnd > Length of
true ->
L = Length - ShiftStart,
<<Head:ShiftStart/bitstring, _:L/bitstring>> = Bitstring,
Head;
false ->
<<Head:ShiftStart/bitstring, _:ShiftEnd/bitstring, Tail/bitstring>> = Bitstring,
<<Head/bitstring, Tail/bitstring>>
end.
delete_0001_test() ->
?assertEqual(<<"">>, delete(<<"">>)).
delete_0002_test() ->
?assertEqual(<<"">>, delete(<<"">>, 0)).
delete_0003_test() ->
?assertEqual(<<"">>, delete(<<"">>, 0, 0)).
delete_0004_test() ->
?assertEqual(<<"abc">>, delete(<<"abcd">>)).
delete_0005_test() ->
?assertEqual(<<"bcd">>, delete(<<"abcd">>, 0)).
delete_0006_test() ->
?assertEqual(<<"ad">>, delete(<<"abcd">>, 1, 2)).
delete_0007_test() ->
?assertEqual(<<"d">>, delete(<<"abcd">>, 0, 3)).
delete_0008_test() ->
?assertEqual(<<"">>, delete(<<"abcd">>, 0, 10)).
delete_0009_test() ->
?assertEqual(<<"a">>, delete(<<"abcd">>, 1, 10)).
delete_0010_test() ->
?assertEqual(<<"acd">>, delete(<<"abcd">>, 1)).
delete_0011_test() ->
?assertEqual(<<"abd">>, delete(<<"abcd">>, 2)).
delete_0012_test() ->
?assertEqual(<<"abc">>, delete(<<"abcd">>, 3)).
delete_0013_test() ->
?assertEqual(<<"abcd">>, delete(<<"abcd">>, 4)).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
append(Bitstring, Text) ->
<<Bitstring/bitstring, Text/bitstring>>.
append(Bitstring, Text, Position)
when Position*8 >= bit_size(Bitstring) ->
<<Bitstring/bitstring, Text/bitstring>>;
append(Bitstring, Text, Position) ->
Shift = Position*8,
<<Head:Shift/bitstring, Tail/bitstring>> = Bitstring,
<<Head/bitstring, Text/bitstring, Tail/bitstring>>.
append_0001_test() ->
?assertEqual(<<"abcde">>, append(<<"abcd">>, <<"e">>)).
append_0002_test() ->
?assertEqual(<<"abcd">>, append(<<"bcd">>, <<"a">>, 0)).
append_0003_test() ->
?assertEqual(<<"abcde">>, append(<<"abcd">>, <<"e">>, 4)).
append_0004_test() ->
?assertEqual(<<"abcde">>, append(<<"abcd">>, <<"e">>, 5)).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
read(Bitstring) ->
Bitstring.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment