From ea6b482f82e016aeb171c3fa37734a97a182f63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Wed, 18 Dec 2019 21:27:35 +0100 Subject: Rename cow_uri_templates to cow_uri_template Fits better since we are dealing with a single template at a time. --- ebin/cowlib.app | 2 +- src/cow_uri_template.erl | 356 ++++++++++++++++++++++++++++++++++++++++++++++ src/cow_uri_templates.erl | 356 ---------------------------------------------- 3 files changed, 357 insertions(+), 357 deletions(-) create mode 100644 src/cow_uri_template.erl delete mode 100644 src/cow_uri_templates.erl diff --git a/ebin/cowlib.app b/ebin/cowlib.app index 5b18a58..db7ea07 100644 --- a/ebin/cowlib.app +++ b/ebin/cowlib.app @@ -1,7 +1,7 @@ {application, 'cowlib', [ {description, "Support library for manipulating Web protocols."}, {vsn, "2.8.0"}, - {modules, ['cow_base64url','cow_cookie','cow_date','cow_hpack','cow_http','cow_http2','cow_http2_machine','cow_http_hd','cow_http_struct_hd','cow_http_te','cow_iolists','cow_link','cow_mimetypes','cow_multipart','cow_qs','cow_spdy','cow_sse','cow_uri','cow_uri_templates','cow_ws']}, + {modules, ['cow_base64url','cow_cookie','cow_date','cow_hpack','cow_http','cow_http2','cow_http2_machine','cow_http_hd','cow_http_struct_hd','cow_http_te','cow_iolists','cow_link','cow_mimetypes','cow_multipart','cow_qs','cow_spdy','cow_sse','cow_uri','cow_uri_template','cow_ws']}, {registered, []}, {applications, [kernel,stdlib,crypto]}, {env, []} diff --git a/src/cow_uri_template.erl b/src/cow_uri_template.erl new file mode 100644 index 0000000..eac784f --- /dev/null +++ b/src/cow_uri_template.erl @@ -0,0 +1,356 @@ +%% Copyright (c) 2019, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% This is a full level 4 implementation of URI Templates +%% as defined by RFC6570. + +-module(cow_uri_template). + +-export([parse/1]). +-export([expand/2]). + +-type op() :: simple_string_expansion + | reserved_expansion + | fragment_expansion + | label_expansion_with_dot_prefix + | path_segment_expansion + | path_style_parameter_expansion + | form_style_query_expansion + | form_style_query_continuation. + +-type var_list() :: [ + {no_modifier, binary()} + | {{prefix_modifier, pos_integer()}, binary()} + | {explode_modifier, binary()} +]. + +-type uri_template() :: [ + binary() | {expr, op(), var_list()} +]. +-export_type([uri_template/0]). + +-type variables() :: #{ + binary() => binary() + | integer() + | float() + | [binary()] + | #{binary() => binary()} +}. + +-include("cow_inline.hrl"). +-include("cow_parse.hrl"). + +%% Parse a URI template. + +-spec parse(binary()) -> uri_template(). +parse(URITemplate) -> + parse(URITemplate, <<>>). + +parse(<<>>, <<>>) -> + []; +parse(<<>>, Acc) -> + [Acc]; +parse(<<${,R/bits>>, <<>>) -> + parse_expr(R); +parse(<<${,R/bits>>, Acc) -> + [Acc|parse_expr(R)]; +%% @todo Probably should reject unallowed characters so that +%% we don't produce invalid URIs. +parse(<>, Acc) when C =/= $} -> + parse(R, <>). + +parse_expr(<<$+,R/bits>>) -> + parse_var_list(R, reserved_expansion, []); +parse_expr(<<$#,R/bits>>) -> + parse_var_list(R, fragment_expansion, []); +parse_expr(<<$.,R/bits>>) -> + parse_var_list(R, label_expansion_with_dot_prefix, []); +parse_expr(<<$/,R/bits>>) -> + parse_var_list(R, path_segment_expansion, []); +parse_expr(<<$;,R/bits>>) -> + parse_var_list(R, path_style_parameter_expansion, []); +parse_expr(<<$?,R/bits>>) -> + parse_var_list(R, form_style_query_expansion, []); +parse_expr(<<$&,R/bits>>) -> + parse_var_list(R, form_style_query_continuation, []); +parse_expr(R) -> + parse_var_list(R, simple_string_expansion, []). + +parse_var_list(<>, Op, List) + when ?IS_ALPHANUM(C) or (C =:= $_) -> + parse_varname(R, Op, List, <>). + +parse_varname(<>, Op, List, Name) + when ?IS_ALPHANUM(C) or (C =:= $_) or (C =:= $.) or (C =:= $%) -> + parse_varname(R, Op, List, <>); +parse_varname(<<$:,C,R/bits>>, Op, List, Name) + when (C =:= $1) or (C =:= $2) or (C =:= $3) or (C =:= $4) or (C =:= $5) + or (C =:= $6) or (C =:= $7) or (C =:= $8) or (C =:= $9) -> + parse_prefix_modifier(R, Op, List, Name, <>); +parse_varname(<<$*,$,,R/bits>>, Op, List, Name) -> + parse_var_list(R, Op, [{explode_modifier, Name}|List]); +parse_varname(<<$*,$},R/bits>>, Op, List, Name) -> + [{expr, Op, lists:reverse([{explode_modifier, Name}|List])}|parse(R, <<>>)]; +parse_varname(<<$,,R/bits>>, Op, List, Name) -> + parse_var_list(R, Op, [{no_modifier, Name}|List]); +parse_varname(<<$},R/bits>>, Op, List, Name) -> + [{expr, Op, lists:reverse([{no_modifier, Name}|List])}|parse(R, <<>>)]. + +parse_prefix_modifier(<>, Op, List, Name, Acc) + when ?IS_DIGIT(C), byte_size(Acc) < 4 -> + parse_prefix_modifier(R, Op, List, Name, <>); +parse_prefix_modifier(<<$,,R/bits>>, Op, List, Name, Acc) -> + parse_var_list(R, Op, [{{prefix_modifier, binary_to_integer(Acc)}, Name}|List]); +parse_prefix_modifier(<<$},R/bits>>, Op, List, Name, Acc) -> + [{expr, Op, lists:reverse([{{prefix_modifier, binary_to_integer(Acc)}, Name}|List])}|parse(R, <<>>)]. + +%% Expand a URI template (after parsing it if necessary). + +-spec expand(binary() | uri_template(), variables()) -> iodata(). +expand(URITemplate, Vars) when is_binary(URITemplate) -> + expand(parse(URITemplate), Vars); +expand(URITemplate, Vars) -> + expand1(URITemplate, Vars). + +expand1([], _) -> + []; +expand1([Literal|Tail], Vars) when is_binary(Literal) -> + [Literal|expand1(Tail, Vars)]; +expand1([{expr, simple_string_expansion, VarList}|Tail], Vars) -> + [simple_string_expansion(VarList, Vars)|expand1(Tail, Vars)]; +expand1([{expr, reserved_expansion, VarList}|Tail], Vars) -> + [reserved_expansion(VarList, Vars)|expand1(Tail, Vars)]; +expand1([{expr, fragment_expansion, VarList}|Tail], Vars) -> + [fragment_expansion(VarList, Vars)|expand1(Tail, Vars)]; +expand1([{expr, label_expansion_with_dot_prefix, VarList}|Tail], Vars) -> + [label_expansion_with_dot_prefix(VarList, Vars)|expand1(Tail, Vars)]; +expand1([{expr, path_segment_expansion, VarList}|Tail], Vars) -> + [path_segment_expansion(VarList, Vars)|expand1(Tail, Vars)]; +expand1([{expr, path_style_parameter_expansion, VarList}|Tail], Vars) -> + [path_style_parameter_expansion(VarList, Vars)|expand1(Tail, Vars)]; +expand1([{expr, form_style_query_expansion, VarList}|Tail], Vars) -> + [form_style_query_expansion(VarList, Vars)|expand1(Tail, Vars)]; +expand1([{expr, form_style_query_continuation, VarList}|Tail], Vars) -> + [form_style_query_continuation(VarList, Vars)|expand1(Tail, Vars)]. + +simple_string_expansion(VarList, Vars) -> + lists:join($,, [ + apply_modifier(Modifier, unreserved, $,, Value) + || {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]). + +reserved_expansion(VarList, Vars) -> + lists:join($,, [ + apply_modifier(Modifier, reserved, $,, Value) + || {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]). + +fragment_expansion(VarList, Vars) -> + case reserved_expansion(VarList, Vars) of + [] -> []; + Expanded -> [$#, Expanded] + end. + +label_expansion_with_dot_prefix(VarList, Vars) -> + segment_expansion(VarList, Vars, $.). + +path_segment_expansion(VarList, Vars) -> + segment_expansion(VarList, Vars, $/). + +segment_expansion(VarList, Vars, Sep) -> + Expanded = lists:join(Sep, [ + apply_modifier(Modifier, unreserved, Sep, Value) + || {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]), + case Expanded of + [] -> []; + [[]] -> []; + _ -> [Sep, Expanded] + end. + +path_style_parameter_expansion(VarList, Vars) -> + parameter_expansion(VarList, Vars, $;, $;, trim). + +form_style_query_expansion(VarList, Vars) -> + parameter_expansion(VarList, Vars, $?, $&, no_trim). + +form_style_query_continuation(VarList, Vars) -> + parameter_expansion(VarList, Vars, $&, $&, no_trim). + +parameter_expansion(VarList, Vars, LeadingSep, Sep, Trim) -> + Expanded = lists:join(Sep, [ + apply_parameter_modifier(Modifier, unreserved, Sep, Trim, Name, Value) + || {Modifier, Name, Value} <- lookup_variables(VarList, Vars)]), + case Expanded of + [] -> []; + [[]] -> []; + _ -> [LeadingSep, Expanded] + end. + +lookup_variables([], _) -> + []; +lookup_variables([{Modifier, Name}|Tail], Vars) -> + case Vars of + #{Name := Value} -> [{Modifier, Name, Value}|lookup_variables(Tail, Vars)]; + _ -> lookup_variables(Tail, Vars) + end. + +apply_modifier(no_modifier, AllowedChars, _, List) when is_list(List) -> + lists:join($,, [urlencode(Value, AllowedChars) || Value <- List]); +apply_modifier(explode_modifier, AllowedChars, ExplodeSep, List) when is_list(List) -> + lists:join(ExplodeSep, [urlencode(Value, AllowedChars) || Value <- List]); +apply_modifier(Modifier, AllowedChars, ExplodeSep, Map) when is_map(Map) -> + {JoinSep, KVSep} = case Modifier of + no_modifier -> {$,, $,}; + explode_modifier -> {ExplodeSep, $=} + end, + lists:reverse(lists:join(JoinSep, + maps:fold(fun(Key, Value, Acc) -> + [[ + urlencode(Key, AllowedChars), + KVSep, + urlencode(Value, AllowedChars) + ]|Acc] + end, [], Map) + )); +apply_modifier({prefix_modifier, MaxLen}, AllowedChars, _, Value) -> + urlencode(string:slice(binarize(Value), 0, MaxLen), AllowedChars); +apply_modifier(_, AllowedChars, _, Value) -> + urlencode(binarize(Value), AllowedChars). + +apply_parameter_modifier(_, _, _, _, _, []) -> + []; +apply_parameter_modifier(_, _, _, _, _, Map) when Map =:= #{} -> + []; +apply_parameter_modifier(no_modifier, AllowedChars, _, _, Name, List) when is_list(List) -> + [ + Name, + $=, + lists:join($,, [urlencode(Value, AllowedChars) || Value <- List]) + ]; +apply_parameter_modifier(explode_modifier, AllowedChars, ExplodeSep, _, Name, List) when is_list(List) -> + lists:join(ExplodeSep, [[ + Name, + $=, + urlencode(Value, AllowedChars) + ] || Value <- List]); +apply_parameter_modifier(Modifier, AllowedChars, ExplodeSep, _, Name, Map) when is_map(Map) -> + {JoinSep, KVSep} = case Modifier of + no_modifier -> {$,, $,}; + explode_modifier -> {ExplodeSep, $=} + end, + [ + case Modifier of + no_modifier -> + [ + Name, + $= + ]; + explode_modifier -> + [] + end, + lists:reverse(lists:join(JoinSep, + maps:fold(fun(Key, Value, Acc) -> + [[ + urlencode(Key, AllowedChars), + KVSep, + urlencode(Value, AllowedChars) + ]|Acc] + end, [], Map) + )) + ]; +apply_parameter_modifier(Modifier, AllowedChars, _, Trim, Name, Value0) -> + Value1 = binarize(Value0), + Value = case Modifier of + {prefix_modifier, MaxLen} -> + string:slice(Value1, 0, MaxLen); + no_modifier -> + Value1 + end, + [ + Name, + case Value of + <<>> when Trim =:= trim -> + []; + <<>> when Trim =:= no_trim -> + $=; + _ -> + [ + $=, + urlencode(Value, AllowedChars) + ] + end + ]. + +binarize(Value) when is_integer(Value) -> + integer_to_binary(Value); +binarize(Value) when is_float(Value) -> + float_to_binary(Value, [{decimals, 10}, compact]); +binarize(Value) -> + Value. + +urlencode(Value, unreserved) -> + urlencode_unreserved(Value, <<>>); +urlencode(Value, reserved) -> + urlencode_reserved(Value, <<>>). + +urlencode_unreserved(<>, Acc) + when ?IS_URI_UNRESERVED(C) -> + urlencode_unreserved(R, <>); +urlencode_unreserved(<>, Acc) -> + urlencode_unreserved(R, <>); +urlencode_unreserved(<<>>, Acc) -> + Acc. + +urlencode_reserved(<>, Acc) + when ?IS_URI_UNRESERVED(C) or ?IS_URI_GEN_DELIMS(C) or ?IS_URI_SUB_DELIMS(C) -> + urlencode_reserved(R, <>); +urlencode_reserved(<>, Acc) -> + urlencode_reserved(R, <>); +urlencode_reserved(<<>>, Acc) -> + Acc. + +-ifdef(TEST). +expand_uritemplate_test_() -> + Files = filelib:wildcard("deps/uritemplate-tests/*.json"), + lists:flatten([begin + {ok, JSON} = file:read_file(File), + Tests = jsx:decode(JSON, [return_maps]), + [begin + %% Erlang doesn't have a NULL value. + Vars = maps:remove(<<"undef">>, Vars0), + [ + {iolist_to_binary(io_lib:format("~s - ~s: ~s => ~s", + [filename:basename(File), Section, URITemplate, + if + is_list(Expected) -> lists:join(<<" OR ">>, Expected); + true -> Expected + end + ])), + fun() -> + case Expected of + false -> + {'EXIT', _} = (catch expand(URITemplate, Vars)); + [_|_] -> + Result = iolist_to_binary(expand(URITemplate, Vars)), + io:format("~p", [Result]), + true = lists:member(Result, Expected); + _ -> + Expected = iolist_to_binary(expand(URITemplate, Vars)) + end + end} + || [URITemplate, Expected] <- Cases] + end || {Section, #{ + <<"variables">> := Vars0, + <<"testcases">> := Cases + }} <- maps:to_list(Tests)] + end || File <- Files]). +-endif. diff --git a/src/cow_uri_templates.erl b/src/cow_uri_templates.erl deleted file mode 100644 index 32ca07c..0000000 --- a/src/cow_uri_templates.erl +++ /dev/null @@ -1,356 +0,0 @@ -%% Copyright (c) 2019, Loïc Hoguin -%% -%% Permission to use, copy, modify, and/or distribute this software for any -%% purpose with or without fee is hereby granted, provided that the above -%% copyright notice and this permission notice appear in all copies. -%% -%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -%% This is a full level 4 implementation of URI Templates -%% as defined by RFC6570. - --module(cow_uri_templates). - --export([parse/1]). --export([expand/2]). - --type op() :: simple_string_expansion - | reserved_expansion - | fragment_expansion - | label_expansion_with_dot_prefix - | path_segment_expansion - | path_style_parameter_expansion - | form_style_query_expansion - | form_style_query_continuation. - --type var_list() :: [ - {no_modifier, binary()} - | {{prefix_modifier, pos_integer()}, binary()} - | {explode_modifier, binary()} -]. - --type uri_template() :: [ - binary() | {expr, op(), var_list()} -]. --export_type([uri_template/0]). - --type variables() :: #{ - binary() => binary() - | integer() - | float() - | [binary()] - | #{binary() => binary()} -}. - --include("cow_inline.hrl"). --include("cow_parse.hrl"). - -%% Parse a URI template. - --spec parse(binary()) -> uri_template(). -parse(URITemplate) -> - parse(URITemplate, <<>>). - -parse(<<>>, <<>>) -> - []; -parse(<<>>, Acc) -> - [Acc]; -parse(<<${,R/bits>>, <<>>) -> - parse_expr(R); -parse(<<${,R/bits>>, Acc) -> - [Acc|parse_expr(R)]; -%% @todo Probably should reject unallowed characters so that -%% we don't produce invalid URIs. -parse(<>, Acc) when C =/= $} -> - parse(R, <>). - -parse_expr(<<$+,R/bits>>) -> - parse_var_list(R, reserved_expansion, []); -parse_expr(<<$#,R/bits>>) -> - parse_var_list(R, fragment_expansion, []); -parse_expr(<<$.,R/bits>>) -> - parse_var_list(R, label_expansion_with_dot_prefix, []); -parse_expr(<<$/,R/bits>>) -> - parse_var_list(R, path_segment_expansion, []); -parse_expr(<<$;,R/bits>>) -> - parse_var_list(R, path_style_parameter_expansion, []); -parse_expr(<<$?,R/bits>>) -> - parse_var_list(R, form_style_query_expansion, []); -parse_expr(<<$&,R/bits>>) -> - parse_var_list(R, form_style_query_continuation, []); -parse_expr(R) -> - parse_var_list(R, simple_string_expansion, []). - -parse_var_list(<>, Op, List) - when ?IS_ALPHANUM(C) or (C =:= $_) -> - parse_varname(R, Op, List, <>). - -parse_varname(<>, Op, List, Name) - when ?IS_ALPHANUM(C) or (C =:= $_) or (C =:= $.) or (C =:= $%) -> - parse_varname(R, Op, List, <>); -parse_varname(<<$:,C,R/bits>>, Op, List, Name) - when (C =:= $1) or (C =:= $2) or (C =:= $3) or (C =:= $4) or (C =:= $5) - or (C =:= $6) or (C =:= $7) or (C =:= $8) or (C =:= $9) -> - parse_prefix_modifier(R, Op, List, Name, <>); -parse_varname(<<$*,$,,R/bits>>, Op, List, Name) -> - parse_var_list(R, Op, [{explode_modifier, Name}|List]); -parse_varname(<<$*,$},R/bits>>, Op, List, Name) -> - [{expr, Op, lists:reverse([{explode_modifier, Name}|List])}|parse(R, <<>>)]; -parse_varname(<<$,,R/bits>>, Op, List, Name) -> - parse_var_list(R, Op, [{no_modifier, Name}|List]); -parse_varname(<<$},R/bits>>, Op, List, Name) -> - [{expr, Op, lists:reverse([{no_modifier, Name}|List])}|parse(R, <<>>)]. - -parse_prefix_modifier(<>, Op, List, Name, Acc) - when ?IS_DIGIT(C), byte_size(Acc) < 4 -> - parse_prefix_modifier(R, Op, List, Name, <>); -parse_prefix_modifier(<<$,,R/bits>>, Op, List, Name, Acc) -> - parse_var_list(R, Op, [{{prefix_modifier, binary_to_integer(Acc)}, Name}|List]); -parse_prefix_modifier(<<$},R/bits>>, Op, List, Name, Acc) -> - [{expr, Op, lists:reverse([{{prefix_modifier, binary_to_integer(Acc)}, Name}|List])}|parse(R, <<>>)]. - -%% Expand a URI template (after parsing it if necessary). - --spec expand(binary() | uri_template(), variables()) -> iodata(). -expand(URITemplate, Vars) when is_binary(URITemplate) -> - expand(parse(URITemplate), Vars); -expand(URITemplate, Vars) -> - expand1(URITemplate, Vars). - -expand1([], _) -> - []; -expand1([Literal|Tail], Vars) when is_binary(Literal) -> - [Literal|expand1(Tail, Vars)]; -expand1([{expr, simple_string_expansion, VarList}|Tail], Vars) -> - [simple_string_expansion(VarList, Vars)|expand1(Tail, Vars)]; -expand1([{expr, reserved_expansion, VarList}|Tail], Vars) -> - [reserved_expansion(VarList, Vars)|expand1(Tail, Vars)]; -expand1([{expr, fragment_expansion, VarList}|Tail], Vars) -> - [fragment_expansion(VarList, Vars)|expand1(Tail, Vars)]; -expand1([{expr, label_expansion_with_dot_prefix, VarList}|Tail], Vars) -> - [label_expansion_with_dot_prefix(VarList, Vars)|expand1(Tail, Vars)]; -expand1([{expr, path_segment_expansion, VarList}|Tail], Vars) -> - [path_segment_expansion(VarList, Vars)|expand1(Tail, Vars)]; -expand1([{expr, path_style_parameter_expansion, VarList}|Tail], Vars) -> - [path_style_parameter_expansion(VarList, Vars)|expand1(Tail, Vars)]; -expand1([{expr, form_style_query_expansion, VarList}|Tail], Vars) -> - [form_style_query_expansion(VarList, Vars)|expand1(Tail, Vars)]; -expand1([{expr, form_style_query_continuation, VarList}|Tail], Vars) -> - [form_style_query_continuation(VarList, Vars)|expand1(Tail, Vars)]. - -simple_string_expansion(VarList, Vars) -> - lists:join($,, [ - apply_modifier(Modifier, unreserved, $,, Value) - || {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]). - -reserved_expansion(VarList, Vars) -> - lists:join($,, [ - apply_modifier(Modifier, reserved, $,, Value) - || {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]). - -fragment_expansion(VarList, Vars) -> - case reserved_expansion(VarList, Vars) of - [] -> []; - Expanded -> [$#, Expanded] - end. - -label_expansion_with_dot_prefix(VarList, Vars) -> - segment_expansion(VarList, Vars, $.). - -path_segment_expansion(VarList, Vars) -> - segment_expansion(VarList, Vars, $/). - -segment_expansion(VarList, Vars, Sep) -> - Expanded = lists:join(Sep, [ - apply_modifier(Modifier, unreserved, Sep, Value) - || {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]), - case Expanded of - [] -> []; - [[]] -> []; - _ -> [Sep, Expanded] - end. - -path_style_parameter_expansion(VarList, Vars) -> - parameter_expansion(VarList, Vars, $;, $;, trim). - -form_style_query_expansion(VarList, Vars) -> - parameter_expansion(VarList, Vars, $?, $&, no_trim). - -form_style_query_continuation(VarList, Vars) -> - parameter_expansion(VarList, Vars, $&, $&, no_trim). - -parameter_expansion(VarList, Vars, LeadingSep, Sep, Trim) -> - Expanded = lists:join(Sep, [ - apply_parameter_modifier(Modifier, unreserved, Sep, Trim, Name, Value) - || {Modifier, Name, Value} <- lookup_variables(VarList, Vars)]), - case Expanded of - [] -> []; - [[]] -> []; - _ -> [LeadingSep, Expanded] - end. - -lookup_variables([], _) -> - []; -lookup_variables([{Modifier, Name}|Tail], Vars) -> - case Vars of - #{Name := Value} -> [{Modifier, Name, Value}|lookup_variables(Tail, Vars)]; - _ -> lookup_variables(Tail, Vars) - end. - -apply_modifier(no_modifier, AllowedChars, _, List) when is_list(List) -> - lists:join($,, [urlencode(Value, AllowedChars) || Value <- List]); -apply_modifier(explode_modifier, AllowedChars, ExplodeSep, List) when is_list(List) -> - lists:join(ExplodeSep, [urlencode(Value, AllowedChars) || Value <- List]); -apply_modifier(Modifier, AllowedChars, ExplodeSep, Map) when is_map(Map) -> - {JoinSep, KVSep} = case Modifier of - no_modifier -> {$,, $,}; - explode_modifier -> {ExplodeSep, $=} - end, - lists:reverse(lists:join(JoinSep, - maps:fold(fun(Key, Value, Acc) -> - [[ - urlencode(Key, AllowedChars), - KVSep, - urlencode(Value, AllowedChars) - ]|Acc] - end, [], Map) - )); -apply_modifier({prefix_modifier, MaxLen}, AllowedChars, _, Value) -> - urlencode(string:slice(binarize(Value), 0, MaxLen), AllowedChars); -apply_modifier(_, AllowedChars, _, Value) -> - urlencode(binarize(Value), AllowedChars). - -apply_parameter_modifier(_, _, _, _, _, []) -> - []; -apply_parameter_modifier(_, _, _, _, _, Map) when Map =:= #{} -> - []; -apply_parameter_modifier(no_modifier, AllowedChars, _, _, Name, List) when is_list(List) -> - [ - Name, - $=, - lists:join($,, [urlencode(Value, AllowedChars) || Value <- List]) - ]; -apply_parameter_modifier(explode_modifier, AllowedChars, ExplodeSep, _, Name, List) when is_list(List) -> - lists:join(ExplodeSep, [[ - Name, - $=, - urlencode(Value, AllowedChars) - ] || Value <- List]); -apply_parameter_modifier(Modifier, AllowedChars, ExplodeSep, _, Name, Map) when is_map(Map) -> - {JoinSep, KVSep} = case Modifier of - no_modifier -> {$,, $,}; - explode_modifier -> {ExplodeSep, $=} - end, - [ - case Modifier of - no_modifier -> - [ - Name, - $= - ]; - explode_modifier -> - [] - end, - lists:reverse(lists:join(JoinSep, - maps:fold(fun(Key, Value, Acc) -> - [[ - urlencode(Key, AllowedChars), - KVSep, - urlencode(Value, AllowedChars) - ]|Acc] - end, [], Map) - )) - ]; -apply_parameter_modifier(Modifier, AllowedChars, _, Trim, Name, Value0) -> - Value1 = binarize(Value0), - Value = case Modifier of - {prefix_modifier, MaxLen} -> - string:slice(Value1, 0, MaxLen); - no_modifier -> - Value1 - end, - [ - Name, - case Value of - <<>> when Trim =:= trim -> - []; - <<>> when Trim =:= no_trim -> - $=; - _ -> - [ - $=, - urlencode(Value, AllowedChars) - ] - end - ]. - -binarize(Value) when is_integer(Value) -> - integer_to_binary(Value); -binarize(Value) when is_float(Value) -> - float_to_binary(Value, [{decimals, 10}, compact]); -binarize(Value) -> - Value. - -urlencode(Value, unreserved) -> - urlencode_unreserved(Value, <<>>); -urlencode(Value, reserved) -> - urlencode_reserved(Value, <<>>). - -urlencode_unreserved(<>, Acc) - when ?IS_URI_UNRESERVED(C) -> - urlencode_unreserved(R, <>); -urlencode_unreserved(<>, Acc) -> - urlencode_unreserved(R, <>); -urlencode_unreserved(<<>>, Acc) -> - Acc. - -urlencode_reserved(<>, Acc) - when ?IS_URI_UNRESERVED(C) or ?IS_URI_GEN_DELIMS(C) or ?IS_URI_SUB_DELIMS(C) -> - urlencode_reserved(R, <>); -urlencode_reserved(<>, Acc) -> - urlencode_reserved(R, <>); -urlencode_reserved(<<>>, Acc) -> - Acc. - --ifdef(TEST). -expand_uritemplate_test_() -> - Files = filelib:wildcard("deps/uritemplate-tests/*.json"), - lists:flatten([begin - {ok, JSON} = file:read_file(File), - Tests = jsx:decode(JSON, [return_maps]), - [begin - %% Erlang doesn't have a NULL value. - Vars = maps:remove(<<"undef">>, Vars0), - [ - {iolist_to_binary(io_lib:format("~s - ~s: ~s => ~s", - [filename:basename(File), Section, URITemplate, - if - is_list(Expected) -> lists:join(<<" OR ">>, Expected); - true -> Expected - end - ])), - fun() -> - case Expected of - false -> - {'EXIT', _} = (catch expand(URITemplate, Vars)); - [_|_] -> - Result = iolist_to_binary(expand(URITemplate, Vars)), - io:format("~p", [Result]), - true = lists:member(Result, Expected); - _ -> - Expected = iolist_to_binary(expand(URITemplate, Vars)) - end - end} - || [URITemplate, Expected] <- Cases] - end || {Section, #{ - <<"variables">> := Vars0, - <<"testcases">> := Cases - }} <- maps:to_list(Tests)] - end || File <- Files]). --endif. -- cgit v1.2.3