Skip to content

Instantly share code, notes, and snippets.

@asabil
Created September 6, 2011 10:00
Show Gist options
  • Save asabil/1197162 to your computer and use it in GitHub Desktop.
Save asabil/1197162 to your computer and use it in GitHub Desktop.
Cowboy HTTP REST
-module(cowboy_http_rest).
-behaviour(cowboy_http_handler).
-export([
init/3,
handle/2,
terminate/2
]).
-export([
behaviour_info/1
]).
behaviour_info(callbacks) -> [
{init, 3},
%{service_available, 2},
%{known_methods, 2},
%{uri_too_long, 2},
%{allowed_methods, 2},
%{malformed_request, 2},
%{is_authorized, 2},
%{forbidden, 2},
%{valid_content_headers, 2},
%{valid_entity_length, 2},
%{options, 2},
{content_types_provided, 2},
%{content_types_accepted, 2},
%{languages_provided, 2},
%{charsets_provided, 2},
%{encodings_provided, 2},
%{resource_exists, 2},
%{generate_etag, 2},
%{last_modified, 2},
%{moved_permanently, 2},
%{previously_existed, 2},
%{moved_temporarily, 2},
%{allow_missing_post, 2},
%{delete_resource, 2},
%{post_is_create, 2},
%{process_post, 2},
%{is_conflict, 2},
%{multiple_choices, 2},
{terminate, 2}
];
behaviour_info(_Other) ->
undefined.
-include_lib("cowboy/include/http.hrl").
-type resource() :: module().
-type resource_state() :: term().
-type http_content() :: file_content() | data_content().
-type file_content() :: {file, Path :: binary()}.
-type data_content() :: {data, Data :: binary()}.
-type http_response() :: http_content() | {http_headers(), http_content()}.
-type content_provider() :: fun((#http_req{}, resource_state()) -> {http_response(), #http_req{}, resource_state()}).
-type content_encoder() :: fun((http_content()) -> http_content()).
-record(state, {
resource :: resource(),
resource_state :: resource_state(),
content_type :: undefined | {ContentType :: binary(), content_provider()},
content_charset :: undefined | {ContentCharset :: binary(), content_encoder()},
content_encoding :: undefined | {ContentEncoding :: binary(), content_encoder()},
variances = [] :: [atom()],
response_status = 599 :: atom() | 100..600,
response_headers = [] :: http_headers(),
response_body :: undefined | http_content()
}).
%% @hidden
init(Transport, Req, {Resource, Opts}) ->
{ok, Req1, ResourceState} = Resource:init(Transport, Req, Opts),
{ok, Req1, #state{resource=Resource, resource_state=ResourceState}}.
%% @hidden
handle(Req, State) ->
try b13(Req, State) of
{Req2, State2} -> reply(Req2, State2)
catch
throw:{reply, Req2, State2} ->
reply(Req2, State2);
exit:{not_implemented, Req2, State2} ->
reply(Req2, State2#state{response_status = 501})
end.
%% @hidden
terminate(Req, #state{resource=Resource, resource_state=ResourceState}) ->
Resource:terminate(Req, ResourceState).
%% @private
reply(Req, State, StatusCode, Headers) ->
throw({reply, Req, State#state{
response_status = StatusCode,
response_headers = Headers ++ State#state.response_headers
}}).
%% @private
reply(Req, State, StatusCode) ->
throw({reply, Req, State#state{
response_status = StatusCode
}}).
%% @private
reply(Req, State) ->
{Req1, State1} = finalize_content({Req, State}),
Status = State1#state.response_status,
Headers = finalize_headers(State1#state.response_headers),
{ok, ReqDone} = case State1#state.response_body of
undefined ->
cowboy_http_req:reply(Status, Headers, <<>>, Req1);
{data, Data} ->
cowboy_http_req:reply(Status, Headers, Data, Req1);
{file, Path} ->
case file:read_file(Path) of
{ok, Data} ->
cowboy_http_req:reply(Status, Headers, Data, Req1);
{error, _Reason} ->
cowboy_http_req:reply(404, [], <<>>, Req1)
end
end,
{ok, ReqDone, State1}.
%% @private
finalize_content({Req, #state{response_body = undefined} = State}) ->
{Req, State};
finalize_content({Req, State}) ->
finalize_content_charset(case get_response_header('Content-Type', Req, State) of
undefined -> add_response_header({'Content-Type', <<"application/octet-stream">>}, {Req, State});
_ -> {Req, State}
end).
finalize_content_charset({Req, #state{content_charset = {<<"*">>, _}} = State}) ->
finalize_content_encoding({Req, State});
finalize_content_charset({Req, #state{content_charset = {Charset, Encoder}} = State}) ->
{_, Type} = get_response_header('Content-Type', Req, State),
Header = {'Content-Type', <<Type/binary, "; charset=", Charset/binary>>},
finalize_content_encoding(
add_response_header(Header, {Req, State#state{response_body = Encoder(State#state.response_body)}})
).
finalize_content_encoding({Req, #state{content_encoding = {Encoding, Encoder}} = State}) ->
Header = {'Content-Encoding', Encoding},
add_response_header(Header, {Req, State#state{response_body = Encoder(State#state.response_body)}}).
%% @private
finalize_headers(Headers) ->
lists:ukeysort(1, Headers).
%% @private
add_response_header(Header, {Req, State}) ->
{Req, State#state{response_headers = [Header | State#state.response_headers]}}.
%% @private
add_response_headers(Headers, {Req, State}) ->
{Req, State#state{response_headers = Headers ++ State#state.response_headers}}.
%% @private
get_response_header(Name, _Req, State) ->
case lists:keyfind(Name, 1, State#state.response_headers) of
false -> undefined;
Header -> Header
end.
%% @private
check_resource(Req, State, Callback, Expected, OnTrue, OnFalse) when is_function(Expected) ->
case call(Req, State, Callback) of
no_call -> next(Req, State, no_call, OnTrue);
{Value, Req2, State2} ->
case Expected(Value) of
true -> next(Req2, State2, Value, OnTrue);
false -> next(Req2, State2, Value, OnFalse)
end
end;
check_resource(Req, State, Callback, Expected, OnTrue, OnFalse) ->
check_resource(Req, State, Callback, fun(Value) ->
Value =:= Expected
end, OnTrue, OnFalse).
%% @private
check_header(Req, State, Header, OnDefined, OnUndefined) ->
{Value, Req2} = cowboy_http_req:header(Header, Req),
case Value of
undefined -> next(Req2, State, undefined, OnUndefined);
_ -> next(Req2, State, Value, OnDefined)
end.
%% @private
check_content(Req, State, Header, Callback, Default, OnSuccess, OnFailure) ->
{Provided0, Req2, State1} = call(Req, State, Callback, []),
Provided = Provided0 ++ Default,
State2 = State1#state{
variances = case length(Provided) > 1 of
true -> [Header, State1#state.variances];
false -> State1#state.variances
end
},
check_header(Req2, State2, Header,
fun
(<<>>, Req3, State3) ->
next(Req3, State3, hd(Provided), OnSuccess);
(<<"*">>, Req3, State3) ->
next(Req3, State3, hd(Provided), OnSuccess);
(Accepted, Req3, State3) ->
case choose(match_header(Header), Provided, parse_header(Header, Accepted)) of
undefined -> next(Req3, State3, undefined, OnFailure);
Chosen -> next(Req3, State3, Chosen, OnSuccess)
end
end,
fun(Req3, State3) ->
next(Req3, State3, hd(Provided), OnSuccess)
end
).
%% @private
check_content(Req, State, Header, Callback, Default, OnSuccess) ->
check_content(Req, State, Header, Callback, Default, OnSuccess, 406).
%% @private
call(Req, #state{resource = Resource, resource_state = ResourceState} = State, Callback) ->
case erlang:function_exported(Resource, Callback, 2) of
true ->
{Reply, Req2, ResourceState2} = Resource:Callback(Req, ResourceState),
{Reply, Req2, State#state{resource_state = ResourceState2}};
false -> no_call
end.
%% @private
call(Req, State, Callback, Default) ->
case call(Req, State, Callback) of
no_call -> {Default, Req, State};
Reply -> Reply
end.
%% @private
next(Req, State, _Value, Next) when is_function(Next, 2) ->
Next(Req, State);
next(Req, State, Value, Next) when is_function(Next, 3) ->
Next(Value, Req, State);
next(Req, State, _Value, StatusCode) when is_atom(StatusCode) ->
reply(Req, State, StatusCode);
next(Req, State, _Value, StatusCode) when is_integer(StatusCode) ->
reply(Req, State, StatusCode).
%% @private
%% Service available?
b13(Req, State) ->
check_resource(Req, State, service_available, true, fun b12/2, 503).
%% @private
%% Known Method?
b12(#http_req{method = Method} = Req, State) ->
check_resource(Req, State, known_methods, fun(Methods) ->
lists:member(Method, Methods)
end, fun b11/2, 501).
%% @private
%% URI too long?
b11(Req, State) ->
check_resource(Req, State, uri_too_long, false, fun b10/2, 414).
%% @private
%% Method allowed?
b10(#http_req{method = Method} = Req, State) ->
check_resource(Req, State, allowed_methods,
fun(Methods) -> lists:member(Method, Methods) end,
fun(Methods, Req2, State2) ->
Allow = list_to_binary(string:join([atom_to_list(M) || M <- Methods], ", ")),
{Req3, State3} = add_response_header({'Allow', Allow}, {Req2, State2}),
b9(Req3, State3)
end,
fun(Methods, Req2, State2) ->
Allow = list_to_binary(string:join([atom_to_list(M) || M <- Methods], ", ")),
reply(Req2, State2, 405, [{'Allow', Allow}])
end
).
%% @private
%% Content-MD5 present?
b9(Req, State) ->
check_header(Req, State, 'Content-MD5', fun b9a/3, fun b9b/2).
%% @private
%% Content-MD5 valid?
b9a(_ContentMD5, Req, State) ->
b9b(Req, State).
%% @private
%% Malformed?
b9b(Req, State) ->
check_resource(Req, State, malformed_request, false, fun b8/2, 400).
%% @private
%% Authorized?
b8(Req, State) ->
check_resource(Req, State, is_authorized, true,
fun b7/2,
fun(AuthHead, Req2, State2) ->
reply(Req2, State2, 401, [
{'WWW-Authenticate', AuthHead}
])
end
).
%% @private
%% Forbidden?
b7(Req, State) ->
check_resource(Req, State, forbidden, false, fun b6/2, 403).
%% @private
%% Okay Content-* Headers?
b6(Req, State) ->
check_resource(Req, State, valid_content_headers, true, fun b5/2, 501).
%% @private
%% Known Content-Type?
b5(Req, State) ->
check_resource(Req, State, known_content_type, true, fun b4/2, 413).
%% @private
%% Req Entity Too Large?
b4(Req, State) ->
check_resource(Req, State, valid_entity_length, true, fun b3/2, 413).
%% @private
%% OPTIONS?
b3(#http_req{method = 'OPTIONS'} = Req, State) ->
{Headers, Req2, State2} = call(Req, State, options, []),
reply(Req2, State2, 200, [Headers | State#state.response_headers]);
b3(Req, State) ->
c3(Req, State).
%% @private
%% Accept exists?
c3(Req, State) ->
check_content(Req, State, 'Accept', content_types_provided, [], fun(ContentType, Req2, State2) ->
d4(Req2, State2#state{content_type = ContentType})
end).
%% @private
%% Accept-Language exists?
d4(Req, State) ->
% TODO: implement
e5(Req, State).
%% @private
%% Accept-Charset exists?
e5(Req, State) ->
Default = [
{<<"*">>, fun(Content) -> Content end}
],
check_content(Req, State, 'Accept-Charset', charsets_provided, Default, fun(ContentCharset, Req2, State2) ->
f6(Req2, State2#state{content_charset = ContentCharset})
end).
%% @private
%% Accept-Encoding exists?
f6(Req, State) ->
Default = [
{<<"identity">>, fun(Content) -> Content end}
],
check_content(Req, State, 'Accept-Encoding', encodings_provided, Default, fun(Encoding, Req2, State2) ->
g7(Req2, State2#state{content_encoding = Encoding})
end).
%% @private
%% Resource exists?
g7(Req, #state{variances = _Variances} = State) ->
check_resource(Req, State, resource_exists, true, fun g8/2, fun h7/2).
%% @private
%% If-Match exists?
g8(Req, State) ->
check_header(Req, State, 'If-Match',
fun
(<<"*">>, Req2, State2) -> h10(Req2, State2);
(Value, Req2, State2) -> g11(Value, Req2, State2)
end,
fun h10/2
).
%% @private
%% ETag in If-Match?
g11(IfMatch, Req, State) ->
check_resource(Req, State, generate_etag, fun(Etag) ->
not lists:member(Etag, parse_strings(IfMatch))
end, 412, fun h10/2).
%% @private
%% If-Match exists?
h7(Req, State) ->
check_header(Req, State, 'If-Match',
fun
(<<"*">>, Req2, State2) -> reply(Req2, State2, 412);
(_Value, Req2, State2) -> i7(Req2, State2)
end,
fun i7/2
).
%% @private
%% If-Unmodified-Since exists?
h10(Req, State) ->
check_header(Req, State, 'If-Unmodified-Since', fun h11/3, fun i12/2).
%% @private
%% If-Unmodified-Since is valid date?
h11(IfUnmodifiedSince, Req, State) ->
try parse_header('If-Unmodified-Since', IfUnmodifiedSince) of
Date -> h12(Date, Req, State)
catch
error:badarg -> i12(Req, State)
end.
%% @private
%% Last-Modified =< If-Unmodified-Since?
h12(IfUnmodifiedSince, Req, State) ->
check_resource(Req, State, last_modified, fun(LastModified) ->
LastModified =< IfUnmodifiedSince
end, fun i12/2, 412).
%% @private
%% Moved permanently? (apply PUT to different URI)
i4(Req, State) ->
check_resource(Req, State, moved_permanently, false, fun p3/2, fun(Uri, Req2, State2) ->
reply(Req2, State2, 301, [
{'Location', Uri}
])
end).
%% @private
%% PUT?
i7(#http_req{method = 'PUT'} = Req, State) ->
i4(Req, State);
i7(Req, State) ->
k7(Req, State).
%% @private
%% If-none-match exists?
i12(Req, State) ->
check_header(Req, State, 'If-None-Match', fun i13/3, fun l13/2).
%% @private
%% If-None-Match: * exists?
i13(<<"*">>, Req, State) ->
j18(Req, State);
i13(Value, Req, State) ->
k13(Value, Req, State).
%% @private
%% GET or HEAD?
j18(#http_req{method = Method} = Req, State) when Method == 'GET'; Method == 'HEAD' ->
reply(Req, State, 304);
j18(Req, State) ->
reply(Req, State, 412).
%% @private
%% Etag in If-None-Match?
k13(IfNoneMatch, Req, State) ->
check_resource(Req, State, generate_etag, fun(Etag) ->
not lists:member(Etag, parse_strings(IfNoneMatch))
end, fun j18/2, fun l13/2).
%% @private
%% Previously existed?
k7(Req, State) ->
check_resource(Req, State, previously_existed, false, fun l7/2, fun k5/2).
%% @private
%% Moved permanently?
k5(Req, State) ->
check_resource(Req, State, moved_permanently, false, fun l5/2, fun(Uri, Req2, State2) ->
reply(Req2, State2, 301, [
{'Location', Uri}
])
end).
%% @private
%% Moved temporarily?
l5(Req, State) ->
check_resource(Req, State, moved_temporarily, false, fun m5/2, fun(Uri, Req2, State2) ->
reply(Req2, State2, 307, [
{'Location', Uri}
])
end).
%% @private
%% POST?
l7(#http_req{method = 'POST'} = Req, State) ->
m7(Req, State);
l7(Req, State) ->
reply(Req, State, 404).
%% @private
%% If-Modified-Since exists?
l13(Req, State) ->
check_header(Req, State, 'If-Modified-Since', fun l14/3, fun m16/2).
%% @private
%% If-Modified-Since is valid date?
l14(IfModifiedSince, Req, State) ->
try parse_header('If-Modified-Since', IfModifiedSince) of
Date -> l15(Date, Req, State)
catch
error:badarg -> m16(Req, State)
end.
%% @private
%% If-Modified-Since > Now
l15(IfModifiedSince, Req, State) ->
Now = calendar:universal_time(),
case IfModifiedSince > Now of
true -> m16(Req, State);
false -> l17(IfModifiedSince, Req, State)
end.
%% @private
%% Last-Modified > If-Modified-Since?
l17(IfModifiedSince, Req, State) ->
check_resource(Req, State, last_modified, fun(LastModified) ->
LastModified > IfModifiedSince
end, fun m16/2, 304).
%% @private
%% POST?
m5(#http_req{method = 'POST'} = Req, State) ->
n5(Req, State);
m5(Req, State) ->
reply(Req, State, 410).
%% @private
%% Server allows POST to missing resource?
m7(Req, State) ->
check_resource(Req, State, allow_missing_post, true, fun n11/2, 404).
%% @private
%% DELETE?
m16(#http_req{method = 'DELETE'} = Req, State) ->
m20(Req, State);
m16(Req, State) ->
n16(Req, State).
%% @private
%% DELETE enacted immediately? (Also where DELETE is forced.)
m20(Req, State) ->
check_resource(Req, State, delete_resource, true, fun (Req2, State2) ->
check_resource(Req2, State2, delete_completed, true, fun o20/2, 202)
end, 500).
%% @private
%% Server allows POST to missing resource?
n5(Req, State) ->
check_resource(Req, State, allow_missing_post, true, fun n11/2, 410).
%% @private
%% process POST
n11(Req, State) ->
case call(Req, State, post_is_create, false) of
{false, Req2, State2} ->
case call(Req2, State2, process_post) of
no_call ->
reply(Req2, State2, 501);
{Content, Req3, State3} ->
p11(Req3, State3#state{response_body = Content});
{Headers, Content, Req3, State3} ->
{Req4, State4} = add_response_headers(Headers, {Req3, State3#state{response_body = Content}}),
case lists:keyfind('Location', 1, Headers) of
false -> p11(Req4, State4);
_ -> reply(Req4, State4, 303)
end
end;
{true, Req2, State2} ->
erlang:exit({not_implemented, Req2, State2})
end.
%% @private
%% POST?
n16(#http_req{method = 'POST'} = Req, State) ->
n11(Req, State);
n16(Req, State) ->
o16(Req, State).
%% @private
%% Conflict?
o14(Req, State) ->
check_resource(Req, State, is_conflict, false, fun(Req2, State2) ->
erlang:exit({not_implemented, Req2, State2})
end, 409).
%% @private
%% PUT?
o16(#http_req{method = 'PUT'} = Req, State) ->
o14(Req, State);
o16(Req, State) ->
o18(Req, State).
%% @private
%% Multiple representations?
%% Also where body generation for GET and HEAD is done.
o18(#http_req{method = Method} = Req, State) when Method =:= 'GET'; Method =:= 'HEAD' ->
Header = fun({Req0, State0}, Callback, BuildHeader) ->
case call(Req0, State0, Callback) of
no_call -> {Req0, State0};
{Value, R, S} -> add_response_header(BuildHeader(Value), {R, S})
end
end,
{Req1, State1} = Header(Header(Header({Req, State},
generate_etag, fun(ETag) -> {<<"ETag">>, build_string(ETag)} end),
last_modified, fun(Date) -> {'Last-Modified', build_date(Date)} end),
expires, fun(Date) -> {'Expires', build_date(Date)} end),
{ContentType, ContentProvider} = State1#state.content_type,
{Response, Req2, RState} = ContentProvider(Req1, State1#state.resource_state),
State2 = State1#state{resource_state = RState},
{Req3, State3} = case Response of
{Headers, Content} when is_list(Headers) ->
H = Headers ++ [{'Content-Type', ContentType}],
add_response_headers(H, {Req2, State2#state{response_body = Content}});
Content ->
H = {'Content-Type', ContentType},
add_response_header(H, {Req2, State2#state{response_body = Content}})
end,
o18b(Req3, State3);
o18(Req, State) ->
o18b(Req, State).
%% @private
%% Multiple choices?
o18b(Req, State) ->
check_resource(Req, State, multiple_choices, false, 200, 300).
%% @private
%% Response includes an entity?
o20(Req, #state{response_body = undefined} = State) ->
reply(Req, State, 204);
o20(Req, State) ->
o18(Req, State).
%% @private
%% Conflict?
p3(Req, State) ->
check_resource(Req, State, is_conflict, false, fun(Req2, State2) ->
erlang:exit({not_implemented, Req2, State2})
end, 409).
%% @private
%% New resource?
p11(Req, State) ->
case get_response_header('Location', Req, State) of
undefined -> o20(Req, State);
{Req2, State2} -> reply(Req2, State2, 201)
end.
%% @private
choose(_, _Provided, []) ->
undefined;
choose(Matcher, Provided, [Accept | Rest]) ->
case match(Matcher, Provided, Accept) of
[Choice|_] -> Choice;
[] -> choose(Matcher, Provided, Rest)
end.
%% @private
match(_, [], _Accepted) -> [];
match(_, Provided, {<<"*/*">>, _}) -> Provided;
match(_, Provided, {<<"*">>, _}) -> Provided;
match(Matcher, Provided, {AcceptedType, _AcceptedParams}) ->
[{Type, Params} || {Type, Params} <- Provided, Matcher(Type, AcceptedType)].
%% @private
match_header('Accept') -> fun match_media_type/2;
match_header('Accept-Encoding') -> fun match_encoding/2;
match_header('Accept-Charset') -> fun match_encoding/2.
%% @private
match_media_type(_Provided, <<"*">>) -> true;
match_media_type(_Provided, <<"*/*">>) -> true;
match_media_type(Provided, Provided) -> true;
match_media_type(Provided, Accepted) ->
[P1 | _P2] = binary:split(Provided, <<"/">>),
[A1 | A2] = binary:split(Accepted, <<"/">>),
case A2 of
[<<"*">>] when A1 == P1 -> true;
_ -> false
end.
%% @private
match_encoding(_Provided, <<"*">>) -> true;
match_encoding(Provided, Provided) -> true;
match_encoding(_Provided, _Accepted) -> false.
%% @private
parse_header('Accept', Accept) ->
prioritize(parse_parametrized(Accept));
parse_header('Accept-Charset', Charsets) ->
prioritize(parse_parametrized(Charsets));
parse_header('Accept-Encoding', Encodings) ->
prioritize(parse_parametrized(Encodings)) ++ [{<<"identity">>, []}];
parse_header('If-Unmodified-Since', Date) ->
parse_date(Date);
parse_header('If-Modified-Since', Date) ->
parse_date(Date).
%% @private
prioritize(Choices) ->
%% TODO: implement
Choices.
%% @private
parse_date(Date) ->
try httpd_util:convert_request_date(binary_to_list(Date)) of
bad_date -> erlang:error(badarg, Date);
D -> D
catch
error:_ -> erlang:error(badarg, Date)
end.
%% @private
build_date(Date) ->
list_to_binary(httpd_util:rfc1123_date(calendar:universal_time_to_local_time(Date))).
%% @private
parse_parametrized(Value) ->
[case binary:split(Token, <<";">>) of
[Type, Args] -> {strip(Type), parse_kv(Args)};
[Type] -> {strip(Type), []}
end || Token <- binary:split(Value, <<",">>, [global, trim])].
%% @private
parse_kv(Value) ->
parse_kv(binary:split(Value, [<<";">>, <<"=">>], [global, trim]), []).
parse_kv([], Acc) ->
lists:reverse(Acc);
parse_kv([_], Acc) ->
lists:reverse(Acc);
parse_kv([Key, Value | Rest], Acc) ->
case strip(Key) of
<<>> -> parse_kv(Rest, Acc);
K -> parse_kv(Rest, [{K, strip(Value)} | Acc])
end.
%% @private
parse_strings(Value) ->
parse_strings(Value, []).
parse_strings(<<>>, Acc) ->
lists:reverse(Acc);
parse_strings(<<$", Rest/binary>>, Acc) ->
{String, Cont} = parse_string(Rest, <<>>),
parse_strings(Cont, [String | Acc]);
parse_strings(<<_/utf8, Rest/binary>>, Acc) ->
parse_strings(Rest, Acc).
parse_string(<<>>, Acc) ->
{Acc, <<>>};
parse_string(<<$\\, Char/utf8, Rest/binary>>, Acc) ->
parse_string(Rest, <<Acc/binary, Char/utf8>>);
parse_string(<<$", Rest/binary>>, Acc) ->
{Acc, Rest};
parse_string(<<Char/utf8, Rest/binary>>, Acc) ->
parse_string(Rest, <<Acc/binary, Char/utf8>>).
%% @private
build_string(<<$", _/binary>> = String) ->
String;
build_string(String) ->
build_string(String, <<$">>).
build_string(<<>>, Acc) ->
<<Acc/binary, $">>;
build_string(<<$\\, Char/utf8, Rest/binary>>, Acc) ->
build_string(Rest, <<Acc/binary, $\\, Char/utf8>>);
build_string(<<$", Rest/binary>>, Acc) ->
build_string(Rest, <<Acc/binary, $\\, $">>);
build_string(<<Char/utf8, Rest/binary>>, Acc) ->
build_string(Rest, <<Acc/binary, Char/utf8>>).
%% @private
strip(Binary) ->
strip_right(strip_left(Binary)).
%% @private
strip_left(<<C:8, Rest/binary>>) when C == $\s; C == $\t; C == $\n; C == $\r ->
strip_left(Rest);
strip_left(Stripped) ->
Stripped.
%% @private
strip_right(<<C:8, Rest/binary>>) when C == $\s; C == $\t; C == $\n; C == $\r ->
case strip_right(Rest) of
<<>> -> <<>>;
T -> <<C:8, T/binary>>
end;
strip_right(<<C:8, Rest/binary>>) ->
<<C:8, (strip_right(Rest))/binary>>;
strip_right(<<>>) ->
<<>>.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
parse_header_test_() ->
Tests = [
{<<"ISO-8859-1,utf-8;q=0.7,*;q=0.3">>, [
{<<"ISO-8859-1">>,[]},
{<<"utf-8">>,[{<<"q">>,<<"0.7">>}]},
{<<"*">>,[{<<"q">>,<<"0.3">>}]}
]}
],
[{String, fun() ->
?assertEqual(Expect, parse_header('Accept-Charset', String))
end} || {String, Expect} <- Tests].
parse_parametrized_test_() ->
Tests = [
{<<>>, []},
{<<"text/html">>, [{<<"text/html">>, []}]},
{<<"text/html;q=1.0">>, [{<<"text/html">>, [{<<"q">>, <<"1.0">>}]}]},
{<<"text/html;q=1.0;v=hello">>, [{<<"text/html">>, [{<<"q">>, <<"1.0">>}, {<<"v">>,<<"hello">>}]}]},
{<<"text/html;q=1.0;v=hello,*/*">>, [
{<<"text/html">>, [{<<"q">>, <<"1.0">>}, {<<"v">>,<<"hello">>}]},
{<<"*/*">>, []}
]}
],
[{String, fun() ->
?assertEqual(Expect, parse_parametrized(String))
end} || {String, Expect} <- Tests].
parse_kv_test_() ->
Tests = [
{<<>>, []},
{<<"q=1.0;v=hello">>, [{<<"q">>,<<"1.0">>}, {<<"v">>,<<"hello">>}]},
{<<"q=1.0; v=hello">>, [{<<"q">>,<<"1.0">>}, {<<"v">>,<<"hello">>}]},
{<<"q = 1.0 ; v = hello">>, [{<<"q">>,<<"1.0">>}, {<<"v">>,<<"hello">>}]},
{<<"q = 1.0 ; v = hello;w=">>, [{<<"q">>,<<"1.0">>}, {<<"v">>,<<"hello">>}]}
],
[{String, fun() ->
?assertEqual(Expect, parse_kv(String))
end} || {String, Expect} <- Tests].
parse_strings_test_() ->
Tests = [
{<<>>, []},
{<<"\"\"">>, [<<"">>]},
{<<"\"hello\"">>, [<<"hello">>]},
{<<"\"hello\", \"world\"">>, [<<"hello">>, <<"world">>]},
{<<"\"hello\" wonderful \"world\"">>, [<<"hello">>, <<"world">>]}
],
[{String, fun() ->
?assertEqual(Expect, parse_strings(String))
end} || {String, Expect} <- Tests].
build_string_test_() ->
Tests = [
{<<>>, <<"\"\"">>},
{<<"hello">>, <<"\"hello\"">>},
{<<"hello world">>, <<"\"hello world\"">>},
{<<"hello \"world\"">>, <<"\"hello \\\"world\\\"\"">>}
],
[{String, fun() ->
?assertEqual(Expect, build_string(String))
end} || {String, Expect} <- Tests].
strip_test_() ->
Tests = [
{<<>>, {<<>>, <<>>, <<>>}},
{<<" a">>, {<<"a">>, <<" a">>, <<"a">>}},
{<<" a ">>, {<<"a ">>, <<" a">>, <<"a">>}},
{<<" aaa bbb ">>, {<<"aaa bbb ">>, <<" aaa bbb">>, <<"aaa bbb">>}}
],
[{String, fun() ->
{Left, Right, Both} = Expect,
?assertEqual(Left, strip_left(String)),
?assertEqual(Right, strip_right(String)),
?assertEqual(Both, strip(String))
end} || {String, Expect} <- Tests].
-endif.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment