zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

cow_uri_template.erl (11559B)


      1 %% Copyright (c) 2019, Loïc Hoguin <essen@ninenines.eu>
      2 %%
      3 %% Permission to use, copy, modify, and/or distribute this software for any
      4 %% purpose with or without fee is hereby granted, provided that the above
      5 %% copyright notice and this permission notice appear in all copies.
      6 %%
      7 %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
      8 %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
      9 %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     10 %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     11 %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     12 %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     13 %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     14 
     15 %% This is a full level 4 implementation of URI Templates
     16 %% as defined by RFC6570.
     17 
     18 -module(cow_uri_template).
     19 
     20 -export([parse/1]).
     21 -export([expand/2]).
     22 
     23 -type op() :: simple_string_expansion
     24 	| reserved_expansion
     25 	| fragment_expansion
     26 	| label_expansion_with_dot_prefix
     27 	| path_segment_expansion
     28 	| path_style_parameter_expansion
     29 	| form_style_query_expansion
     30 	| form_style_query_continuation.
     31 
     32 -type var_list() :: [
     33 	{no_modifier, binary()}
     34 	| {{prefix_modifier, pos_integer()}, binary()}
     35 	| {explode_modifier, binary()}
     36 ].
     37 
     38 -type uri_template() :: [
     39 	binary() | {expr, op(), var_list()}
     40 ].
     41 -export_type([uri_template/0]).
     42 
     43 -type variables() :: #{
     44 	binary() => binary()
     45 		| integer()
     46 		| float()
     47 		| [binary()]
     48 		| #{binary() => binary()}
     49 }.
     50 
     51 -include("cow_inline.hrl").
     52 -include("cow_parse.hrl").
     53 
     54 %% Parse a URI template.
     55 
     56 -spec parse(binary()) -> uri_template().
     57 parse(URITemplate) ->
     58 	parse(URITemplate, <<>>).
     59 
     60 parse(<<>>, <<>>) ->
     61 	[];
     62 parse(<<>>, Acc) ->
     63 	[Acc];
     64 parse(<<${,R/bits>>, <<>>) ->
     65 	parse_expr(R);
     66 parse(<<${,R/bits>>, Acc) ->
     67 	[Acc|parse_expr(R)];
     68 %% @todo Probably should reject unallowed characters so that
     69 %% we don't produce invalid URIs.
     70 parse(<<C,R/bits>>, Acc) when C =/= $} ->
     71 	parse(R, <<Acc/binary, C>>).
     72 
     73 parse_expr(<<$+,R/bits>>) ->
     74 	parse_var_list(R, reserved_expansion, []);
     75 parse_expr(<<$#,R/bits>>) ->
     76 	parse_var_list(R, fragment_expansion, []);
     77 parse_expr(<<$.,R/bits>>) ->
     78 	parse_var_list(R, label_expansion_with_dot_prefix, []);
     79 parse_expr(<<$/,R/bits>>) ->
     80 	parse_var_list(R, path_segment_expansion, []);
     81 parse_expr(<<$;,R/bits>>) ->
     82 	parse_var_list(R, path_style_parameter_expansion, []);
     83 parse_expr(<<$?,R/bits>>) ->
     84 	parse_var_list(R, form_style_query_expansion, []);
     85 parse_expr(<<$&,R/bits>>) ->
     86 	parse_var_list(R, form_style_query_continuation, []);
     87 parse_expr(R) ->
     88 	parse_var_list(R, simple_string_expansion, []).
     89 
     90 parse_var_list(<<C,R/bits>>, Op, List)
     91 		when ?IS_ALPHANUM(C) or (C =:= $_) ->
     92 	parse_varname(R, Op, List, <<C>>).
     93 
     94 parse_varname(<<C,R/bits>>, Op, List, Name)
     95 		when ?IS_ALPHANUM(C) or (C =:= $_) or (C =:= $.) or (C =:= $%) ->
     96 	parse_varname(R, Op, List, <<Name/binary,C>>);
     97 parse_varname(<<$:,C,R/bits>>, Op, List, Name)
     98 		when (C =:= $1) or (C =:= $2) or (C =:= $3) or (C =:= $4) or (C =:= $5)
     99 			or (C =:= $6) or (C =:= $7) or (C =:= $8) or (C =:= $9) ->
    100 	parse_prefix_modifier(R, Op, List, Name, <<C>>);
    101 parse_varname(<<$*,$,,R/bits>>, Op, List, Name) ->
    102 	parse_var_list(R, Op, [{explode_modifier, Name}|List]);
    103 parse_varname(<<$*,$},R/bits>>, Op, List, Name) ->
    104 	[{expr, Op, lists:reverse([{explode_modifier, Name}|List])}|parse(R, <<>>)];
    105 parse_varname(<<$,,R/bits>>, Op, List, Name) ->
    106 	parse_var_list(R, Op, [{no_modifier, Name}|List]);
    107 parse_varname(<<$},R/bits>>, Op, List, Name) ->
    108 	[{expr, Op, lists:reverse([{no_modifier, Name}|List])}|parse(R, <<>>)].
    109 
    110 parse_prefix_modifier(<<C,R/bits>>, Op, List, Name, Acc)
    111 		when ?IS_DIGIT(C), byte_size(Acc) < 4 ->
    112 	parse_prefix_modifier(R, Op, List, Name, <<Acc/binary,C>>);
    113 parse_prefix_modifier(<<$,,R/bits>>, Op, List, Name, Acc) ->
    114 	parse_var_list(R, Op, [{{prefix_modifier, binary_to_integer(Acc)}, Name}|List]);
    115 parse_prefix_modifier(<<$},R/bits>>, Op, List, Name, Acc) ->
    116 	[{expr, Op, lists:reverse([{{prefix_modifier, binary_to_integer(Acc)}, Name}|List])}|parse(R, <<>>)].
    117 
    118 %% Expand a URI template (after parsing it if necessary).
    119 
    120 -spec expand(binary() | uri_template(), variables()) -> iodata().
    121 expand(URITemplate, Vars) when is_binary(URITemplate) ->
    122 	expand(parse(URITemplate), Vars);
    123 expand(URITemplate, Vars) ->
    124 	expand1(URITemplate, Vars).
    125 
    126 expand1([], _) ->
    127 	[];
    128 expand1([Literal|Tail], Vars) when is_binary(Literal) ->
    129 	[Literal|expand1(Tail, Vars)];
    130 expand1([{expr, simple_string_expansion, VarList}|Tail], Vars) ->
    131 	[simple_string_expansion(VarList, Vars)|expand1(Tail, Vars)];
    132 expand1([{expr, reserved_expansion, VarList}|Tail], Vars) ->
    133 	[reserved_expansion(VarList, Vars)|expand1(Tail, Vars)];
    134 expand1([{expr, fragment_expansion, VarList}|Tail], Vars) ->
    135 	[fragment_expansion(VarList, Vars)|expand1(Tail, Vars)];
    136 expand1([{expr, label_expansion_with_dot_prefix, VarList}|Tail], Vars) ->
    137 	[label_expansion_with_dot_prefix(VarList, Vars)|expand1(Tail, Vars)];
    138 expand1([{expr, path_segment_expansion, VarList}|Tail], Vars) ->
    139 	[path_segment_expansion(VarList, Vars)|expand1(Tail, Vars)];
    140 expand1([{expr, path_style_parameter_expansion, VarList}|Tail], Vars) ->
    141 	[path_style_parameter_expansion(VarList, Vars)|expand1(Tail, Vars)];
    142 expand1([{expr, form_style_query_expansion, VarList}|Tail], Vars) ->
    143 	[form_style_query_expansion(VarList, Vars)|expand1(Tail, Vars)];
    144 expand1([{expr, form_style_query_continuation, VarList}|Tail], Vars) ->
    145 	[form_style_query_continuation(VarList, Vars)|expand1(Tail, Vars)].
    146 
    147 simple_string_expansion(VarList, Vars) ->
    148 	lists:join($,, [
    149 		apply_modifier(Modifier, unreserved, $,, Value)
    150 	|| {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]).
    151 
    152 reserved_expansion(VarList, Vars) ->
    153 	lists:join($,, [
    154 		apply_modifier(Modifier, reserved, $,, Value)
    155 	|| {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]).
    156 
    157 fragment_expansion(VarList, Vars) ->
    158 	case reserved_expansion(VarList, Vars) of
    159 		[] -> [];
    160 		Expanded -> [$#, Expanded]
    161 	end.
    162 
    163 label_expansion_with_dot_prefix(VarList, Vars) ->
    164 	segment_expansion(VarList, Vars, $.).
    165 
    166 path_segment_expansion(VarList, Vars) ->
    167 	segment_expansion(VarList, Vars, $/).
    168 
    169 segment_expansion(VarList, Vars, Sep) ->
    170 	Expanded = lists:join(Sep, [
    171 		apply_modifier(Modifier, unreserved, Sep, Value)
    172 	|| {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]),
    173 	case Expanded of
    174 		[] -> [];
    175 		[[]] -> [];
    176 		_ -> [Sep, Expanded]
    177 	end.
    178 
    179 path_style_parameter_expansion(VarList, Vars) ->
    180 	parameter_expansion(VarList, Vars, $;, $;, trim).
    181 
    182 form_style_query_expansion(VarList, Vars) ->
    183 	parameter_expansion(VarList, Vars, $?, $&, no_trim).
    184 
    185 form_style_query_continuation(VarList, Vars) ->
    186 	parameter_expansion(VarList, Vars, $&, $&, no_trim).
    187 
    188 parameter_expansion(VarList, Vars, LeadingSep, Sep, Trim) ->
    189 	Expanded = lists:join(Sep, [
    190 		apply_parameter_modifier(Modifier, unreserved, Sep, Trim, Name, Value)
    191 	|| {Modifier, Name, Value} <- lookup_variables(VarList, Vars)]),
    192 	case Expanded of
    193 		[] -> [];
    194 		[[]] -> [];
    195 		_ -> [LeadingSep, Expanded]
    196 	end.
    197 
    198 lookup_variables([], _) ->
    199 	[];
    200 lookup_variables([{Modifier, Name}|Tail], Vars) ->
    201 	case Vars of
    202 		#{Name := Value} -> [{Modifier, Name, Value}|lookup_variables(Tail, Vars)];
    203 		_ -> lookup_variables(Tail, Vars)
    204 	end.
    205 
    206 apply_modifier(no_modifier, AllowedChars, _, List) when is_list(List) ->
    207 	lists:join($,, [urlencode(Value, AllowedChars) || Value <- List]);
    208 apply_modifier(explode_modifier, AllowedChars, ExplodeSep, List) when is_list(List) ->
    209 	lists:join(ExplodeSep, [urlencode(Value, AllowedChars) || Value <- List]);
    210 apply_modifier(Modifier, AllowedChars, ExplodeSep, Map) when is_map(Map) ->
    211 	{JoinSep, KVSep} = case Modifier of
    212 		no_modifier -> {$,, $,};
    213 		explode_modifier -> {ExplodeSep, $=}
    214 	end,
    215 	lists:reverse(lists:join(JoinSep,
    216 		maps:fold(fun(Key, Value, Acc) ->
    217 			[[
    218 				urlencode(Key, AllowedChars),
    219 				KVSep,
    220 				urlencode(Value, AllowedChars)
    221 			]|Acc]
    222 		end, [], Map)
    223 	));
    224 apply_modifier({prefix_modifier, MaxLen}, AllowedChars, _, Value) ->
    225 	urlencode(string:slice(binarize(Value), 0, MaxLen), AllowedChars);
    226 apply_modifier(_, AllowedChars, _, Value) ->
    227 	urlencode(binarize(Value), AllowedChars).
    228 
    229 apply_parameter_modifier(_, _, _, _, _, []) ->
    230 	[];
    231 apply_parameter_modifier(_, _, _, _, _, Map) when Map =:= #{} ->
    232 	[];
    233 apply_parameter_modifier(no_modifier, AllowedChars, _, _, Name, List) when is_list(List) ->
    234 	[
    235 		Name,
    236 		$=,
    237 		lists:join($,, [urlencode(Value, AllowedChars) || Value <- List])
    238 	];
    239 apply_parameter_modifier(explode_modifier, AllowedChars, ExplodeSep, _, Name, List) when is_list(List) ->
    240 	lists:join(ExplodeSep, [[
    241 		Name,
    242 		$=,
    243 		urlencode(Value, AllowedChars)
    244 	] || Value <- List]);
    245 apply_parameter_modifier(Modifier, AllowedChars, ExplodeSep, _, Name, Map) when is_map(Map) ->
    246 	{JoinSep, KVSep} = case Modifier of
    247 		no_modifier -> {$,, $,};
    248 		explode_modifier -> {ExplodeSep, $=}
    249 	end,
    250 	[
    251 		case Modifier of
    252 			no_modifier ->
    253 				[
    254 					Name,
    255 					$=
    256 				];
    257 			explode_modifier ->
    258 				[]
    259 		end,
    260 		lists:reverse(lists:join(JoinSep,
    261 			maps:fold(fun(Key, Value, Acc) ->
    262 				[[
    263 					urlencode(Key, AllowedChars),
    264 					KVSep,
    265 					urlencode(Value, AllowedChars)
    266 				]|Acc]
    267 			end, [], Map)
    268 		))
    269 	];
    270 apply_parameter_modifier(Modifier, AllowedChars, _, Trim, Name, Value0) ->
    271 	Value1 = binarize(Value0),
    272 	Value = case Modifier of
    273 		{prefix_modifier, MaxLen} ->
    274 			string:slice(Value1, 0, MaxLen);
    275 		no_modifier ->
    276 			Value1
    277 	end,
    278 	[
    279 		Name,
    280 		case Value of
    281 			<<>> when Trim =:= trim ->
    282 				[];
    283 			<<>> when Trim =:= no_trim ->
    284 				$=;
    285 			_ ->
    286 				[
    287 					$=,
    288 					urlencode(Value, AllowedChars)
    289 				]
    290 		end
    291 	].
    292 
    293 binarize(Value) when is_integer(Value) ->
    294 	integer_to_binary(Value);
    295 binarize(Value) when is_float(Value) ->
    296 	float_to_binary(Value, [{decimals, 10}, compact]);
    297 binarize(Value) ->
    298 	Value.
    299 
    300 urlencode(Value, unreserved) ->
    301 	urlencode_unreserved(Value, <<>>);
    302 urlencode(Value, reserved) ->
    303 	urlencode_reserved(Value, <<>>).
    304 
    305 urlencode_unreserved(<<C,R/bits>>, Acc)
    306 		when ?IS_URI_UNRESERVED(C) ->
    307 	urlencode_unreserved(R, <<Acc/binary,C>>);
    308 urlencode_unreserved(<<C,R/bits>>, Acc) ->
    309 	urlencode_unreserved(R, <<Acc/binary,$%,?HEX(C)>>);
    310 urlencode_unreserved(<<>>, Acc) ->
    311 	Acc.
    312 
    313 urlencode_reserved(<<C,R/bits>>, Acc)
    314 		when ?IS_URI_UNRESERVED(C) or ?IS_URI_GEN_DELIMS(C) or ?IS_URI_SUB_DELIMS(C) ->
    315 	urlencode_reserved(R, <<Acc/binary,C>>);
    316 urlencode_reserved(<<C,R/bits>>, Acc) ->
    317 	urlencode_reserved(R, <<Acc/binary,$%,?HEX(C)>>);
    318 urlencode_reserved(<<>>, Acc) ->
    319 	Acc.
    320 
    321 -ifdef(TEST).
    322 expand_uritemplate_test_() ->
    323 	Files = filelib:wildcard("deps/uritemplate-tests/*.json"),
    324 	lists:flatten([begin
    325 		{ok, JSON} = file:read_file(File),
    326 		Tests = jsx:decode(JSON, [return_maps]),
    327 		[begin
    328 			%% Erlang doesn't have a NULL value.
    329 			Vars = maps:remove(<<"undef">>, Vars0),
    330 			[
    331 				{iolist_to_binary(io_lib:format("~s - ~s: ~s => ~s",
    332 					[filename:basename(File), Section, URITemplate,
    333 						if
    334 							is_list(Expected) -> lists:join(<<" OR ">>, Expected);
    335 							true -> Expected
    336 						end
    337 					])),
    338 					fun() ->
    339 						case Expected of
    340 							false ->
    341 								{'EXIT', _} = (catch expand(URITemplate, Vars));
    342 							[_|_] ->
    343 								Result = iolist_to_binary(expand(URITemplate, Vars)),
    344 								io:format("~p", [Result]),
    345 								true = lists:member(Result, Expected);
    346 							_ ->
    347 								Expected = iolist_to_binary(expand(URITemplate, Vars))
    348 						end
    349 					end}
    350 			|| [URITemplate, Expected] <- Cases]
    351 		end || {Section, #{
    352 			<<"variables">> := Vars0,
    353 			<<"testcases">> := Cases
    354 		}} <- maps:to_list(Tests)]
    355 	end || File <- Files]).
    356 -endif.