Last active
February 14, 2018 19:01
-
-
Save niamtokik/9b9ea703f797c70a5eeba4e9f3801984 to your computer and use it in GitHub Desktop.
Simple File Representation In Erlang/OTP with gen_statem Behavior
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| %%%=================================================================== | |
| %%% @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 | |
| ]. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| %%%=================================================================== | |
| %%% | |
| %%%=================================================================== | |
| -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