Current section

Files

Jump to
accept src accept_header.erl
Raw

src/accept_header.erl

%% @doc HTTP instrumentation helpers
-module(accept_header).
-export([parse/1,
negotiate/2]).
-include("accept.hrl").
%%====================================================================
%% Public API
%%====================================================================
%% @doc
%% Parses Accept header, returns a list of media_ranges.
%% @end
-spec parse(AcceptString) -> Result when
AcceptString :: binary() | string (),
Result :: [#media_range{}].
parse(AcceptString) ->
lists:map(fun parse_media_range/1,
string:tokens(ensure_string(AcceptString), ",")).
%% @doc
%% Negotiates the most appropriate content_type given the accept header
%% and a list of alternatives.
%% @end
-spec negotiate(Header, Alternatives) -> Match when
Header :: BinaryOrString,
Alternatives :: [Alternative],
Alternative :: BinaryOrString | {BinaryOrString | Tag},
BinaryOrString :: binary() | string(),
Tag :: any(),
Match :: Tag | nomatch.
negotiate(Header, Alternatives) ->
MediaRanges = parse(Header),
Alts = lists:map(fun (Alt) ->
{A, Tag} = case Alt of
{_, _} -> Alt;
_ -> {Alt, Alt}
end,
PA = parse_media_range(ensure_string(A)),
%% list of Alt-MR scores
AltMRScores = lists:map(fun (MR) ->
{score_alt(MR, PA), MR}
end,
MediaRanges),
%% best Media Range match for this Alternative
[{Score, BMR} | _ ] = lists:sort(fun scored_cmp/2,
AltMRScores),
case Score of
0 ->
{-1, Tag};
_ ->
#media_range{q = BMRQ} = BMR,
{BMRQ, Tag}
end
end,
Alternatives),
%% find alternative with the best score
%% keysort is stable so order of Alternatives preserved
%% after sorting Tail has the best score.
%% However if multiple alternatives have the same score as Tail
%% we should find first best alternative to respect user's priority.
{_, Tag} = find_preferred_best(lists:keysort(1, Alts)),
Tag.
%%====================================================================
%% Private Parts
%%====================================================================
parse_media_range(RawMRString) ->
[MR | RawParams] = string:tokens(RawMRString, ";"),
[Type, Subtype] = lists:map(fun string:strip/1, string:tokens(MR, "/")),
Params = lists:filtermap(fun parse_media_range_param/1, RawParams),
{Q, ParamsWOQ} = find_media_range_q(Params),
#media_range{type = Type,
subtype = Subtype,
q = Q,
params = ParamsWOQ}.
parse_media_range_param(Param) ->
case string:tokens(Param, "=") of
[Name, Value] -> {true, {Name, Value}};
_ -> false %% simply ignore malformed Name=Value pairs
end.
find_media_range_q(Params) ->
{parse_q(proplists:get_value("q", Params, "1")),
proplists:delete("q", Params)}.
parse_q(Q) ->
try
case lists:member($., Q) of
true ->
list_to_float(Q);
false ->
list_to_integer(Q)
end
catch error:badarg ->
0
end.
scored_cmp({S1, _}, {S2, _}) ->
S1 > S2.
find_preferred_best(Sorted) ->
[B | R] = lists:reverse(Sorted),
find_preferred_best(B, R).
find_preferred_best({Q, _}, [{Q, _} = H | R]) ->
find_preferred_best(H, R);
find_preferred_best(B, []) ->
B;
find_preferred_best(B, _) ->
B.
%% Alternative "text/plain; version=4"
%% text/plain; version=4 > text/plan > text/plain; n=v > text/* > */* > image/*
score_alt(#media_range{type = Type,
subtype = SubType,
params = MRParams},
#media_range{type = Type,
subtype = SubType,
params = AltParams}) ->
8 + 4 + score_params(MRParams, AltParams);
score_alt(#media_range{type = Type,
subtype = "*"},
#media_range{type = Type}) ->
8 + 3;
score_alt(#media_range{type = "*"},
_) ->
8;
score_alt(_, _) ->
0.
%% If media range doesn't have params 1
%% If params match 2
%% otherwise 0
score_params([], _) ->
1;
score_params(MRParams, AltParams) when length(MRParams) == length(AltParams) ->
case lists:sort(MRParams) == lists:sort(AltParams) of
true -> 2;
_ -> 0
end;
score_params(_, _) ->
0.
ensure_string(V) when is_list(V) ->
V;
ensure_string(V) when is_binary(V) ->
binary_to_list(V).