Skip to content

Commit

Permalink
Merge pull request #6 from yowcow/record-at-mw
Browse files Browse the repository at this point in the history
Enqueue/dequeue improvement
  • Loading branch information
Yoko OYAMA authored Jul 4, 2020
2 parents 9fbb540 + f35bc37 commit f5d7176
Show file tree
Hide file tree
Showing 9 changed files with 285 additions and 136 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ otp_release:
- "22.0"
- "22.1"
- "22.2"
- "22.3"
- "23.0"

script:
- make all test
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,28 @@ end_per_suite(_) ->

(See src/stubby.erl for more options starting up a server)

In a testcase, make a request to stubby url, then get the most recent request body with:
By default, a booted stubby serves:

* `/`: always respond with status code 200
* `/blackhole/[...]`: always respond with status code 204

In a testcase, make a request to stubby URL, then get the most recent request to the **specified** path with:

```erlang
{ok, Body} = stubby:get_recent()
{ok, #{
headers := Headers,
scheme := Scheme,
host := Host,
port := Port,
path := Path,
qs := QueryString,
body := Body
}} = stubby:get_recent("/path/to/endopoint")
```

When no request is recorded yet, this call blocks until the first request is made.


See also
--------

Expand Down
17 changes: 12 additions & 5 deletions src/stubby.erl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
start/1,
start/2,
stop/0,
get_recent/0
get_recent/1
]).

%% See https://github.com/ninenines/cowboy/blob/master/src/cowboy_router.erl for details.
Expand Down Expand Up @@ -51,7 +51,14 @@ start(Host, Routes) ->
{ok, _} = cowboy:start_clear(
?LISTENER,
[{port, 0}],
#{env => #{dispatch => Dispatch}}
#{
env => #{dispatch => Dispatch},
middlewares => [
cowboy_router,
stubby_recorder_middleware,
cowboy_handler
]
}
),
Url = lists:flatten(io_lib:format("http://~s:~p", [Host, ranch:get_port(?LISTENER)])),
ok = check_ready(Url),
Expand Down Expand Up @@ -79,6 +86,6 @@ stop() ->

%% @doc Fetches a record from the recorder FIFO queue.
%% If empty, the call is blocked until the next enqueue.
-spec get_recent() -> stubby_recorder:result().
get_recent() ->
stubby_recorder:get_recent().
-spec get_recent(string()) -> stubby_recorder:result().
get_recent(Path) ->
stubby_recorder:get_recent(list_to_binary(Path)).
18 changes: 3 additions & 15 deletions src/stubby_blackhole_handler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,9 @@

-export([init/2]).

