Skip to content

Commit

Permalink
Elixir interface (#37)
Browse files Browse the repository at this point in the history
* Elixir interface for bookish_spork_request

* Update doc

* Fixe coverage
  • Loading branch information
Alexey Nikitin committed Jan 27, 2019
1 parent 7d1bd8b commit 70100b0
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 32 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -234,7 +234,7 @@ defmodule ChuckNorrisApiTest do
assert ChuckNorrisApi.random == "Chuck norris tried to crank that soulja boy but it wouldn't crank up"

{:ok, request} = :bookish_spork.capture_request
assert :bookish_spork_request.uri(request) == '/jokes/random'
assert request.uri == '/jokes/random'
end
end

Expand Down
2 changes: 1 addition & 1 deletion doc/README.md
Expand Up @@ -234,7 +234,7 @@ defmodule ChuckNorrisApiTest do
assert ChuckNorrisApi.random == "Chuck norris tried to crank that soulja boy but it wouldn't crank up"

{:ok, request} = :bookish_spork.capture_request
assert :bookish_spork_request.uri(request) == '/jokes/random'
assert request.uri == '/jokes/random'
end
end

Expand Down
4 changes: 2 additions & 2 deletions doc/bookish_spork_request.md
Expand Up @@ -56,7 +56,7 @@ Content-Length header value as intger
### header/2 ###

<pre><code>
header(Request::<a href="#type-t">t()</a>, HeaderName::string()) -&gt; string() | undefined
header(Request::<a href="#type-t">t()</a>, HeaderName::string()) -&gt; string() | nil
</code></pre>
<br />

Expand Down Expand Up @@ -111,7 +111,7 @@ path with query string
### version/1 ###

<pre><code>
version(Request::<a href="#type-t">t()</a>) -&gt; string() | undefined
version(Request::<a href="#type-t">t()</a>) -&gt; string() | nil
</code></pre>
<br />

Expand Down
2 changes: 1 addition & 1 deletion doc/overview.edoc
Expand Up @@ -227,7 +227,7 @@ defmodule ChuckNorrisApiTest do
assert ChuckNorrisApi.random == "Chuck norris tried to crank that soulja boy but it wouldn't crank up"

{:ok, request} = :bookish_spork.capture_request
assert :bookish_spork_request.uri(request) == '/jokes/random'
assert request.uri == '/jokes/random'
end
end
</pre>
Expand Down
6 changes: 6 additions & 0 deletions elvis.config
Expand Up @@ -5,6 +5,12 @@
dirs => ["src"],
include_dirs => ["include"],
filter => "*.erl",
rules => [
{elvis_style, function_naming_convention, #{
ignore => [bookish_spork_request],
regex => "^([a-z][a-z0-9]*_?)*$"
}}
],
ruleset => erl_files
},
#{
Expand Down
83 changes: 56 additions & 27 deletions src/bookish_spork_request.erl
@@ -1,5 +1,10 @@
-module(bookish_spork_request).

-export([
'__struct__'/0,
'__struct__'/1
]).

-export([
new/0,
request_line/4,
Expand All @@ -23,23 +28,47 @@
Minor :: integer()
}.

-record(request, {
method :: undefined | atom(),
uri :: undefined | string(),
version :: undefined | http_version(),
headers = #{} :: map(),
body :: undefined | binary()
}).

-opaque t() :: #request{}.
-opaque t() :: #{
'__struct__' := ?MODULE,
method := nil | atom(),
uri := nil | string(),
version := nil | http_version(),
headers := map(),
body := nil | binary()
}.

-export_type([
t/0
]).

-spec '__struct__'() -> t().
%% @private
'__struct__'() ->
new().

-spec '__struct__'(From :: list() | map()) -> t().
%% @private
'__struct__'(From) ->
new(From).

-spec new() -> t().
%% @private
new() -> #request{}.
new() ->
#{
'__struct__' => ?MODULE,
method => nil,
uri => nil,
version => nil,
headers => #{},
body => nil
}.

-spec new(From :: list() | map()) -> t().
%% @private
new(List) when is_list(List) ->
new(maps:from_list(List));
new(Map) when is_map(Map) ->
maps:fold(fun maps:update/3, new(), Map).

