Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Changing the API to socketio_data and fixing the streaming + parsing

Introducing a dependency on proper for property-based tests for
the protocol stuff.
  • Loading branch information...
commit d75a3cac73c78653cf0f020b6918bf232c444561 1 parent 4eb2fa4
@ferd ferd authored yrashk committed
View
1  rebar.config
@@ -1,2 +1,3 @@
{rebar_plugins, [agner_rebar_plugin]}.
{lib_dirs, ["deps"]}.
+{erl_opts, [debug_info, {i,"deps/proper/include/"}]}.
View
2  src/socketio.app.src
@@ -3,7 +3,7 @@
{description, ""},
{vsn, "1"},
{registered, []},
- {agner, [{requires, ["misultin","ossp_uuid","jsx"]}]},
+ {agner, [{requires, ["misultin","ossp_uuid","jsx","proper"]}]},
{applications, [
kernel,
stdlib
View
158 src/socketio_data.erl
@@ -1,6 +1,8 @@
-module(socketio_data).
-include_lib("socketio.hrl").
--export([encode/1, parse/2, string_reader/2]).
+%-export([encode/1, decode/1]).
+-export([encode/1, decode/1]).
+-export([parse/2, string_reader/2]). %% Old protocol
-record(parser,
{
@@ -15,13 +17,19 @@
buf = []
}).
+%%%%%%%%%%%%%%%%%%%%
+%%% The Sockets.js server-side protocol as processed by https://github.com/LearnBoost/Socket.IO-node/blob/master/lib/socket.io/utils.js
+%%%%%%%%%%%%%%%%%%%%
+%%
+%% Frame: ~m~
+%% Message: some string
+%% JSON Message: ~j~ ++ String Version of JSON object
+%% Heartbeat: ~h~ ++ Index
+%% Result: Frame ++ Length of Message ++ Frame ++ Message
+-define(FRAME, "~m~").
+-define(JSON_FRAME, "~j~").
+-define(HEARTBEAT_FRAME, "~h~").
-%% Not sure if session messages are any different. All i know is they're the first message
-%% that arrives from the client.
-%% -record(session,
-%% {
-%% id
-%% }).
encode(#msg{ content = Content, json = false }) when is_list(Content) ->
Length = integer_to_list(length(Content)),
@@ -37,6 +45,103 @@ encode(#heartbeat{ index = Index }) ->
Length = integer_to_list(length(String) + 3),
"~m~" ++ Length ++ "~m~~h~" ++ String.
+decode(#msg{content=Str}) when is_list(Str) ->
+ header(Str).
+
+header(?FRAME ++ Rest) ->
+ header(Rest, []);
+header(L = [$~|_]) ->
+ {incomplete, fun(S) -> header(L++S) end}.
+
+header(?FRAME ++ Rest=[_|_], Acc)->
+ Length = list_to_integer(lists:reverse(Acc)),
+ body(Length, Rest);
+header([N|Rest], Acc) when N >= $0, N =< $9 ->
+ header(Rest, [N|Acc]);
+header(L, Acc) when L =:= []; L =:= "~"; L =:= "~m" -> %% Breaking them macros
+ {incomplete, fun(S) -> header(L ++ S, Acc) end}.
+
+body(Length, ?JSON_FRAME++Body) ->
+ json(Length-3, Body);
+body(Length, ?HEARTBEAT_FRAME++Body) ->
+ heartbeat(Length-3, Body, []);
+%% The 3 following clause avoid matching JSON or Heartbeats as messages
+%% In the case of streaming.
+body(Length, []) when Length > 3 ->
+ {incomplete, fun(S) -> body(Length, S) end};
+body(Length, L=[_]) when Length > 3 ->
+ {incomplete, fun(S) -> body(Length, L++S) end};
+body(Length, L=[_,_]) when Length > 3 ->
+ {incomplete, fun(S) -> body(Length, L++S) end};
+body(Length, Body) when length(Body) >= Length ->
+ #msg{content=lists:sublist(Body, Length)};
+body(Length, Body) -> %% Return a continuation
+ LengthRemaining = Length - length(Body),
+ {incomplete,
+ fun(Input) -> body_stream(LengthRemaining, Input, Body) end}.
+
+body_stream(Length, Input, PartialBody) ->
+ NewPartial = lists:sublist(Input, Length),
+ Body = [PartialBody, NewPartial],
+ case Length - length(NewPartial) of
+ 0 -> #msg{content=lists:flatten(Body)};
+ N when is_integer(N), N > 0 ->
+ {incomplete,
+ fun(NewInput) -> body_stream(N, NewInput, Body) end}
+ end.
+
+json(Length, Body) when length(Body) >= Length ->
+ Object = lists:sublist(Body, Length),
+ #msg{content=jsx:json_to_term(list_to_binary(Object)), json=true};
+%% This stops caring about the content lenght. Maybe we should care about it,
+%% by mixing in the protocol and json stuff
+json(_Length, Body) ->
+ wrap(jsx:json_to_term(list_to_binary(Body), [{stream,true}])).
+
+wrap({incomplete, F}) ->
+ {incomplete, fun(Txt) -> wrap(F(list_to_binary(Txt))) end};
+wrap(X) ->
+ #msg{content=X, json=true}.
+
+heartbeat(0, _, Acc) -> #heartbeat{index=list_to_integer(lists:reverse(Acc))};
+heartbeat(Length, [], Acc) ->
+ {incomplete, fun(Str) -> heartbeat(Length, Str, Acc) end};
+heartbeat(Length, [N|Rest], Acc) when N >= $0, N =< $9 ->
+ heartbeat(Length-1, Rest, [N|Acc]).
+
+
+%% TESTS
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+%% For more reliable tests, see the proper module in tests/prop_transport.erl
+
+simple_msg_test() ->
+ X = decode(#msg{content="~m~11~m~Hello world"}),
+ ?assertMatch("Hello world", X#msg.content).
+
+complex_msg_test() ->
+ X = decode(#msg{content="~m~11~m~Hello~world"}),
+ ?assertMatch("Hello~world", X#msg.content).
+
+simple_heartbeat_test() ->
+ X = decode(#msg{content="~m~4~m~~h~0"}),
+ ?assertMatch(0, X#heartbeat.index).
+
+simple_json_test() ->
+ X = decode(#msg{content="~m~20~m~~j~{\"hello\":\"world\"}"}),
+ ?assertMatch(#msg{content=[{<<"hello">>,<<"world">>}], json=true}, X).
+
+json_encoding_test() ->
+ JSON = [{<<"hello">>,<<"world">>}],
+ Msg = #msg{content = JSON, json=true},
+ Data = encode(Msg),
+ X = decode(#msg{content=Data}),
+ ?assertMatch(#msg{content=JSON, json=true}, X).
+
+-endif.
+
+%%% OLD API
+
parse(Reader, Fun) when is_function(Reader), is_function(Fun) ->
parse(#parser{ reader_fun = Reader, f = Fun }).
@@ -93,42 +198,3 @@ string_reader(#parser{ reader = undefined } = Parser, String) ->
string_reader(#parser{ reader = String } = Parser, String) ->
{eof, Parser}.
-
-%% TESTS
--include_lib("eunit/include/eunit.hrl").
--ifdef(TEST).
-simple_msg_test() ->
- parse(fun (Parser) -> string_reader(Parser, "~m~11~m~Hello world") end, fun (X) -> self() ! X end),
- receive X ->
- ?assertMatch(#msg{ content = "Hello world", length = 11}, X)
- end.
-
-complex_msg_test() ->
- parse(fun (Parser) -> string_reader(Parser, "~m~11~m~Hello~world") end, fun (X) -> self() ! X end),
- receive X ->
- ?assertMatch(#msg{ content = "Hello~world", length = 11}, X)
- end.
-
-simple_heartbeat_test() ->
- parse(fun (Parser) -> string_reader(Parser, "~m~1~m~h~0") end, fun (X) -> self() ! X end),
- receive X ->
- ?assertMatch(#heartbeat{ index = 0 }, X)
- end.
-
-simple_json_test() ->
- parse(fun (Parser) -> string_reader(Parser, "~m~20~m~~j~{\"hello\":\"world\"}") end, fun (X) -> self() ! X end),
- receive X ->
- ?assertMatch(#msg{ content = [{<<"hello">>,<<"world">>}], json = true, length = _ }, X)
- end.
-
-json_encoding_test() ->
- JSON = [{<<"hello">>,<<"world">>}],
- Msg = #msg{ content = JSON, json = true, length = 17 },
- Data = encode(Msg),
- parse(fun (Parser) -> string_reader(Parser, Data) end, fun (X) -> self() ! X end),
- receive X ->
- ?assertMatch(#msg{ content = JSON, json = true, length = _ }, X)
- end.
-
-
--endif.
View
139 test/prop_transport.erl
@@ -0,0 +1,139 @@
+-module(prop_transport).
+-include("../include/socketio.hrl").
+-include_lib("proper/include/proper.hrl").
+
+prop_sanity_msg() ->
+ ?FORALL(Str, gen_string(),
+ begin
+ S = eval(Str),
+ String = #msg{content=S},
+ Encoded = socketio_data:encode(String),
+ #msg{content=Parsed} = socketio_data:decode(#msg{content=Encoded}),
+ ?WHENFAIL(
+ io:format("~p =/= ~p~n", [S,Parsed]),
+ S =:= Parsed
+ )
+ end).
+
+prop_stream_parse_msg() ->
+ ?FORALL({Str, N}, gen_stream(gen_encoded_msg()),
+ begin
+ {Start, End} = lists:split(N, Str),
+ #msg{content = Decoded} = socketio_data:decode(#msg{content=Str}),
+ {incomplete, F} = socketio_data:decode(#msg{content=Start}),
+ #msg{content = StreamDecoded} = F(End),
+ equals(Decoded, StreamDecoded)
+ end).
+
+prop_sanity_heartbeat() ->
+ ?FORALL(N, heartbeat(),
+ begin
+ Hb = #heartbeat{index=N},
+ Encoded = socketio_data:encode(Hb),
+ #heartbeat{index=Parsed} = socketio_data:decode(#msg{content=Encoded}),
+ ?WHENFAIL(
+ io:format("~p =/= ~p~n", [N, Parsed]),
+ N =:= Parsed
+ )
+ end).
+
+prop_stream_parse_heartbeat() ->
+ ?FORALL({Str, N}, gen_stream(gen_encoded_heartbeat()),
+ begin
+ {Start, End} = lists:split(N, Str),
+ #heartbeat{index = Decoded} = socketio_data:decode(#msg{content=Str}),
+ {incomplete, F} = socketio_data:decode(#msg{content=Start}),
+ #heartbeat{index = StreamDecoded} = F(End),
+ equals(Decoded, StreamDecoded)
+ end).
+
+prop_sanity_json() ->
+ ?FORALL(Term, json(),
+ begin
+ J = #msg{content=Term, json=true},
+ Encoded = socketio_data:encode(J),
+ #msg{content=Parsed, json=true} = socketio_data:decode(#msg{content=Encoded}),
+ ?WHENFAIL(
+ io:format("~p =/= ~p~n", [Term, Parsed]),
+ Term =:= Parsed
+ )
+ end).
+
+prop_stream_parse_json() ->
+ ?FORALL({Str, N}, gen_stream(gen_encoded_json()),
+ begin
+ {Start, End} = lists:split(N, Str),
+ #msg{content=Decoded, json=true} = socketio_data:decode(#msg{content=Str}),
+ {incomplete, F} = socketio_data:decode(#msg{content=Start}),
+ #msg{content=StreamDecoded, json=true} = F(End),
+ equals(Decoded, StreamDecoded)
+ end).
+
+%%% GENERATORS
+%% generates strings with a more certain presence of escape characters
+gen_stream(T) ->
+ ?LET(Str, T,
+ begin
+ L = length(Str),
+ {Str, choose(1,L-1)}
+ end).
+
+gen_encoded_msg() ->
+ ?LET(Str, gen_string(),
+ begin
+ S = eval(Str),
+ String = #msg{content=S},
+ socketio_data:encode(String)
+ end).
+
+gen_encoded_heartbeat() ->
+ ?LET(N, heartbeat(),
+ begin
+ Hb = #heartbeat{index=N},
+ socketio_data:encode(Hb)
+ end).
+
+gen_encoded_json() ->
+ ?LET(Term, json(),
+ begin
+ J = #msg{content=Term, json=true},
+ socketio_data:encode(J)
+ end).
+
+gen_string() ->
+ ?LAZY(weighted_union([
+ {1, []},
+ {1, [$~|string()]},
+ {10, [char()|gen_string()]}
+ ])).
+
+heartbeat() -> ?LET(N, int(), abs(N)).
+
+json() ->
+ ?LET(Compiled,
+ ?LET(X, [json_data(),end_json], lists:flatten(X)),
+ jsx:json_to_term(jsx:format(jsx:eventify(Compiled)))).
+
+json_data() ->
+ ?LAZY(weighted_union([
+ {1,?LET(X, [start_array, json_data(), end_array], lists:flatten(X))},
+ {1,?LET(X, [start_object,json_object(),end_object], lists:flatten(X))}
+ ])).
+
+json_object() ->
+ ?LAZY(union([[], [key(), val()]])).
+
+key() -> {key, ascii()}.
+val() ->
+ union([
+ {literal,true},
+ {literal,false},
+ {literal,null},
+ {integer,?LET(N, int(), integer_to_list(N))},
+ {string, ascii()},
+ {float,
+ ?LET({A,B}, {int(),nat()}, integer_to_list(A)++[$.]++integer_to_list(B))}
+ ]).
+
+ascii() ->
+ list(choose($a,$z)).
Please sign in to comment.
Something went wrong with that request. Please try again.