%% @doc A request recording handler mounted to stubby server at <code>/blackhole/[...]</code>.
%% When requested, the request body is enqueued to recorder FIFO queue, and a response with status code 204 is returned.
%% A request body can be gzipped when <code>Content-Encoding: gzip</code> is specified.
%% @doc A blackhole handler mounted to stubby server at <code>/blackhole/[...]</code>.
%% When requested, a response with status code 204 is returned.
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
init(Req0, State) ->
{ok, Data0, Req} = cowboy_req:read_body(Req0, #{length => infinity}),
Encoding = cowboy_req:header(<<"content-encoding">>, Req0, undefined),
Data = decode_body(Data0, Encoding),
ok = stubby_recorder:put_recent(Data),
init(Req, State) ->
Resp = cowboy_req:reply(204, #{}, <<>>, Req),
{ok, Resp, State}.

decode_body(Data, <<"gzip">>) ->
zlib:gunzip(Data);
decode_body(Data, undefined) ->
Data;
decode_body(_, Encoding) ->
throw({content_encoding, Encoding}).
179 changes: 143 additions & 36 deletions src/stubby_recorder.erl
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,16 @@
-export([
start/0,
stop/0,
put_recent/1,
get_recent/0
put_recent/2,
get_recent/1
]).

-ifdef(TEST).
-export([
enqueue/2,
dequeue/1
]).
-endif.

-export_type([
body/0,
result/0
]).

-type body() :: binary().
-type body() :: term().
-type result() :: {ok, body()}.

-define(RECORDER, ?MODULE).
Expand All @@ -37,57 +30,171 @@ stop() ->

%% @private
start_recorder() ->
loop([], []).
loop(#{}, #{}).

%% @private
enqueue(Item, Queue) ->
[Item | Queue].
enqueue(Key, Item, Tree) ->
case Tree of
#{Key := Queue} ->
Tree#{Key => [Item|Queue]};
_ ->
Tree#{Key => [Item]}
end.

%% @private
dequeue(Queue) ->
[Item|L] = lists:reverse(Queue),
{Item, lists:reverse(L)}.
dequeue(Key, Tree) ->
case Tree of
#{Key := []} ->
none;
#{Key := Queue} ->
[Item|T] = lists:reverse(Queue),
{Item, Tree#{Key => lists:reverse(T)}};
_ ->
none
end.

%% @private
loop(Records, Getters) ->
receive
{put, From, Data} ->
{put, From, Key, Data} ->
From ! ok,
case Getters of
[] ->
loop(enqueue(Data, Records), []);
case dequeue(Key, Getters) of
{KeyGetter, Getters1} ->
KeyGetter ! {ok, Data},
loop(Records, Getters1);
_ ->
{Getter, L} = dequeue(Getters),
Getter ! {ok, Data},
loop(Records, L)
loop(enqueue(Key, Data, Records), Getters)
end;
{get, Getter} ->
case Records of
[] ->
loop([], enqueue(Getter, Getters));
{get, Getter, Key} ->
case dequeue(Key, Records) of
{Data, Records1} ->
Getter ! {ok, Data},
loop(Records1, Getters);
_ ->
{Data, L} = dequeue(Records),
Getter! {ok, Data},
loop(L, Getters)
loop(Records, enqueue(Key, Getter, Getters))
end;
{quit, From} ->
From ! ok,
ok
end.

%% @doc Puts a record into FIFO queue.
-spec put_recent(body()) -> ok.
put_recent(Data) ->
?RECORDER ! {put, self(), Data},
-spec put_recent(term(), body()) -> ok.
put_recent(Key, Data) ->
?RECORDER ! {put, self(), Key, Data},
receive X -> X end.

%% @doc Gets a record from FIFO queue.
%% If empty, the call is blocked until the next enqueue.
-spec get_recent() -> result().
get_recent() ->
?RECORDER ! {get, self()},
-spec get_recent(term()) -> result().
get_recent(Key) ->
?RECORDER ! {get, self(), Key},
receive X -> X end.

stop_recorder() ->
?RECORDER ! {quit, self()},
receive X -> X end.


-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

enqueue_test_() ->
Cases = [
{
"0 element",
foo,
foo,
#{bar => []},
#{
foo => [foo],
bar => []
}
},
{
"1 element",
foo,
bar,
#{
foo => [foo],
bar => []
},
#{
foo => [bar, foo],
bar => []
}
},
{
"2 elements",
foo,
buz,
#{
foo => [bar, foo],
bar => []
},
#{
foo => [buz, bar, foo],
bar => []
}
}
],
F = fun({Title, Key, Input, Queue, Expected}) ->
Actual = enqueue(Key, Input, Queue),
{Title, ?_assertEqual(Expected, Actual)}
end,
lists:map(F, Cases).

dequeue_test_() ->
Cases = [
{
"0 element",
bar,
#{
foo => [foo],
bar => []
},
none
},
{
"1 element",
foo,
#{
foo => [foo],
bar => []
},
{foo, #{
foo => [],
bar => []
}}
},
{
"2 elements",
foo,
#{
foo => [bar, foo],
bar => []
},
{foo, #{
foo => [bar],
bar => []
}}
},
{
"3 elements",
foo,
#{
foo => [buz, bar, foo],
bar => []
},
{foo, #{
foo => [buz, bar],
bar => []
}}
}
],
F = fun({Title, Key, Input, Expected}) ->
Actual = dequeue(Key, Input),
{Title, ?_assertEqual(Expected, Actual)}
end,
lists:map(F, Cases).
-endif.
46 changes: 46 additions & 0 deletions src/stubby_recorder_middleware.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
-module(stubby_recorder_middleware).

-export([execute/2]).

-type stubby_record() :: #{
headers := map(),
scheme := binary(),
host := binary(),
port := integer(),
path := binary(),
qs := binary(),
body := binary()
}.

%% @doc A request recording middleware for all routes.
%% When requested, the request is enqueued to recorder FIFO queue.
%% A request body can be gzipped when <code>Content-Encoding: gzip</code> is specified.
execute(#{path := Path} = Req0, Env) ->
{ok, Data0, Req} = cowboy_req:read_body(Req0, #{length => infinity}),
Encoding = cowboy_req:header(<<"content-encoding">>, Req0, undefined),
Data = decode_body(Data0, Encoding),
ok = stubby_recorder:put_recent(Path, build_record(Req0#{body => Data})),
{ok, Req, Env}.

%% @private
decode_body(Data, <<"gzip">>) ->
zlib:gunzip(Data);
decode_body(Data, undefined) ->
Data;
decode_body(_, Encoding) ->
throw({content_encoding, Encoding}).

%% @private
-spec build_record(Req::term()) -> stubby_record().
build_record(Req) ->
build_record(
[headers, scheme, host, port, path, qs, body],
Req,
#{}
).

%% @private
build_record([], _, Acc) ->
Acc;
build_record([K|T], Req, Acc) ->
build_record(T, Req, Acc#{K => maps:get(K, Req)}).
Loading

0 comments on commit f5d7176

Please sign in to comment.