-spec request_line(
Request :: t(),
Expand All @@ -49,68 +78,68 @@ new() -> #request{}.
) -> t().
%% @private
request_line(Request, Method, Uri, Version) ->
Request#request{ method = Method, uri = Uri, version = Version }.
maps:merge(Request, #{ method => Method, uri => Uri, version => Version }).

-spec add_header(Request :: t(), Name :: string(), Value :: string()) -> t().
%% @private
add_header(Request, Name, Value) when is_atom(Name) ->
add_header(Request, atom_to_list(Name), Value);
add_header(#request{ headers = Headers } = Request, Name, Value) ->
add_header(#{ headers := Headers } = Request, Name, Value) ->
HeaderName = string:lowercase(Name),
Request#request{ headers = maps:put(HeaderName, Value, Headers) }.
maps:update(headers, maps:put(HeaderName, Value, Headers), Request).

-spec content_length(Request :: t()) -> integer().
%% @doc Content-Length header value as intger
content_length(Request) ->
case header(Request, "content-length") of
undefined ->
nil ->
0;
ContentLength ->
list_to_integer(ContentLength)
end.

-spec method(Request :: t()) -> atom().
%% @doc http verb: 'GET', 'POST','PUT', 'DELETE', 'OPTIONS', ...
method(#request{ method = Method}) ->
method(#{ method := Method}) ->
Method.

-spec uri(Request :: t()) -> string().
%% @doc path with query string
uri(#request{ uri = Uri}) ->
uri(#{ uri := Uri}) ->
Uri.

-spec version(Request :: t()) -> string() | undefined.
-spec version(Request :: t()) -> string() | nil.
%% @doc http protocol version tuple. Most often would be `{1, 1}'
version(#request{ version = Version }) ->
version(#{ version := Version }) ->
Version.

-spec header(Request :: t(), HeaderName :: string()) -> string() | undefined.
-spec header(Request :: t(), HeaderName :: string()) -> string() | nil.
%% @doc Returns a particular header from request. Header name is lowerced
header(#request{ headers = Headers }, HeaderName) ->
maps:get(HeaderName, Headers, undefined).
header(#{ headers := Headers }, HeaderName) ->
maps:get(HeaderName, Headers, nil).

-spec headers(Request :: t()) -> map().
%% @doc http headers map. Header names are normalized and lowercased
headers(#request{ headers = Headers }) ->
headers(#{ headers := Headers }) ->
Headers.

-spec body(Request :: t()) -> binary().
%% @doc request body
body(#request{ body = Body }) ->
body(#{ body := Body }) ->
Body.

-spec body(Request :: t(), Body :: binary()) -> t().
%% @private
body(Request, Body) ->
Request#request{ body = Body }.
maps:update(body, Body, Request).

-spec is_keepalive(Request :: t()) -> boolean().
%% @doc tells you if the request is keepalive or not [https://tools.ietf.org/html/rfc6223]
is_keepalive(#request{ headers = #{"connection" := Conn }, version = {1, 0} }) ->
is_keepalive(#{ headers := #{"connection" := Conn }, version := {1, 0} }) ->
string:lowercase(Conn) =:= "keep-alive";
is_keepalive(#request{ version = {1, 0} }) ->
is_keepalive(#{ version := {1, 0} }) ->
false;
is_keepalive(#request{ headers = #{"connection" := "close" }, version = {1, 1} }) ->
is_keepalive(#{ headers := #{"connection" := "close"}, version := {1, 1} }) ->
false;
is_keepalive(_) ->
true.
33 changes: 33 additions & 0 deletions test/bookish_spork_request_test.erl
Expand Up @@ -2,6 +2,39 @@

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

elixir_interface_test_() ->
Request = bookish_spork_request:new(),
[?_assertEqual(Request, bookish_spork_request:'__struct__'()),
?_assertEqual(Request, bookish_spork_request:'__struct__'([])),
?_assertEqual(Request, bookish_spork_request:'__struct__'(#{}))].

new_test_() ->
Method = 'POST',
Uri = "/foo/bar",
Version = {1, 1},
Body = <<"Hello">>,
Request = lists:foldl(fun(F, Acc) -> F(Acc) end, bookish_spork_request:new(), [
fun(Req) -> bookish_spork_request:request_line(Req, Method, Uri, Version) end,
fun(Req) -> bookish_spork_request:add_header(Req, "X-Foo", "Bar") end,
fun(Req) -> bookish_spork_request:body(Req, Body) end
]),
Map = #{
method => Method,
uri => Uri,
version => Version,
headers => #{"x-foo" => "Bar"},
body => Body
},
List = [
{method, Method},
{uri, Uri},
{version, Version},
{headers, #{"x-foo" => "Bar"}},
{body, Body}
],
[?_assertEqual(Request, bookish_spork_request:new(Map)),
?_assertEqual(Request, bookish_spork_request:new(List))].

content_length_test_() ->
Request = bookish_spork_request:new(),
RequestWithContentLength = bookish_spork_request:add_header(Request, "Content-Length", "17"),
Expand Down

0 comments on commit 70100b0

Please sign in to comment.