- add encoding with test unit

- add readme and license
- add decoding feature with test unit
- add rebar files
This commit is contained in:
niamtokik
2021-03-19 17:03:33 +00:00
parent cee49dd68d
commit 467db66751
7 changed files with 524 additions and 39 deletions

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
.rebar3
_*
.eunit
*.o
*.beam
*.plt
*.swp
*.swo
.erlang.cookie
ebin
log
erl_crash.dump
.rebar
logs
_build
.idea
rebar3.crashdump
**~

14
LICENSE Normal file
View File

@@ -0,0 +1,14 @@
Copyright (c) 2021 Mathieu Kerjouan <contact [at] steepath [dot] eu>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

74
README.md Normal file
View File

@@ -0,0 +1,74 @@
# redis
`redis` is an application and library implementing redis protocol in
Erlang.
## Usage
```sh
rebar3 shell
```
### Serializer
Encoding Erlang terms in Redis data format using `redis:encode/1`
function:
```erlang
% integer
redis:encode(1).
% simple string
redis:encode(<<"test">>).
% bulk string
redis:encode({bulk_string, <<"test">>).
% array
redis:encode([1,2,3,<<"test">>, {bulk_string, <<"test">>}]).
% error
redis:encode({error, <<"my message">>}).
```
Decoding Redis data in Erlang term with `redis:decode/1` function:
```erlang
% simple string
redis:decode(<<"+OK\r\n">>).
% integer
redis:decode(<<":1\r\n">>).
% bulk string
redis:decode(<<"$3\r\nfoo\r\n">>).
% array
redis:decode(<<"*0\r\n\r\n">>).
% error
redis:decode(<<"-Message\r\n">>).
```
### Client
wip.
### Server
wip.
## Test
```sh
rebar3 eunit
```
# Resources and References
* https://redis.io/topics/protocol
# About
Made with <3 by Mathieu Kerjouan with [Erlang](erlang.org/) and
[rebar3](https://www.rebar3.org).

7
rebar.config Normal file
View File

@@ -0,0 +1,7 @@
{erl_opts, [debug_info]}.
{deps, []}.
{shell, [
% {config, "config/sys.config"},
{apps, [redis]}
]}.

1
rebar.lock Normal file
View File

@@ -0,0 +1 @@
[].

View File

@@ -1,45 +1,275 @@
%%%-------------------------------------------------------------------
%%% @doc
%%% @end
%%%-------------------------------------------------------------------
-module(redis).
-compile(export_all).
-export([encode/1, decode/1]).
-include_lib("eunit/include/eunit.hrl").
-type resp_type() :: simple_string | error | integer | bulk_string | array.
-type resp_simple_string() :: {simple_string, bitstring()}.
-type resp_error() :: {error, bitstring()}.
-type resp_integer() :: {integer, integer()}.
-type resp_bulk_string() :: {bulk_string, bitstring()}.
-type resp_types() :: resp_simple_string() |
resp_integer() |
resp_bulk_string().
-type encode_error() :: {error, bitstring() | binary()}.
-type encode_integer() :: integer().
-type encode_string() :: bitstring() | binary().
-type encode_bulk_string() :: {bulk_string, bitstring()}.
-type encode_array() :: [encode_integer() |
encode_string() |
encode_bulk_string()].
-type encode_types() :: encode_integer() |
encode_string() |
encode_bulk_string() |
encode_array().
-type encode_return_ok() :: bitstring() | binary().
-type encode_return_error() :: {error, atom()}.
-type resp_array() :: {array, [resp_types()]}.
-spec resp(Rest) -> Return when
Rest :: resp(),
Return :: bitstring().
resp(simple_string) -> <<"+">>;
resp(error) -> <<"-">>;
resp(integer) -> <<":">>;
resp(bulk_string) -> <<"$">>;
resp(array) -> <<"*">>.
ok() ->
<<"+OK\r\n">>.
error(Message) ->
<<"-ERR ", Message, "\r\n">>.
wrongtype(Message) ->
<<"-WRONGTYPE ", Message, "\r\n">>.
string(String) ->
Length = erlang:size(String),
LengthStr = erlang:integer_to_binary(Length),
case Length of
0 -> <<"$0\r\n\r\n">>;
_ -> <<"$", LengthStr, "\r\n", String, "\r\n">>
%%-------------------------------------------------------------------
%% @doc
%% @end
%%-------------------------------------------------------------------
-spec encode(Data) -> Return when
Data :: encode_types(),
Return :: encode_return_ok() | encode_return_error().
encode(Integer)
when is_integer(Integer) ->
Value = erlang:integer_to_binary(Integer),
<<":", Value/bitstring, "\r\n">>;
encode(Bitstring)
when is_bitstring(Bitstring) ->
case Return = encode_valid_char(Bitstring) of
{ok, Valid} -> <<"+", Valid/bitstring, "\r\n">>;
Return -> Return
end;
encode({bulk_string, null}) ->
<<"$-1\r\n">>;
encode({bulk_string, Bitstring})
when is_bitstring(Bitstring) ->
Length = erlang:integer_to_binary(erlang:size(Bitstring)),
<<"$", Length/bitstring, "\r\n", Bitstring/bitstring, "\r\n">>;
encode([]) ->
<<"*0\r\n">>;
encode(Array)
when is_list(Array) ->
{ok, Elements, Counter} = encode_array(Array, <<>>, 0),
C = erlang:integer_to_binary(Counter),
<<"*", C/bitstring, "\r\n", Elements/bitstring>>;
encode({error, Bitstring})
when is_bitstring(Bitstring) ->
case Return = encode_valid_char(Bitstring) of
{ok, Valid} -> <<"-", Valid/bitstring, "\r\n">>;
Return -> Return
end.
encode_test() ->
[{"encode bitstring", [?assertEqual(<<"+OK\r\n">>
,encode(<<"OK">>))
,?assertEqual({error, badchar}
,encode(<<"OK\r">>))
,?assertEqual({error, badchar}
,encode(<<"OK\n">>))
,?assertEqual({error, badchar}
,encode(<<"OK\r\n">>))
]}
,{"encode integer", [?assertEqual(<<":0\r\n">>
,encode(0))
,?assertEqual(<<":10000\r\n">>
,encode(10000))
]}
,{"encode bulk string", [?assertEqual(<<"$-1\r\n">>
,encode({bulk_string, null}))
,?assertEqual(<<"$0\r\n\r\n">>
,encode({bulk_string, <<>>}))
,?assertEqual(<<"$6\r\nfoobar\r\n">>
,encode({bulk_string, <<"foobar">>}))
]}
,{"encode array", [?assertEqual(<<"*0\r\n">>, encode([]))
,?assertEqual(<<"*3\r\n:1\r\n:2\r\n:3\r\n">>
,encode([1,2,3]))
,?assertEqual(<<"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n">>
,encode([{bulk_string, <<"foo">>}
,{bulk_string, <<"bar">>}]))
,?assertEqual(<<"*5\r\n:1\r\n:2\r\n:3\r\n:4\r\n$6\r\nfoobar\r\n">>
,encode([1,2,3,4,{bulk_string, <<"foobar">>}]))
,?assertEqual(<<"*1\r\n*0\r\n">>
,encode([[]]))
,?assertEqual(<<"*1\r\n*3\r\n:1\r\n:2\r\n:3\r\n">>
,encode([[1,2,3]]))
]}
,{"encode error", [?assertEqual(<<"-Error message\r\n">>
,encode({error, <<"Error message">>}))
,?assertEqual(<<"-ERR unknown command 'foobar'\r\n">>
,encode({error, <<"ERR unknown command 'foobar'">>}))
,?assertEqual(<<"-WRONGTYPE Operation against a key holding the wrong kind of value\r\n">>
,encode({error, <<"WRONGTYPE Operation against a key holding the wrong kind of value">>}))
]}
].
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec encode_valid_char(Bitstring) -> Return when
Bitstring :: bitstring() | binary(),
Return :: {ok, bitstring() | binary()} |
{error, badchar}.
encode_valid_char(Bitstring)
when is_bitstring(Bitstring) ->
try
nomatch = binary:match(Bitstring, <<"\r">>),
nomatch = binary:match(Bitstring, <<"\n">>)
of
_ -> {ok, Bitstring}
catch
error:_ -> {error, badchar}
end.
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec encode_array(List, Buffer, Counter) -> Return when
List :: list(),
Buffer :: bitstring() | binary(),
Counter :: integer(),
Return :: bitstring() | binary().
encode_array([], Buffer, Counter) ->
{ok, Buffer, Counter};
encode_array([H|T], Buffer, Counter) ->
Data = encode(H),
Return = <<Buffer/bitstring, Data/bitstring>>,
encode_array(T, Return, Counter+1).
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
decode(<<"+", Rest/bitstring>>) ->
decode_string(Rest);
decode(<<":", Rest/bitstring>>) ->
decode_integer(Rest);
decode(<<"$", Rest/bitstring>>) ->
decode_bulk_string(Rest);
decode(<<"*", Rest/bitstring>>) ->
decode_array(Rest);
decode(<<"-", Rest/bitstring>>) ->
decode_error(Rest).
decode_test() ->
[{"decode integer", [?assertEqual({ok, 0}
,decode(<<":0\r\n">>))
,?assertEqual({ok, 1}
,decode(<<":1\r\n">>))
,?assertEqual({ok, 999999}
,decode(<<":999999\r\n">>))
]}
,{"decode simple string", [?assertEqual({ok, <<>>}
,decode(<<"+\r\n">>))
,?assertEqual({ok, <<"test">>}
,decode(<<"+test\r\n">>))
]}
,{"decode bulk string", [?assertEqual({ok, <<"test">>}
,decode(<<"$4\r\ntest\r\n">>))
]}
,{"decode array", [?assertEqual({ok, []}
,decode(<<"*0\r\n">>))
,?assertEqual({ok, [1,2,3]}
,decode(<<"*3\r\n:1\r\n:2\r\n:3\r\n">>))
]}
,{"decode error", [?assertEqual({ok, {error, <<"ERR">>}}
,decode(<<"-ERR\r\n">>))
]}
].
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
decode_string(Bitstring) ->
decode_string(Bitstring, <<>>).
decode_string(<<"\r\n">>, Buffer) ->
{ok, Buffer};
decode_string(<<"\r\n", Rest/bitstring>>, Buffer) ->
{ok, Buffer, Rest};
decode_string(<<"\r", _/bitstring>>, _) ->
{error, badchar};
decode_string(<<"\n", _/bitstring>>, _) ->
{error, badchar};
decode_string(<<Char, Rest/bitstring>>, Buffer) ->
decode_string(Rest, <<Buffer/bitstring, Char>>).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
decode_integer(Integer) ->
decode_integer(Integer, <<>>).
decode_integer(<<"\r\n">>, Buffer) ->
{ok, erlang:binary_to_integer(Buffer)};
decode_integer(<<"\r\n", Rest/bitstring>>, Buffer) ->
{ok, erlang:binary_to_integer(Buffer), Rest};
decode_integer(<<Char, Rest/bitstring>>, Buffer)
when Char >= $0 andalso Char =< $9 ->
decode_integer(Rest, <<Buffer/bitstring, Char>>).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
decode_separator(Bitstring) ->
case binary:split(Bitstring, <<"\r\n">>) of
[<<>>] -> {ok, <<>>};
[<<>>,<<>>] -> {ok, <<>>};
[Value,Rest] -> {ok, Value, Rest}
end.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
decode_bulk_string(<<"-1\r\n">>) ->
{ok, nil};
decode_bulk_string(<<"-1\r\n", Rest/bitstring>>) ->
{ok, nil, Rest};
decode_bulk_string(Bitstring) ->
{ok, Size, Rest} = decode_integer(Bitstring),
Length = Size*8,
<<String:Length/bitstring, "\r\n", R/bitstring>> = Rest,
case R of
<<>> -> {ok, String};
_ -> {ok, String, R}
end.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
decode_array(<<"0\r\n">>) ->
{ok, []};
decode_array(<<"0\r\n", Rest/bitstring>>) ->
{ok, [], Rest};
decode_array(Bitstring) ->
{ok, Size, Rest} = decode_integer(Bitstring),
decode_array(Rest, Size, []).
decode_array(<<>>, 0, Buffer) ->
{ok, lists:reverse(Buffer)};
decode_array(Rest, 0, Buffer) ->
{ok, lists:reverse(Buffer), Rest};
decode_array(Bitstring, Size, Buffer) ->
case decode(Bitstring) of
{ok, Result} ->
decode_array(<<>>, Size-1, [Result|Buffer]);
{ok, Result, Rest} ->
decode_array(Rest, Size-1, [Result|Buffer])
end.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
decode_error(Bitstring) ->
decode_error(Bitstring, <<>>).
decode_error(<<"\r\n">>, Buffer) ->
{ok, {error, Buffer}};
decode_error(<<"\r", Rest/bitstring>>, _) ->
{error, badchar};
decode_error(<<"\n", Rest/bitstring>>, _) ->
{error, badchar};
decode_error(<<Char, Rest/bitstring>>, Buffer) ->
decode_error(Rest, <<Buffer/bitstring, Char>>).

141
test/redis_SUITE.erl Normal file
View File

@@ -0,0 +1,141 @@
%%%-------------------------------------------------------------------
%%% @author Mathieu Kerjouan <contact [at] steepath [dot] eu>
%%% @copyright 2021 (c) Mathieu Kerjouan
%%%
%%% @doc
%%% @end
%%%-------------------------------------------------------------------
-module(redis_SUITE).
-compile(export_all).
-include_lib("common_test/include/ct.hrl").
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec suite() -> Return when
Return :: [tuple()].
suite() ->
[{timetrap,{seconds,30}}].
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec init_per_suite(Config) -> Return when
Config :: [tuple()],
Reason :: term(),
Return :: Config | {skip,Reason} | {skip_and_save,Reason,Config}.
init_per_suite(Config) ->
Config.
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec end_per_suite(Config) -> Return when
Config :: [tuple()],
Return :: term() | {save_config,Config}.
end_per_suite(_Config) ->
ok.
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec init_per_group(GroupName, Config) -> Return when
GroupName :: atom(),
Config :: [tuple()],
Reason :: term(),
Return :: Config | {skip,Reason} | {skip_and_save,Reason,Config}.
init_per_group(_GroupName, Config) ->
Config.
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec end_per_group(GroupName, Config) -> Return when
GroupName :: atom(),
Config :: [tuple()],
Return :: term() | {save_config,Config}.
end_per_group(_GroupName, _Config) ->
ok.
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec init_per_testcase(TestCase, Config) -> Return when
TestCase :: atom(),
Config :: [tuple()],
Reason :: term(),
Return :: Config | {skip,Reason} | {skip_and_save,Reason,Config}.
init_per_testcase(_TestCase, Config) ->
Config.
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec end_per_testcase(TestCase, Config) -> Return when
TestCase :: atom(),
Config :: [tuple()],
Reason :: term(),
Return :: term() | {save_config,Config} | {fail,Reason}.
end_per_testcase(_TestCase, _Config) ->
ok.
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec groups() -> Return when
Group :: {GroupName,Properties,GroupsAndTestCases},
GroupName :: atom(),
Properties :: [parallel | sequence | Shuffle | {RepeatType,N}],
GroupsAndTestCases :: [Group | {group,GroupName} | TestCase],
TestCase :: atom(),
Shuffle :: shuffle | {shuffle,{integer(),integer(),integer()}},
RepeatType :: repeat | repeat_until_all_ok | repeat_until_all_fail |
repeat_until_any_ok | repeat_until_any_fail,
N :: integer() | forever,
Return :: [Group].
groups() ->
[].
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec all() -> Return when
GroupsAndTestCases :: [{group,GroupName} | TestCase],
GroupName :: atom(),
TestCase :: atom(),
Reason :: term(),
Return :: GroupsAndTestCases | {skip,Reason}.
all() ->
[my_test_case].
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec my_test_case() -> Return when
Return :: [tuple()].
my_test_case() ->
[].
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec my_test_case(Config) -> Return when
Config :: [tuple()],
Reason :: term(),
Comment :: term(),
Return :: ok | erlang:exit() | {skip,Reason} | {comment,Comment} |
{save_config,Config} | {skip_and_save,Reason,Config}.
my_test_case(_Config) ->
ok.