From 467db66751c9e25b6e758703a92fd5d121aa3de6 Mon Sep 17 00:00:00 2001 From: niamtokik Date: Fri, 19 Mar 2021 17:03:33 +0000 Subject: [PATCH] - add encoding with test unit - add readme and license - add decoding feature with test unit - add rebar files --- .gitignore | 18 +++ LICENSE | 14 ++ README.md | 74 +++++++++++ rebar.config | 7 + rebar.lock | 1 + src/redis.erl | 308 +++++++++++++++++++++++++++++++++++++------ test/redis_SUITE.erl | 141 ++++++++++++++++++++ 7 files changed, 524 insertions(+), 39 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 rebar.config create mode 100644 rebar.lock create mode 100644 test/redis_SUITE.erl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d17546 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.rebar3 +_* +.eunit +*.o +*.beam +*.plt +*.swp +*.swo +.erlang.cookie +ebin +log +erl_crash.dump +.rebar +logs +_build +.idea +rebar3.crashdump +**~ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b7b39f2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2021 Mathieu Kerjouan + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4785c1b --- /dev/null +++ b/README.md @@ -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). diff --git a/rebar.config b/rebar.config new file mode 100644 index 0000000..e7cc8cf --- /dev/null +++ b/rebar.config @@ -0,0 +1,7 @@ +{erl_opts, [debug_info]}. +{deps, []}. + +{shell, [ + % {config, "config/sys.config"}, + {apps, [redis]} +]}. diff --git a/rebar.lock b/rebar.lock new file mode 100644 index 0000000..57afcca --- /dev/null +++ b/rebar.lock @@ -0,0 +1 @@ +[]. diff --git a/src/redis.erl b/src/redis.erl index 45f457f..1d59948 100644 --- a/src/redis.erl +++ b/src/redis.erl @@ -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 resp_array() :: {array, [resp_types()]}. +-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()}. - --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 = <>, + 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(<>, Buffer) -> + decode_string(Rest, <>). + +%%-------------------------------------------------------------------- +%% +%%-------------------------------------------------------------------- +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(<>, Buffer) + when Char >= $0 andalso Char =< $9 -> + decode_integer(Rest, <>). + +%%-------------------------------------------------------------------- +%% +%%-------------------------------------------------------------------- +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, + <> = 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(<>, Buffer) -> + decode_error(Rest, <>). + + diff --git a/test/redis_SUITE.erl b/test/redis_SUITE.erl new file mode 100644 index 0000000..a4d14ab --- /dev/null +++ b/test/redis_SUITE.erl @@ -0,0 +1,141 @@ +%%%------------------------------------------------------------------- +%%% @author Mathieu Kerjouan +%%% @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. +