fluxer/fluxer_gateway/src/gateway/gateway_codec.erl
2026-02-17 12:22:36 +00:00

121 lines
3.5 KiB
Erlang

%% Copyright (C) 2026 Fluxer Contributors
%%
%% This file is part of Fluxer.
%%
%% Fluxer is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as published by
%% the Free Software Foundation, either version 3 of the License, or
%% (at your option) any later version.
%%
%% Fluxer is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
-module(gateway_codec).
-export([
encode/2,
decode/2,
parse_encoding/1
]).
-type encoding() :: json.
-type frame_type() :: text | binary.
-export_type([encoding/0]).
-spec parse_encoding(binary() | undefined) -> encoding().
parse_encoding(_) ->
json.
-spec encode(map(), encoding()) -> {ok, iodata(), frame_type()} | {error, term()}.
encode(Message, json) ->
try
Encoded = iolist_to_binary(json:encode(Message)),
{ok, Encoded, text}
catch
_:Reason ->
{error, {encode_failed, Reason}}
end.
-spec decode(binary(), encoding()) -> {ok, map()} | {error, term()}.
decode(Data, json) ->
try
Decoded = json:decode(Data),
{ok, Decoded}
catch
_:Reason ->
{error, {decode_failed, Reason}}
end.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
parse_encoding_test_() ->
[
?_assertEqual(json, parse_encoding(<<"json">>)),
?_assertEqual(json, parse_encoding(<<"etf">>)),
?_assertEqual(json, parse_encoding(undefined)),
?_assertEqual(json, parse_encoding(<<"invalid">>)),
?_assertEqual(json, parse_encoding(<<>>))
].
encode_json_test_() ->
Message = #{<<"op">> => 0, <<"d">> => #{<<"test">> => true}},
[
?_assertMatch({ok, _, text}, encode(Message, json)),
?_test(begin
{ok, Encoded, text} = encode(Message, json),
?assert(is_binary(Encoded))
end)
].
encode_empty_map_test() ->
{ok, Encoded, text} = encode(#{}, json),
?assertEqual(<<"{}">>, Encoded).
encode_nested_test() ->
Message = #{<<"a">> => #{<<"b">> => #{<<"c">> => 1}}},
{ok, Encoded, text} = encode(Message, json),
?assert(is_binary(Encoded)),
{ok, Decoded} = decode(Encoded, json),
?assertEqual(Message, Decoded).
decode_json_test_() ->
Data = <<"{\"op\":0,\"d\":{\"test\":true}}">>,
[
?_assertMatch({ok, _}, decode(Data, json)),
?_test(begin
{ok, Decoded} = decode(Data, json),
?assertEqual(0, maps:get(<<"op">>, Decoded))
end)
].
decode_invalid_json_test() ->
?assertMatch({error, {decode_failed, _}}, decode(<<"not json">>, json)).
decode_empty_object_test() ->
{ok, Decoded} = decode(<<"{}">>, json),
?assertEqual(#{}, Decoded).
roundtrip_json_test_() ->
Messages = [
#{<<"op">> => 10, <<"d">> => #{<<"heartbeat_interval">> => 41250}},
#{<<"op">> => 0, <<"s">> => 1, <<"t">> => <<"READY">>, <<"d">> => #{}},
#{<<"list">> => [1, 2, 3], <<"bool">> => true, <<"null">> => null}
],
[
?_test(begin
{ok, Encoded, _} = encode(Msg, json),
{ok, Decoded} = decode(iolist_to_binary(Encoded), json),
?assertEqual(Msg, Decoded)
end)
|| Msg <- Messages
].
-endif.