%%% -*- erlang -*- %%% %%% This file is part of hackney_lib released under the Apache 2 license. %%% See the NOTICE for more information. -module(hackney_headers_new). -author("benoitc"). %% API -export([ new/0, new/1, from_list/1, append/3, store/2, store/3, store_new/3, delete/2, is_key/2, lookup/2, get_value/2, get_value/3, size/1, fold/3, merge/2, to_list/1, to_iolist/1, to_binary/1, parse_content_type/1, parse_media_type/2 ]). -define(kl(Key), hackney_bstr:to_lower(Key)). -type headers() ::term(). -type key() :: binary(). -type value() :: binary() | {binary() | list({binary(), binary()} | binary())}. -type headers_list() :: [{key(), value()}]. -export_types([headers/0, key/0, value/0]). %% @doc initialize an empty headers objecy -spec new() -> headers(). new() -> {0, dict:new()}. -spec new(headers_list() | headers()) -> headers(). new(H) when is_list(H) -> from_list(H); new({_, _}=H) -> H; new(_) -> erlang:error(badarg). %% @doc create headers from a list -spec from_list(headers_list()) -> headers(). from_list(HeadersList) when is_list(HeadersList) -> lists:foldl( fun({Key, Value}, Headers) -> append(Key, Value, Headers) end, {0, dict:new()}, HeadersList ); from_list(_) -> erlang:error(badarg). %% @doc append a new value to the list of value for the the header field %% if the key has not been recorded the list will be created witht eh value as the first item. -spec append(key(), value(), headers()) -> headers(). append(Key, Value, {N, Headers}) -> KL = ?kl(Key), {N + 1, dict:append(KL, {N, Key, Value}, Headers)}. %% @doc replace the content of the header field with the value or the list of values. -spec store(key(), value() | [value()], headers()) -> headers(). store(Key, Values, {N, Headers}) when is_list(Values) -> KL = ?kl(Key), lists:foldl( fun(V, {I, H}) -> {I + 1, dict:append(KL, [{I, Key, V}], H)} end, {N, dict:store(KL, [], Headers)}, Values ); store(Key, Value, {N, Headers}) -> KL = ?kl(Key), {N +1, dict:store(KL, [{N, Key, Value}], Headers)}. %% @doc store a list of headers. Replacing oldest -spec store(headers_list(), headers()) -> headers(). store(KVs, Headers) when is_list(KVs) -> lists:foldl( fun({K, V}, H) -> store(K, V, H) end, Headers, KVs ). %% @doc only store a vakue if the key exist. -spec store_new(key(), value(), headers()) -> {boolean(), headers()}. store_new(Key, Value, {_N, DictHeaders}=Headers) -> KL = ?kl(Key), case dict:is_key(KL, DictHeaders) of true -> {false, Headers}; false -> {true, store(Key, Value, Headers)} end. %% @doc delete a field from headers. -spec delete(key(), headers()) -> headers(). delete(Key, {N, Headers}=H) -> KL = ?kl(Key), case dict:find(KL, Headers) of {ok, Values} -> Headers2 = dict:erase(KL, Headers), {N - length(Values), Headers2}; error -> H end. %% @doc is the header field exists or no -spec is_key(key(), headers()) -> true | false. is_key(Key, {_, Headers}) -> dict:is_key(?kl(Key), Headers). lookup(Key, {_, Headers}) -> case dict:find(?kl(Key), Headers) of {ok, KVs} -> [{K, V} || {_, K, V} <- KVs]; error -> [] end. %% @doc get the first value of an headers or return undefined -spec get_value(key(), headers()) -> value() | undefined. get_value(Key, Headers) -> get_value(Key, Headers, undefined). %% @doc get the first value of an headers or return the default -spec get_value(key(), headers(), any()) -> value() | any(). get_value(Key, Headers, Default) -> case lookup(Key, Headers) of [] -> Default; [{_Key, Value}|_] -> Value end. %% @doc return the number of headers fields -spec size(headers()) -> non_neg_integer(). size({N, _}) -> N. fold(Fun, Acc, {_, Headers}) -> Lines = dict:fold( fun(_, Value, Acc1) -> Value ++ Acc1 end, [], Headers ), do_fold(lists:sort(Lines), Fun, Acc). do_fold([{_, Key, Value} | Rest], Fun, Acc) -> do_fold(Rest, Fun, Fun(Key, Value, Acc)); do_fold([], _Fun, Acc) -> Acc. %% @doc merge 2 headers objects. If a key is already exising in HEader1, it will be kept. -spec merge(headers(), headers()) -> headers(). merge(Headers1, {_, DictHeaders2}) -> dict:fold( fun(Key, Value, H) -> case is_key(Key, H) of true -> H; false -> [{_, K, V} | Rest] = Value, do_merge(Rest, store(K, V, H)) end end, Headers1, DictHeaders2 ). do_merge([{_, K, V} | R], H) -> do_merge(R, append(K, V, H)); do_merge([], H) -> H. %% @doc convert headers to a list -spec to_list(headers()) -> headers_list(). to_list(Headers) -> Result = fold( fun(Key, Value, Acc) -> [{Key, Value} | Acc] end, [], Headers ), lists:reverse(Result). %% @doc conver headers to an iolist. Useful to send them over the wire. -spec to_iolist(headers()) -> iolist(). to_iolist(Headers) -> L = fold( fun(Key, Value0, L1) -> case Value0 of {Value, Params} -> [[ hackney_bstr:to_binary(Key),": ", hackney_bstr:to_binary(Value), params_to_iolist(Params, []), "\r\n" ] | L1]; _ -> [[hackney_bstr:to_binary(Key),": ", hackney_bstr:to_binary(Value0), "\r\n"] | L1] end end, [], Headers ), lists:reverse(["\r\n" | L ]). params_to_iolist([{K, V} | Rest], List) -> List2 = [[";", hackney_bstr:to_binary(K), "=", hackney_bstr:to_binary(V)] | List], params_to_iolist(Rest, List2); params_to_iolist([K | Rest], List) -> List2 = [[";", hackney_bstr:to_binary(K)] | List], params_to_iolist(Rest, List2); params_to_iolist([], List) -> lists:reverse(List). %% @doc transform headers to a binary that can be used to construct a request -spec to_binary(headers()) -> binary(). to_binary(Headers) -> iolist_to_binary(to_iolist(Headers)). %% We lowercase the charset header as we know it's case insensitive. %% TODO: change api to parse content -spec parse_content_type(binary()) -> any(). parse_content_type(Data) -> parse_media_type(Data, fun (Rest, Type, SubType) -> hackney_bstr:params(Rest, fun (<<>>, Params) -> case lists:keyfind(<<"charset">>, 1, Params) of false -> {Type, SubType, Params}; {_, Charset} -> Charset2 = hackney_bstr:to_lower(Charset), Params2 = lists:keyreplace(<<"charset">>, 1, Params, {<<"charset">>, Charset2}), {Type, SubType, Params2} end; (_Rest2, _) -> {error, badarg} end) end). %% @doc Parse a media type. -spec parse_media_type(binary(), fun()) -> any(). parse_media_type(Data, Fun) -> hackney_bstr:token_ci(Data, fun (_Rest, <<>>) -> {error, badarg}; (<< $/, Rest/binary >>, Type) -> hackney_bstr:token_ci(Rest, fun (_Rest2, <<>>) -> {error, badarg}; (Rest2, SubType) -> Fun(Rest2, Type, SubType) end); %% This is a non-strict parsing clause required by some user agents %% that use * instead of */* in the list of media types. (Rest, <<"*">> = Type) -> hackney_bstr:token_ci(<<"*", Rest/binary>>, fun (_Rest2, <<>>) -> {error, badarg}; (Rest2, SubType) -> Fun(Rest2, Type, SubType) end); (_Rest, _Type) -> {error, badarg} end).