aboutsummaryrefslogtreecommitdiffstats
path: root/src/cow_http_struct_hd.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/cow_http_struct_hd.erl')
-rw-r--r--src/cow_http_struct_hd.erl144
1 files changed, 121 insertions, 23 deletions
diff --git a/src/cow_http_struct_hd.erl b/src/cow_http_struct_hd.erl
index acbf7b2..373c8da 100644
--- a/src/cow_http_struct_hd.erl
+++ b/src/cow_http_struct_hd.erl
@@ -32,13 +32,16 @@
-export([parse_dictionary/1]).
-export([parse_item/1]).
-export([parse_list/1]).
+-export([dictionary/1]).
+-export([item/1]).
+-export([list/1]).
-include("cow_parse.hrl").
-type sh_list() :: [sh_item() | sh_inner_list()].
-type sh_inner_list() :: sh_with_params([sh_item()]).
-type sh_params() :: #{binary() => sh_bare_item() | undefined}.
--type sh_dictionary() :: #{binary() => sh_item() | sh_inner_list()}.
+-type sh_dictionary() :: {#{binary() => sh_item() | sh_inner_list()}, [binary()]}.
-type sh_item() :: sh_with_params(sh_bare_item()).
-type sh_bare_item() :: integer() | float() | boolean()
| {string | token | binary, binary()}.
@@ -53,39 +56,39 @@
(C =:= $z)
).
-%% Public interface.
+%% Parsing.
-spec parse_dictionary(binary()) -> sh_dictionary().
parse_dictionary(<<>>) ->
- #{};
+ {#{}, []};
parse_dictionary(<<C,R/bits>>) when ?IS_LC_ALPHA(C) ->
- {Dict, <<>>} = parse_dict_key(R, #{}, <<C>>),
- Dict.
+ {Dict, Order, <<>>} = parse_dict_key(R, #{}, [], <<C>>),
+ {Dict, Order}.
-parse_dict_key(<<$=,$(,R0/bits>>, Acc, K) ->
+parse_dict_key(<<$=,$(,R0/bits>>, Acc, Order, K) ->
false = maps:is_key(K, Acc),
{Item, R} = parse_inner_list(R0, []),
- parse_dict_before_sep(R, Acc#{K => Item});
-parse_dict_key(<<$=,R0/bits>>, Acc, K) ->
+ parse_dict_before_sep(R, Acc#{K => Item}, [K|Order]);
+parse_dict_key(<<$=,R0/bits>>, Acc, Order, K) ->
false = maps:is_key(K, Acc),
{Item, R} = parse_item1(R0),
- parse_dict_before_sep(R, Acc#{K => Item});
-parse_dict_key(<<C,R/bits>>, Acc, K)
+ parse_dict_before_sep(R, Acc#{K => Item}, [K|Order]);
+parse_dict_key(<<C,R/bits>>, Acc, Order, K)
when ?IS_LC_ALPHA(C) or ?IS_DIGIT(C)
or (C =:= $_) or (C =:= $-) or (C =:= $*) ->
- parse_dict_key(R, Acc, <<K/binary,C>>).
+ parse_dict_key(R, Acc, Order, <<K/binary,C>>).
-parse_dict_before_sep(<<C,R/bits>>, Acc) when ?IS_WS(C) ->
- parse_dict_before_sep(R, Acc);
-parse_dict_before_sep(<<C,R/bits>>, Acc) when C =:= $, ->
- parse_dict_before_member(R, Acc);
-parse_dict_before_sep(<<>>, Acc) ->
- {Acc, <<>>}.
+parse_dict_before_sep(<<C,R/bits>>, Acc, Order) when ?IS_WS(C) ->
+ parse_dict_before_sep(R, Acc, Order);
+parse_dict_before_sep(<<C,R/bits>>, Acc, Order) when C =:= $, ->
+ parse_dict_before_member(R, Acc, Order);
+parse_dict_before_sep(<<>>, Acc, Order) ->
+ {Acc, lists:reverse(Order), <<>>}.
-parse_dict_before_member(<<C,R/bits>>, Acc) when ?IS_WS(C) ->
- parse_dict_before_member(R, Acc);
-parse_dict_before_member(<<C,R/bits>>, Acc) when ?IS_LC_ALPHA(C) ->
- parse_dict_key(R, Acc, <<C>>).
+parse_dict_before_member(<<C,R/bits>>, Acc, Order) when ?IS_WS(C) ->
+ parse_dict_before_member(R, Acc, Order);
+parse_dict_before_member(<<C,R/bits>>, Acc, Order) when ?IS_LC_ALPHA(C) ->
+ parse_dict_key(R, Acc, Order, <<C>>).
-spec parse_item(binary()) -> sh_item().
parse_item(Bin) ->
@@ -218,7 +221,7 @@ parse_binary(<<C,R/bits>>, Acc) when ?IS_ALPHANUM(C) or (C =:= $+) or (C =:= $/)
parse_binary(R, <<Acc/binary,C>>).
-ifdef(TEST).
-struct_hd_test_() ->
+parse_struct_hd_test_() ->
Files = filelib:wildcard("deps/structured-header-tests/*.json"),
lists:flatten([begin
{ok, JSON} = file:read_file(File),
@@ -248,7 +251,7 @@ struct_hd_test_() ->
<<"list">> when MustFail; CanFail ->
{'EXIT', _} = (catch parse_list(Raw));
<<"dictionary">> ->
- Expected = (catch parse_dictionary(Raw));
+ {Expected, _Order} = (catch parse_dictionary(Raw));
<<"item">> ->
Expected = (catch parse_item(Raw));
<<"list">> ->
@@ -320,3 +323,98 @@ trim_ws_end(Value, N) ->
Value2
end.
-endif.
+
+%% Building.
+
+-spec dictionary(#{binary() => sh_item() | sh_inner_list()}
+ | [{binary(), sh_item() | sh_inner_list()}])
+ -> iolist().
+%% @todo Also accept this? dictionary({Map, Order}) ->
+dictionary(Map) when is_map(Map) ->
+ dictionary(maps:to_list(Map));
+dictionary(KVList) when is_list(KVList) ->
+ lists:join(<<", ">>, [
+ [Key, $=, item_or_inner_list(Value)]
+ || {Key, Value} <- KVList]).
+
+-spec item(sh_item()) -> iolist().
+item({with_params, BareItem, Params}) ->
+ [bare_item(BareItem), params(Params)].
+
+-spec list(sh_list()) -> iolist().
+list(List) ->
+ lists:join(<<", ">>, [item_or_inner_list(Value) || Value <- List]).
+
+item_or_inner_list(Value={with_params, List, _}) when is_list(List) ->
+ inner_list(Value);
+item_or_inner_list(Value) ->
+ item(Value).
+
+inner_list({with_params, List, Params}) ->
+ [$(, lists:join($\s, [item(Value) || Value <- List]), $), params(Params)].
+
+bare_item({string, String}) ->
+ [$", escape_string(String, <<>>), $"];
+bare_item({token, Token}) ->
+ Token;
+bare_item({binary, Binary}) ->
+ [$*, base64:encode(Binary), $*];
+bare_item(Integer) when is_integer(Integer) ->
+ integer_to_binary(Integer);
+%% In order to properly reproduce the float as a string we
+%% must first determine how many decimals we want in the
+%% fractional component, otherwise rounding errors may occur.
+bare_item(Float) when is_float(Float) ->
+ Decimals = case trunc(Float) of
+ I when I >= 10000000000000 -> 1;
+ I when I >= 1000000000000 -> 2;
+ I when I >= 100000000000 -> 3;
+ I when I >= 10000000000 -> 4;
+ I when I >= 1000000000 -> 5;
+ _ -> 6
+ end,
+ float_to_binary(Float, [{decimals, Decimals}, compact]);
+bare_item(true) ->
+ <<"?1">>;
+bare_item(false) ->
+ <<"?0">>.
+
+escape_string(<<>>, Acc) -> Acc;
+escape_string(<<$\\,R/bits>>, Acc) -> escape_string(R, <<Acc/binary,$\\,$\\>>);
+escape_string(<<$",R/bits>>, Acc) -> escape_string(R, <<Acc/binary,$\\,$">>);
+escape_string(<<C,R/bits>>, Acc) -> escape_string(R, <<Acc/binary,C>>).
+
+params(Params) ->
+ maps:fold(fun
+ (Key, undefined, Acc) ->
+ [[$;, Key]|Acc];
+ (Key, Value, Acc) ->
+ [[$;, Key, $=, bare_item(Value)]|Acc]
+ end, [], Params).
+
+-ifdef(TEST).
+struct_hd_identity_test_() ->
+ Files = filelib:wildcard("deps/structured-header-tests/*.json"),
+ lists:flatten([begin
+ {ok, JSON} = file:read_file(File),
+ Tests = jsx:decode(JSON, [return_maps]),
+ [
+ {iolist_to_binary(io_lib:format("~s: ~s", [filename:basename(File), Name])), fun() ->
+ Expected = expected_to_term(Expected0),
+ case HeaderType of
+ <<"dictionary">> ->
+ {Expected, _Order} = parse_dictionary(iolist_to_binary(dictionary(Expected)));
+ <<"item">> ->
+ Expected = parse_item(iolist_to_binary(item(Expected)));
+ <<"list">> ->
+ Expected = parse_list(iolist_to_binary(list(Expected)))
+ end
+ end}
+ || #{
+ <<"name">> := Name,
+ <<"header_type">> := HeaderType,
+ %% We only run tests that must not fail.
+ <<"expected">> := Expected0
+ } <- Tests]
+ end || File <- Files]).
+-endif.