├── .gitattributes ├── .github └── workflows │ └── erlang.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── elvis.config ├── erlang.mk ├── rebar.config ├── rebar.config.script ├── rebar.lock ├── restc.d ├── src ├── restc.app.src ├── restc.erl ├── restc_body.erl └── restc_util.erl └── test ├── mochiweb_util.erl ├── prop_restc.erl ├── prop_restc_body.erl └── restc_SUITE.erl /.gitattributes: -------------------------------------------------------------------------------- 1 | erlang.mk -diff -------------------------------------------------------------------------------- /.github/workflows/erlang.yml: -------------------------------------------------------------------------------- 1 | name: Erlang CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | 7 | build_and_test: 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: erlef/setup-beam@v1.16.0 14 | with: 15 | otp-version: "24" 16 | rebar3-version: "3.20.0" 17 | - name: Compile 18 | run: rebar3 compile 19 | - name: Run tests 20 | run: rebar3 ct 21 | - name: Run xref 22 | run: rebar3 xref 23 | - name: Run elvis 24 | run: make elvis_rock 25 | - name: Run proper tests 26 | run: rebar3 proper -n 1000 27 | 28 | dialyze: 29 | 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: erlef/setup-beam@v1.16.0 35 | with: 36 | otp-version: "24" 37 | rebar3-version: "3.20.0" 38 | - name: Run dialyzer 39 | run: rebar3 dialyzer 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ebin 2 | deps/ 3 | .dialyzer_plt 4 | erl_crash.dump 5 | .eunit 6 | .rebar 7 | *.plt 8 | .erlang.mk 9 | _build 10 | *.crashdump 11 | *.beam 12 | .elvis 13 | .rebar3 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 KIVRA 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = restc 2 | 3 | # Dependencies ########################################################## 4 | DEPS = hackney jsx erlsom 5 | 6 | dep_hackney = hex 1.20.1 7 | dep_jsx = hex 3.1.0 8 | dep_erlsom = hex 1.5.1 9 | 10 | # Standard targets ##################################################### 11 | include erlang.mk 12 | 13 | app:: rebar.config 14 | 15 | ELVIS_IN_PATH := $(shell elvis --version 2> /dev/null) 16 | ELVIS_LOCAL := $(shell .elvis/_build/default/bin/elvis --version 2> /dev/null) 17 | 18 | elvis_rock: 19 | ifdef ELVIS_IN_PATH 20 | elvis rock 21 | else ifdef ELVIS_LOCAL 22 | .elvis/_build/default/bin/elvis rock 23 | else 24 | $(MAKE) compile_elvis 25 | .elvis/_build/default/bin/elvis rock 26 | endif 27 | 28 | compile_elvis: 29 | git clone https://github.com/inaka/elvis.git --branch 1.0.1 --single-branch .elvis && \ 30 | cd .elvis && \ 31 | rebar3 compile && \ 32 | rebar3 escriptize && \ 33 | cd .. 34 | 35 | # eof 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Hex.pm](https://img.shields.io/hexpm/v/restc.svg?maxAge=2592000)](https://hex.pm/packages/restc) restclient -- An erlang REST Client library 2 | ==================================== 3 | 4 | ## DESCRIPTION 5 | 6 | restclient is a library to help with consuming RESTful web services. It supports 7 | encoding and decoding JSON, Percent and XML and comes with a convenience 8 | function for working with urls and query parameters. 9 | 10 | ## USAGE 11 | 12 | Include restclient as a rebar dependency with: 13 | 14 | {deps, [{restc, ".*", {git, "git://github.com/kivra/restclient.git", {tag, "0.8.2"}}}]}. 15 | 16 | You have to start inets before using the client and if you want to use https make sure to start ssl before. 17 | Then you can use the client as: 18 | 19 | ``` erlang 20 | Erlang/OTP 19 [erts-8.2] [source] [64-bit] [smp:8:8] [async-threads:0] [kernel-poll:false] 21 | 22 | Eshell V8.2 (abort with ^G) 23 | 1> application:ensure_all_started(restc). 24 | {ok,[idna,mimerl,certifi,ssl_verify_fun,metrics,hackney, 25 | mochiweb_util,restc]} 26 | 27 | 2> restc:request(get, "https://api.github.com"). 28 | {ok,200, 29 | [{<<"Server">>,<<"GitHub.com">>}, 30 | {<<"Date">>,<<"Thu, 11 May 2017 07:36:16 GMT">>}, 31 | {<<"Content-Type">>,<<"application/json; charset=utf-8">>}, 32 | {<<"Content-Length">>,<<"2039">>}, 33 | {<<"Status">>,<<"200 OK">>}, 34 | {<<"X-GitHub-Req"...>>,<<"8E05:5C9"...>>}], 35 | [{<<"current_user_url">>,<<"https://api.github.com/user">>}, 36 | {<<"current_user_authorizations_html_url">>, 37 | <<"https://github.com/settings/connections/applications{/client_id}">>}, 38 | {<<"authorizations_url">>, 39 | <<"https://api.github.com/authorizations">>}, 40 | {<<...>>,...}, 41 | {...}|...]} 42 | 3> restc:request(get, "https://api.github.com/herp-derp-404", [200]). 43 | {error,404, 44 | [{<<"Server">>,<<"GitHub.com">>}, 45 | {<<"Date">>,<<"Thu, 11 May 2017 07:37:27 GMT">>}, 46 | {<<"Content-Type">>,<<"application/json; charset=utf-8">>}, 47 | {<<"Content-Length">>,<<"77">>}, 48 | {<<"Status">>,<<"404 Not Found">>}, 49 | {<<"X-RateLimit-Limit">>,<<"60">>}, 50 | {<<"X-RateLimit-Remaining">>,<<"56">>}, 51 | {<<"X-RateLimit-Reset">>,<<"1494491776">>}, 52 | {<<"X-GitHub-Media-Type">>,<<"github.v3">>}, 53 | {<<"Access-Control-Expose-Headers">>, 54 | <<"ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit"...>>}, 55 | {<<"Access-Control-Allow-Origin">>,<<"*">>}, 56 | {<<"Content-Security-Policy">>,<<"default-src 'none'">>}, 57 | {<<"Strict-Transport-Security">>, 58 | <<"max-age=31536000; includeSubdomains; preload">>}, 59 | {<<"X-Content-Type-Options">>,<<"nosniff">>}, 60 | {<<"X-Frame-Options">>,<<"deny">>}, 61 | {<<"X-XSS-Protection">>,<<"1; mode=block">>}, 62 | {<<"X-GitHub-Request-Id">>, 63 | <<"8C1D:5C90:54F34B8:6C6FF4D:59"...>>}], 64 | [{<<"message">>,<<"Not Found">>}, 65 | {<<"documentation_url">>, 66 | <<"https://developer.github.com/v3">>}]} 67 | 68 | ``` 69 | 70 | There's also convenience functions for working with urls and query string: 71 | 72 | ``` erlang 73 | 7> restc:construct_url("http://www.example.com/te", "res/res1/res2", [{"q1", "qval1"}, {"q2", "qval2"}]). 74 | "http://www.example.com/te/res/res1/res2?q1=qval1&q2=qval2" 75 | ``` 76 | 77 | ## License 78 | The KIVRA restclient library uses an [MIT license](http://en.wikipedia.org/wiki/MIT_License). So go ahead and do what 79 | you want! 80 | 81 | Lots of fun! 82 | -------------------------------------------------------------------------------- /elvis.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | [ 3 | {elvis, [ 4 | {config, [ 5 | #{ 6 | dirs => ["src"], 7 | filter => "*.erl", 8 | ruleset => erl_files, 9 | ignore => [], 10 | rules => [ 11 | {elvis_text_style, line_length, #{ 12 | limit => 80, 13 | skip_comments => false 14 | }}, 15 | {elvis_text_style, no_tabs}, 16 | {elvis_text_style, no_trailing_whitespace}, 17 | {elvis_style, macro_module_names}, 18 | {elvis_style, nesting_level, #{ 19 | level => 3, 20 | ignore => [] 21 | }}, 22 | {elvis_style, god_modules, #{ 23 | limit => 25, 24 | ignore => [] 25 | }}, 26 | {elvis_style, no_nested_try_catch, #{ignore => []}}, 27 | {elvis_style, invalid_dynamic_call, #{ignore => []}}, 28 | {elvis_style, used_ignored_variable}, 29 | {elvis_style, no_behavior_info}, 30 | {elvis_style, module_naming_convention, #{ 31 | ignore => [], 32 | regex => "^([a-z][a-z0-9]*_?)([a-z0-9]*_?)*$" 33 | }}, 34 | {elvis_style, function_naming_convention, #{ 35 | regex => "^([a-z][a-z0-9]*_?)([a-z0-9]*_?)*$" 36 | }}, 37 | {elvis_style, variable_naming_convention, #{ 38 | regex => "^_?([A-Z][0-9a-zA-Z_]*)$" 39 | }}, 40 | {elvis_style, state_record_and_type}, 41 | {elvis_style, no_spec_with_records}, 42 | {elvis_style, dont_repeat_yourself, #{ 43 | min_complexity => 25, 44 | ignore => [] 45 | }} 46 | ] 47 | }, 48 | #{ 49 | dirs => ["test"], 50 | filter => "*.erl", 51 | rules => [ 52 | {elvis_text_style, line_length, #{ 53 | limit => 100, 54 | skip_comments => false 55 | }}, 56 | {elvis_text_style, no_tabs}, 57 | {elvis_text_style, no_trailing_whitespace}, 58 | {elvis_style, macro_module_names}, 59 | {elvis_style, no_debug_call, #{ignore => []}} 60 | ] 61 | } 62 | ]} 63 | ]} 64 | ]. 65 | 66 | %%%_* Emacs ==================================================================== 67 | %%% Local Variables: 68 | %%% allout-layout: t 69 | %%% erlang-indent-level: 2 70 | %%% End: 71 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [{hackney,"1.20.1"}, 2 | {jsx,"v3.1.0"}, 3 | {erlsom,"1.5.1"} 4 | ]}. 5 | 6 | {project_plugins, [rebar3_proper, erlfmt]}. 7 | 8 | {erlfmt, [ 9 | write, 10 | {print_width, 80} % same as in elvis.config 11 | ]}. 12 | 13 | {profiles, 14 | [{test, [ 15 | {erl_opts, [nowarn_export_all]}, 16 | {deps, [ proper 17 | , {meck, "0.8.13"} 18 | ]} 19 | ]} 20 | ]}. 21 | 22 | {xref_checks,[undefined_function_calls,undefined_functions,locals_not_used, 23 | deprecated_function_calls,deprecated_functions]}. 24 | 25 | {erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard]}. 26 | 27 | {dialyzer, [{plt_extra_apps, [xmerl, erlsom]}]}. 28 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | case erlang:function_exported(rebar3, main, 1) of 2 | true -> % rebar3 3 | CONFIG; 4 | false -> % rebar 2.x or older 5 | %% Rebuild deps, possibly including those that have been moved to 6 | %% profiles 7 | [{deps, [ 8 | {hackney, ".*", {git, "https://github.com/benoitc/hackney.git", {tag, "1.20.1"}}}, 9 | {jsx, ".*", {git, "https://github.com/talentdeficit/jsx.git", {tag, "v3.1.0"}}}, 10 | {erlsom, ".*", {git, "https://github.com/willemdj/erlsom.git", {tag, "1.5.1"}}} 11 | ]} | [Config || {Key, _Value}=Config <- CONFIG, Key =/= deps]] 12 | end. 13 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"certifi">>,{pkg,<<"certifi">>,<<"2.6.1">>},1}, 3 | {<<"erlsom">>,{pkg,<<"erlsom">>,<<"1.5.1">>},0}, 4 | {<<"hackney">>,{pkg,<<"hackney">>,<<"1.20.1">>},0}, 5 | {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},1}, 6 | {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0}, 7 | {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1}, 8 | {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},1}, 9 | {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.1">>},1}, 10 | {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},1}, 11 | {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},1}]}. 12 | [ 13 | {pkg_hash,[ 14 | {<<"certifi">>, <<"DBAB8E5E155A0763EEA978C913CA280A6B544BFA115633FA20249C3D396D9493">>}, 15 | {<<"erlsom">>, <<"C8FE2BABD33FF0846403F6522328B8AB676F896B793634CFE7EF181C05316C03">>}, 16 | {<<"hackney">>, <<"8D97AEC62DDDDD757D128BFD1DF6C5861093419F8F7A4223823537BAD5D064E2">>}, 17 | {<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>}, 18 | {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}, 19 | {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, 20 | {<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>}, 21 | {<<"parse_trans">>, <<"16328AB840CC09919BD10DAB29E431DA3AF9E9E7E7E6F0089DD5A2D2820011D8">>}, 22 | {<<"ssl_verify_fun">>, <<"CF344F5692C82D2CD7554F5EC8FD961548D4FD09E7D22F5B62482E5AEAEBD4B0">>}, 23 | {<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]}, 24 | {pkg_hash_ext,[ 25 | {<<"certifi">>, <<"524C97B4991B3849DD5C17A631223896272C6B0AF446778BA4675A1DFF53BB7E">>}, 26 | {<<"erlsom">>, <<"7965485494C5844DD127656AC40F141AADFA174839EC1BE1074E7EDF5B4239EB">>}, 27 | {<<"hackney">>, <<"FE9094E5F1A2A2C0A7D10918FEE36BFEC0EC2A979994CFF8CFE8058CD9AF38E3">>}, 28 | {<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>}, 29 | {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}, 30 | {<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>}, 31 | {<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>}, 32 | {<<"parse_trans">>, <<"07CD9577885F56362D414E8C4C4E6BDF10D43A8767ABB92D24CBE8B24C54888B">>}, 33 | {<<"ssl_verify_fun">>, <<"BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680">>}, 34 | {<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]} 35 | ]. 36 | -------------------------------------------------------------------------------- /restc.d: -------------------------------------------------------------------------------- 1 | 2 | COMPILE_FIRST += 3 | -------------------------------------------------------------------------------- /src/restc.app.src: -------------------------------------------------------------------------------- 1 | % vim: ft=erlang: 2 | %% ---------------------------------------------------------------------------- 3 | %% 4 | %% restc: Erlang Rest Client 5 | %% 6 | %% Copyright (c) 2012-2019 KIVRA 7 | %% 8 | %% Permission is hereby granted, free of charge, to any person obtaining a 9 | %% copy of this software and associated documentation files (the "Software"), 10 | %% to deal in the Software without restriction, including without limitation 11 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | %% and/or sell copies of the Software, and to permit persons to whom the 13 | %% Software is furnished to do so, subject to the following conditions: 14 | %% 15 | %% The above copyright notice and this permission notice shall be included in 16 | %% all copies or substantial portions of the Software. 17 | %% 18 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | %% DEALINGS IN THE SOFTWARE. 25 | %% 26 | %% ---------------------------------------------------------------------------- 27 | {application, restc, [ {description, "Erlang Rest Client"} 28 | , {vsn, git} 29 | , {applications, [ kernel 30 | , stdlib 31 | , asn1 32 | , crypto 33 | , public_key 34 | , ssl 35 | , hackney 36 | , jsx 37 | ]} 38 | , {env, []} 39 | , {registered, []} 40 | , {modules, []} 41 | , {maintainers, ["kivra"]} 42 | , {licenses, ["MIT"]} 43 | , {links, [{"GitHub", "https://github.com/kivra/restclient"}]} 44 | ]}. 45 | 46 | %%% eof 47 | -------------------------------------------------------------------------------- /src/restc.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% restc: Erlang Rest Client 4 | %% 5 | %% Copyright (c) 2012-2019 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | -module(restc). 28 | 29 | -export([request/1]). 30 | -export([request/2]). 31 | -export([request/3]). 32 | -export([request/4]). 33 | -export([request/5]). 34 | -export([request/6]). 35 | -export([request/7]). 36 | 37 | -export([construct_url/2]). 38 | -export([construct_url/3]). 39 | -export([construct_url/4]). 40 | 41 | -type method() :: head | 42 | get | 43 | put | 44 | patch | 45 | post | 46 | trace | 47 | options | 48 | delete. 49 | -type url() :: binary() | string(). 50 | -type headers() :: [header()]. 51 | -type header() :: {binary(), binary()}. 52 | -type options() :: [option()]. 53 | -type option() :: {atom(), term()} | atom(). 54 | -type querys() :: [qry()]. 55 | -type qry() :: {string(), string()}. 56 | -type status_codes() :: [status_code()]. 57 | -type status_code() :: integer(). 58 | -type reason() :: term(). 59 | -type content_type() :: multi | json | xml | percent | png. 60 | -type body() :: binary() | 61 | jsx:json_term() | 62 | tuple() | % check erlsom:simple_form/1,2 63 | multi_body(). 64 | -type multi_part() :: {Name::binary(), Value::binary()} | 65 | { file 66 | , Path::binary() 67 | , Name::binary() 68 | , Headers::headers()}. 69 | -type multi_body() :: [multi_part()]. 70 | -type response() :: { ok, Status::status_code() 71 | , Headers::headers() 72 | , Body::body()} | 73 | { error, Status::status_code() 74 | , Headers::headers() 75 | , Body::body()} | 76 | { error, Reason::reason()}. 77 | 78 | -define(DEFAULT_ENCODING, json). 79 | -define(DEFAULT_CTYPE, <<"application/json">>). 80 | 81 | -export_type([ method/0 82 | , url/0 83 | , headers/0 84 | , header/0 85 | , options/0 86 | , option/0 87 | , querys/0 88 | , qry/0 89 | , status_codes/0 90 | , status_code/0 91 | , reason/0 92 | , content_type/0 93 | , body/0 94 | , response/0 95 | ]). 96 | 97 | -import(restc_util, [ string_to_binary/1]). 98 | 99 | %%% API ======================================================================== 100 | 101 | -spec request(Url::url()) -> Response::response(). 102 | request(Url) -> 103 | request(get, Url). 104 | 105 | -spec request(Method::method(), Url::url()) -> Response::response(). 106 | request(Method, Url) -> 107 | request(Method, Url, []). 108 | 109 | -spec request(Method::method(), 110 | Url::url(), 111 | Expect::status_codes()) -> Response::response(). 112 | request(Method, Url, Expect) -> 113 | request(Method, ?DEFAULT_ENCODING, Url, Expect). 114 | 115 | -spec request(Method::method(), 116 | Type::content_type(), 117 | Url::url(), 118 | Expect::status_codes()) -> Response::response(). 119 | request(Method, Type, Url, Expect) -> 120 | request(Method, Type, Url, Expect, []). 121 | 122 | -spec request(Method::method(), 123 | Type::content_type(), 124 | Url::url(), 125 | Expect::status_codes(), 126 | Headers::headers()) -> Response::response(). 127 | request(Method, Type, Url, Expect, Headers) -> 128 | request(Method, Type, Url, Expect, Headers, []). 129 | 130 | -spec request(Method::method(), 131 | Type::content_type(), 132 | Url::url(), 133 | Expect::status_codes(), 134 | Headers::headers(), 135 | Body::body()) -> Response::response(). 136 | request(Method, Type, Url, Expect, Headers, Body) -> 137 | request(Method, Type, Url, Expect, Headers, Body, []). 138 | 139 | -spec request(Method::method(), 140 | Type::content_type(), 141 | Url::url(), 142 | Expect::status_codes(), 143 | Headers::headers(), 144 | Body::body(), 145 | Options::options()) -> Response::response(). 146 | request(Method, Type, Url, Expect, Headers0, Body, Options) -> 147 | Headers1 = normalize_headers(Headers0), 148 | Headers = lists:usort([ accept(Headers1, Type) 149 | , content_type(Headers1, Type) | Headers1]), 150 | Retries = proplists:get_value(retries, Options, 0), 151 | request_loop(Method, Type, Url, Expect, Headers, Body, Options, Retries). 152 | 153 | request_loop(Method, Type, Url, Expect, Headers, Body, Options, Retries) -> 154 | Response = 155 | parse_response( 156 | do_request(Method, Type, Url, Headers, Body, Options), Options), 157 | case Response of 158 | {ok, Status, H, B} -> 159 | case check_expect(Status, Expect) of 160 | true -> Response; 161 | false when Retries > 0 -> 162 | request_loop(Method, Type, Url, Expect, Headers, Body, Options, 163 | Retries-1); 164 | false -> 165 | {error, Status, H, B} 166 | end; 167 | _Error when Retries > 0 -> 168 | request_loop(Method, Type, Url, Expect, Headers, Body, Options, 169 | Retries-1); 170 | Error -> 171 | Error 172 | end. 173 | 174 | -spec construct_url(FullPath::url(), Query::querys()) -> Url::url(). 175 | construct_url(FullPath, Query) -> 176 | construct_url(FullPath, <<>>, Query). 177 | 178 | -spec construct_url(BaseUrl::url(), 179 | Path::url(), 180 | Query::querys()) -> Url::url(). 181 | construct_url(BaseUrl, Path, Query) -> 182 | construct_url(BaseUrl, Path, Query, []). 183 | 184 | -spec construct_url(BaseUrl::url(), 185 | Path::url(), 186 | Query::querys(), 187 | Options::[option()]) -> Url::url(). 188 | construct_url(BaseUrl, Path, Query, Options) -> 189 | BaseUrlBin = string_to_binary(BaseUrl), 190 | PathBin = string_to_binary(Path), 191 | QueryBin = lists:map(fun({K, V}) -> 192 | {string_to_binary(K), string_to_binary(V)} 193 | end, Query), 194 | UrlBin = hackney_url:make_url(BaseUrlBin, PathBin, QueryBin), 195 | case Options of 196 | [return_binary] -> UrlBin; 197 | [] -> binary_to_list(UrlBin) 198 | end. 199 | 200 | %%% INTERNAL =================================================================== 201 | normalize_headers(Headers) -> 202 | lists:map(fun({Key, Val}) -> 203 | {string:lowercase(Key), Val} 204 | end, Headers). 205 | 206 | accept(Headers, Type) -> 207 | case lists:keyfind(<<"accept">>, 1, Headers) of 208 | {<<"accept">>, Accept} -> {<<"accept">>, Accept}; 209 | false -> default_accept(Type) 210 | end. 211 | 212 | default_accept(Type) -> 213 | AccessType = get_accesstype(Type), 214 | {<<"accept">>, <>}. 215 | 216 | content_type(Headers, Type) -> 217 | case lists:keyfind(<<"content-type">>, 1, Headers) of 218 | {<<"content-type">>, ContentType} -> {<<"content-type">>, ContentType}; 219 | false -> default_content_type(Type) 220 | end. 221 | 222 | default_content_type(Type) -> 223 | {<<"content-type">>, get_ctype(Type)}. 224 | 225 | do_request(post, json = _Type, Url, Headers, [] = _Body, Options) -> 226 | hackney:request(post, Url, Headers, [], Options); 227 | do_request(post, Type, Url, Headers, Body, Options) -> 228 | EncodedBody = restc_body:encode(Type, Body), 229 | hackney:request(post, Url, Headers, EncodedBody, Options); 230 | do_request(put, Type, Url, Headers, Body, Options) -> 231 | EncodedBody = restc_body:encode(Type, Body), 232 | hackney:request(put, Url, Headers, EncodedBody, Options); 233 | do_request(patch, Type, Url, Headers, Body, Options) -> 234 | EncodedBody = restc_body:encode(Type, Body), 235 | hackney:request(patch, Url, Headers, EncodedBody, Options); 236 | do_request(Method, _, Url, Headers, _, Options) when is_atom(Method) -> 237 | hackney:request(Method, Url, Headers, [], Options). 238 | 239 | check_expect(_Status, []) -> 240 | true; 241 | check_expect(Status, Expect) -> 242 | lists:member(Status, Expect). 243 | 244 | parse_response({ok, 204, Headers, Client}, _Opts) -> 245 | ok = hackney:close(Client), 246 | {ok, 204, Headers, []}; 247 | parse_response({ok, Status, Headers, Client}, Opts) -> 248 | NormalizedHeaders = normalize_headers(Headers), 249 | {<<"content-type">>, ContentType} = 250 | content_type(NormalizedHeaders, ?DEFAULT_CTYPE), 251 | Type = parse_type(ContentType), 252 | case hackney:body(Client) of 253 | {ok, Body} -> {ok, Status, Headers, restc_body:decode(Type, Body, Opts)}; 254 | {error, _}=E -> E 255 | end; 256 | parse_response({error, Type}, _Opts) -> 257 | {error, Type}. 258 | 259 | parse_type(Type) -> 260 | case binary:split(Type, <<";">>) of 261 | [CType, _] -> CType; 262 | _ -> Type 263 | end. 264 | 265 | get_accesstype(json) -> <<"application/json">>; 266 | get_accesstype(xml) -> <<"application/xml">>; 267 | get_accesstype(percent) -> <<"application/json">>; 268 | get_accesstype(png) -> <<"image/png">>; 269 | get_accesstype(multi) -> <<"multipart/form-data">>; 270 | get_accesstype(_) -> get_ctype(?DEFAULT_ENCODING). 271 | 272 | get_ctype(json) -> <<"application/json">>; 273 | get_ctype(xml) -> <<"application/xml">>; 274 | get_ctype(percent) -> <<"application/x-www-form-urlencoded">>; 275 | get_ctype(png) -> <<"image/png">>; 276 | get_ctype(multi) -> <<"multipart/form-data">>; 277 | get_ctype(_) -> get_ctype(?DEFAULT_ENCODING). 278 | 279 | %%%_* Emacs ============================================================ 280 | %%% Local Variables: 281 | %%% allout-layout: t 282 | %%% erlang-indent-level: 2 283 | %%% End: 284 | -------------------------------------------------------------------------------- /src/restc_body.erl: -------------------------------------------------------------------------------- 1 | -module(restc_body). 2 | 3 | -export([encode/2, decode/3]). 4 | 5 | encode(json, Body) -> 6 | jsx:encode(Body); 7 | encode(percent, Body) when is_map(Body) -> 8 | hackney_url:qs(maps:to_list(Body), []); 9 | encode(percent, Body) -> 10 | hackney_url:qs(Body, []); 11 | encode(xml, Body) -> 12 | lists:flatten(xmerl:export_simple(Body, xmerl_xml)); 13 | encode(multi, Body) -> 14 | {multipart, Body}. 15 | 16 | decode(_, <<>>, Opts) -> 17 | case proplists:get_bool(return_maps, Opts) of 18 | true -> #{}; 19 | _ -> [] 20 | end; 21 | decode(<<"application/json">>, Body, Opts0) -> 22 | Opts = 23 | case lists:member(return_maps, Opts0) of 24 | true -> [{return_maps, true}]; 25 | false -> [{return_maps, false}] 26 | end, 27 | jsx:decode(Body, Opts); 28 | decode(<<"application/xml">>, Body, _Opts) -> 29 | {ok, Data, _} = erlsom:simple_form(binary_to_list(Body)), 30 | Data; 31 | decode(<<"text/xml">>, Body, Opts) -> 32 | decode(<<"application/xml">>, Body, Opts); 33 | decode(<<"image/png">>, Body, _Opts) -> 34 | Body; 35 | decode(<<"application/x-www-form-urlencoded">>, Body, Opts) -> 36 | KeyValueList = hackney_url:parse_qs(Body), 37 | case proplists:get_bool(return_maps, Opts) of 38 | true -> maps:from_list(KeyValueList); 39 | _ -> KeyValueList 40 | end; 41 | decode(_, Body, _Opts) -> 42 | Body. 43 | -------------------------------------------------------------------------------- /src/restc_util.erl: -------------------------------------------------------------------------------- 1 | -module(restc_util). 2 | 3 | -export([string_to_binary/1, to_binary/1]). 4 | 5 | string_to_binary(V) when is_binary(V) -> V; 6 | string_to_binary(V) when is_list(V) -> list_to_binary(V). 7 | 8 | to_binary(V) when is_integer(V) -> integer_to_binary(V); 9 | to_binary(V) when is_atom(V) -> atom_to_binary(V, utf8); 10 | to_binary(V) -> string_to_binary(V). 11 | -------------------------------------------------------------------------------- /test/mochiweb_util.erl: -------------------------------------------------------------------------------- 1 | %% @author Bob Ippolito 2 | %% @copyright 2007 Mochi Media, Inc. 3 | 4 | %% @doc Utilities for parsing and quoting. 5 | 6 | -module(mochiweb_util). 7 | -author('bob@mochimedia.com'). 8 | -export([join/2, quote_plus/1, urlencode/1, parse_qs/1, unquote/1]). 9 | -export([path_split/1]). 10 | -export([urlsplit/1, urlsplit_path/1, urlunsplit/1, urlunsplit_path/1]). 11 | -export([guess_mime/1, parse_header/1]). 12 | -export([shell_quote/1, cmd/1, cmd_string/1, cmd_port/2, cmd_status/1, cmd_status/2]). 13 | -export([record_to_proplist/2, record_to_proplist/3]). 14 | -export([safe_relative_path/1, partition/2]). 15 | -export([parse_qvalues/1, pick_accepted_encodings/3]). 16 | -export([make_io/1]). 17 | 18 | -define(PERCENT, 37). % $\% 19 | -define(FULLSTOP, 46). % $\. 20 | -define(IS_HEX(C), ((C >= $0 andalso C =< $9) orelse 21 | (C >= $a andalso C =< $f) orelse 22 | (C >= $A andalso C =< $F))). 23 | -define(QS_SAFE(C), ((C >= $a andalso C =< $z) orelse 24 | (C >= $A andalso C =< $Z) orelse 25 | (C >= $0 andalso C =< $9) orelse 26 | (C =:= ?FULLSTOP orelse C =:= $- orelse C =:= $~ orelse 27 | C =:= $_ 28 | %% Make it more RFC3986 complaint 29 | %%sub-delims that hackney whitelists 30 | orelse C =:= $! orelse C =:= $$ orelse C =:= $( 31 | orelse C =:= $) orelse C =:= $* 32 | %% Other valid chars 33 | orelse C =:= $@ 34 | ))). 35 | 36 | hexdigit(C) when C < 10 -> $0 + C; 37 | hexdigit(C) when C < 16 -> $A + (C - 10). 38 | 39 | unhexdigit(C) when C >= $0, C =< $9 -> C - $0; 40 | unhexdigit(C) when C >= $a, C =< $f -> C - $a + 10; 41 | unhexdigit(C) when C >= $A, C =< $F -> C - $A + 10. 42 | 43 | %% @spec partition(String, Sep) -> {String, [], []} | {Prefix, Sep, Postfix} 44 | %% @doc Inspired by Python 2.5's str.partition: 45 | %% partition("foo/bar", "/") = {"foo", "/", "bar"}, 46 | %% partition("foo", "/") = {"foo", "", ""}. 47 | partition(String, Sep) -> 48 | case partition(String, Sep, []) of 49 | undefined -> 50 | {String, "", ""}; 51 | Result -> 52 | Result 53 | end. 54 | 55 | partition("", _Sep, _Acc) -> 56 | undefined; 57 | partition(S, Sep, Acc) -> 58 | case partition2(S, Sep) of 59 | undefined -> 60 | [C | Rest] = S, 61 | partition(Rest, Sep, [C | Acc]); 62 | Rest -> 63 | {lists:reverse(Acc), Sep, Rest} 64 | end. 65 | 66 | partition2(Rest, "") -> 67 | Rest; 68 | partition2([C | R1], [C | R2]) -> 69 | partition2(R1, R2); 70 | partition2(_S, _Sep) -> 71 | undefined. 72 | 73 | 74 | 75 | %% @spec safe_relative_path(string()) -> string() | undefined 76 | %% @doc Return the reduced version of a relative path or undefined if it 77 | %% is not safe. safe relative paths can be joined with an absolute path 78 | %% and will result in a subdirectory of the absolute path. 79 | safe_relative_path("/" ++ _) -> 80 | undefined; 81 | safe_relative_path(P) -> 82 | safe_relative_path(P, []). 83 | 84 | safe_relative_path("", Acc) -> 85 | case Acc of 86 | [] -> 87 | ""; 88 | _ -> 89 | string:join(lists:reverse(Acc), "/") 90 | end; 91 | safe_relative_path(P, Acc) -> 92 | case partition(P, "/") of 93 | {"", "/", _} -> 94 | %% /foo or foo//bar 95 | undefined; 96 | {"..", _, _} when Acc =:= [] -> 97 | undefined; 98 | {"..", _, Rest} -> 99 | safe_relative_path(Rest, tl(Acc)); 100 | {Part, "/", ""} -> 101 | safe_relative_path("", ["", Part | Acc]); 102 | {Part, _, Rest} -> 103 | safe_relative_path(Rest, [Part | Acc]) 104 | end. 105 | 106 | %% @spec shell_quote(string()) -> string() 107 | %% @doc Quote a string according to UNIX shell quoting rules, returns a string 108 | %% surrounded by double quotes. 109 | shell_quote(L) -> 110 | shell_quote(L, [$\"]). 111 | 112 | %% @spec cmd_port([string()], Options) -> port() 113 | %% @doc open_port({spawn, mochiweb_util:cmd_string(Argv)}, Options). 114 | cmd_port(Argv, Options) -> 115 | open_port({spawn, cmd_string(Argv)}, Options). 116 | 117 | %% @spec cmd([string()]) -> string() 118 | %% @doc os:cmd(cmd_string(Argv)). 119 | cmd(Argv) -> 120 | os:cmd(cmd_string(Argv)). 121 | 122 | %% @spec cmd_string([string()]) -> string() 123 | %% @doc Create a shell quoted command string from a list of arguments. 124 | cmd_string(Argv) -> 125 | string:join([shell_quote(X) || X <- Argv], " "). 126 | 127 | %% @spec cmd_status([string()]) -> {ExitStatus::integer(), Stdout::binary()} 128 | %% @doc Accumulate the output and exit status from the given application, 129 | %% will be spawned with cmd_port/2. 130 | cmd_status(Argv) -> 131 | cmd_status(Argv, []). 132 | 133 | %% @spec cmd_status([string()], [atom()]) -> {ExitStatus::integer(), Stdout::binary()} 134 | %% @doc Accumulate the output and exit status from the given application, 135 | %% will be spawned with cmd_port/2. 136 | cmd_status(Argv, Options) -> 137 | Port = cmd_port(Argv, [exit_status, stderr_to_stdout, 138 | use_stdio, binary | Options]), 139 | try cmd_loop(Port, []) 140 | after catch port_close(Port) 141 | end. 142 | 143 | %% @spec cmd_loop(port(), list()) -> {ExitStatus::integer(), Stdout::binary()} 144 | %% @doc Accumulate the output and exit status from a port. 145 | cmd_loop(Port, Acc) -> 146 | receive 147 | {Port, {exit_status, Status}} -> 148 | {Status, iolist_to_binary(lists:reverse(Acc))}; 149 | {Port, {data, Data}} -> 150 | cmd_loop(Port, [Data | Acc]) 151 | end. 152 | 153 | %% @spec join([iolist()], iolist()) -> iolist() 154 | %% @doc Join a list of strings or binaries together with the given separator 155 | %% string or char or binary. The output is flattened, but may be an 156 | %% iolist() instead of a string() if any of the inputs are binary(). 157 | join([], _Separator) -> 158 | []; 159 | join([S], _Separator) -> 160 | lists:flatten(S); 161 | join(Strings, Separator) -> 162 | lists:flatten(revjoin(lists:reverse(Strings), Separator, [])). 163 | 164 | revjoin([], _Separator, Acc) -> 165 | Acc; 166 | revjoin([S | Rest], Separator, []) -> 167 | revjoin(Rest, Separator, [S]); 168 | revjoin([S | Rest], Separator, Acc) -> 169 | revjoin(Rest, Separator, [S, Separator | Acc]). 170 | 171 | %% @spec quote_plus(atom() | integer() | float() | string() | binary()) -> string() 172 | %% @doc URL safe encoding of the given term. 173 | quote_plus(Atom) when is_atom(Atom) -> 174 | quote_plus(atom_to_list(Atom)); 175 | quote_plus(Int) when is_integer(Int) -> 176 | quote_plus(integer_to_list(Int)); 177 | quote_plus(Binary) when is_binary(Binary) -> 178 | quote_plus(binary_to_list(Binary)); 179 | quote_plus(Float) when is_float(Float) -> 180 | quote_plus(mochinum:digits(Float)); 181 | quote_plus(String) -> 182 | quote_plus(String, []). 183 | 184 | quote_plus([], Acc) -> 185 | lists:reverse(Acc); 186 | quote_plus([C | Rest], Acc) when ?QS_SAFE(C) -> 187 | quote_plus(Rest, [C | Acc]); 188 | quote_plus([$\s | Rest], Acc) -> 189 | quote_plus(Rest, [$+ | Acc]); 190 | quote_plus([C | Rest], Acc) -> 191 | <> = <>, 192 | quote_plus(Rest, [hexdigit(Lo), hexdigit(Hi), ?PERCENT | Acc]). 193 | 194 | %% @spec urlencode([{Key, Value}]) -> string() 195 | %% @doc URL encode the property list. 196 | urlencode(Props) -> 197 | Pairs = lists:foldr( 198 | fun ({K, V}, Acc) -> 199 | [quote_plus(K) ++ "=" ++ quote_plus(V) | Acc] 200 | end, [], Props), 201 | string:join(Pairs, "&"). 202 | 203 | %% @spec parse_qs(string() | binary()) -> [{Key, Value}] 204 | %% @doc Parse a query string or application/x-www-form-urlencoded. 205 | parse_qs(Binary) when is_binary(Binary) -> 206 | parse_qs(binary_to_list(Binary)); 207 | parse_qs(String) -> 208 | parse_qs(String, []). 209 | 210 | parse_qs([], Acc) -> 211 | lists:reverse(Acc); 212 | parse_qs(String, Acc) -> 213 | {Key, Rest} = parse_qs_key(String), 214 | {Value, Rest1} = parse_qs_value(Rest), 215 | parse_qs(Rest1, [{Key, Value} | Acc]). 216 | 217 | parse_qs_key(String) -> 218 | parse_qs_key(String, []). 219 | 220 | parse_qs_key([], Acc) -> 221 | {qs_revdecode(Acc), ""}; 222 | parse_qs_key([$= | Rest], Acc) -> 223 | {qs_revdecode(Acc), Rest}; 224 | parse_qs_key(Rest=[$; | _], Acc) -> 225 | {qs_revdecode(Acc), Rest}; 226 | parse_qs_key(Rest=[$& | _], Acc) -> 227 | {qs_revdecode(Acc), Rest}; 228 | parse_qs_key([C | Rest], Acc) -> 229 | parse_qs_key(Rest, [C | Acc]). 230 | 231 | parse_qs_value(String) -> 232 | parse_qs_value(String, []). 233 | 234 | parse_qs_value([], Acc) -> 235 | {qs_revdecode(Acc), ""}; 236 | parse_qs_value([$; | Rest], Acc) -> 237 | {qs_revdecode(Acc), Rest}; 238 | parse_qs_value([$& | Rest], Acc) -> 239 | {qs_revdecode(Acc), Rest}; 240 | parse_qs_value([C | Rest], Acc) -> 241 | parse_qs_value(Rest, [C | Acc]). 242 | 243 | %% @spec unquote(string() | binary()) -> string() 244 | %% @doc Unquote a URL encoded string. 245 | unquote(Binary) when is_binary(Binary) -> 246 | unquote(binary_to_list(Binary)); 247 | unquote(String) -> 248 | qs_revdecode(lists:reverse(String)). 249 | 250 | qs_revdecode(S) -> 251 | qs_revdecode(S, []). 252 | 253 | qs_revdecode([], Acc) -> 254 | Acc; 255 | qs_revdecode([$+ | Rest], Acc) -> 256 | qs_revdecode(Rest, [$\s | Acc]); 257 | qs_revdecode([Lo, Hi, ?PERCENT | Rest], Acc) when ?IS_HEX(Lo), ?IS_HEX(Hi) -> 258 | qs_revdecode(Rest, [(unhexdigit(Lo) bor (unhexdigit(Hi) bsl 4)) | Acc]); 259 | qs_revdecode([C | Rest], Acc) -> 260 | qs_revdecode(Rest, [C | Acc]). 261 | 262 | %% @spec urlsplit(Url) -> {Scheme, Netloc, Path, Query, Fragment} 263 | %% @doc Return a 5-tuple, does not expand % escapes. Only supports HTTP style 264 | %% URLs. 265 | urlsplit(Url) -> 266 | {Scheme, Url1} = urlsplit_scheme(Url), 267 | {Netloc, Url2} = urlsplit_netloc(Url1), 268 | {Path, Query, Fragment} = urlsplit_path(Url2), 269 | {Scheme, Netloc, Path, Query, Fragment}. 270 | 271 | urlsplit_scheme(Url) -> 272 | case urlsplit_scheme(Url, []) of 273 | no_scheme -> 274 | {"", Url}; 275 | Res -> 276 | Res 277 | end. 278 | 279 | urlsplit_scheme([C | Rest], Acc) when ((C >= $a andalso C =< $z) orelse 280 | (C >= $A andalso C =< $Z) orelse 281 | (C >= $0 andalso C =< $9) orelse 282 | C =:= $+ orelse C =:= $- orelse 283 | C =:= $.) -> 284 | urlsplit_scheme(Rest, [C | Acc]); 285 | urlsplit_scheme([$: | Rest], Acc=[_ | _]) -> 286 | {string:to_lower(lists:reverse(Acc)), Rest}; 287 | urlsplit_scheme(_Rest, _Acc) -> 288 | no_scheme. 289 | 290 | urlsplit_netloc("//" ++ Rest) -> 291 | urlsplit_netloc(Rest, []); 292 | urlsplit_netloc(Path) -> 293 | {"", Path}. 294 | 295 | urlsplit_netloc("", Acc) -> 296 | {lists:reverse(Acc), ""}; 297 | urlsplit_netloc(Rest=[C | _], Acc) when C =:= $/; C =:= $?; C =:= $# -> 298 | {lists:reverse(Acc), Rest}; 299 | urlsplit_netloc([C | Rest], Acc) -> 300 | urlsplit_netloc(Rest, [C | Acc]). 301 | 302 | 303 | %% @spec path_split(string()) -> {Part, Rest} 304 | %% @doc Split a path starting from the left, as in URL traversal. 305 | %% path_split("foo/bar") = {"foo", "bar"}, 306 | %% path_split("/foo/bar") = {"", "foo/bar"}. 307 | path_split(S) -> 308 | path_split(S, []). 309 | 310 | path_split("", Acc) -> 311 | {lists:reverse(Acc), ""}; 312 | path_split("/" ++ Rest, Acc) -> 313 | {lists:reverse(Acc), Rest}; 314 | path_split([C | Rest], Acc) -> 315 | path_split(Rest, [C | Acc]). 316 | 317 | 318 | %% @spec urlunsplit({Scheme, Netloc, Path, Query, Fragment}) -> string() 319 | %% @doc Assemble a URL from the 5-tuple. Path must be absolute. 320 | urlunsplit({Scheme, Netloc, Path, Query, Fragment}) -> 321 | lists:flatten([case Scheme of "" -> ""; _ -> [Scheme, "://"] end, 322 | Netloc, 323 | urlunsplit_path({Path, Query, Fragment})]). 324 | 325 | %% @spec urlunsplit_path({Path, Query, Fragment}) -> string() 326 | %% @doc Assemble a URL path from the 3-tuple. 327 | urlunsplit_path({Path, Query, Fragment}) -> 328 | lists:flatten([Path, 329 | case Query of "" -> ""; _ -> [$? | Query] end, 330 | case Fragment of "" -> ""; _ -> [$# | Fragment] end]). 331 | 332 | %% @spec urlsplit_path(Url) -> {Path, Query, Fragment} 333 | %% @doc Return a 3-tuple, does not expand % escapes. Only supports HTTP style 334 | %% paths. 335 | urlsplit_path(Path) -> 336 | urlsplit_path(Path, []). 337 | 338 | urlsplit_path("", Acc) -> 339 | {lists:reverse(Acc), "", ""}; 340 | urlsplit_path("?" ++ Rest, Acc) -> 341 | {Query, Fragment} = urlsplit_query(Rest), 342 | {lists:reverse(Acc), Query, Fragment}; 343 | urlsplit_path("#" ++ Rest, Acc) -> 344 | {lists:reverse(Acc), "", Rest}; 345 | urlsplit_path([C | Rest], Acc) -> 346 | urlsplit_path(Rest, [C | Acc]). 347 | 348 | urlsplit_query(Query) -> 349 | urlsplit_query(Query, []). 350 | 351 | urlsplit_query("", Acc) -> 352 | {lists:reverse(Acc), ""}; 353 | urlsplit_query("#" ++ Rest, Acc) -> 354 | {lists:reverse(Acc), Rest}; 355 | urlsplit_query([C | Rest], Acc) -> 356 | urlsplit_query(Rest, [C | Acc]). 357 | 358 | %% @spec guess_mime(string()) -> string() 359 | %% @doc Guess the mime type of a file by the extension of its filename. 360 | guess_mime(File) -> 361 | case mochiweb_mime:from_extension(filename:extension(File)) of 362 | undefined -> 363 | "text/plain"; 364 | Mime -> 365 | Mime 366 | end. 367 | 368 | %% @spec parse_header(string()) -> {Type, [{K, V}]} 369 | %% @doc Parse a Content-Type like header, return the main Content-Type 370 | %% and a property list of options. 371 | parse_header(String) -> 372 | %% TODO: This is exactly as broken as Python's cgi module. 373 | %% Should parse properly like mochiweb_cookies. 374 | [Type | Parts] = [string:strip(S) || S <- string:tokens(String, ";")], 375 | F = fun (S, Acc) -> 376 | case lists:splitwith(fun (C) -> C =/= $= end, S) of 377 | {"", _} -> 378 | %% Skip anything with no name 379 | Acc; 380 | {_, ""} -> 381 | %% Skip anything with no value 382 | Acc; 383 | {Name, [$\= | Value]} -> 384 | [{string:to_lower(string:strip(Name)), 385 | unquote_header(string:strip(Value))} | Acc] 386 | end 387 | end, 388 | {string:to_lower(Type), 389 | lists:foldr(F, [], Parts)}. 390 | 391 | unquote_header("\"" ++ Rest) -> 392 | unquote_header(Rest, []); 393 | unquote_header(S) -> 394 | S. 395 | 396 | unquote_header("", Acc) -> 397 | lists:reverse(Acc); 398 | unquote_header("\"", Acc) -> 399 | lists:reverse(Acc); 400 | unquote_header([$\\, C | Rest], Acc) -> 401 | unquote_header(Rest, [C | Acc]); 402 | unquote_header([C | Rest], Acc) -> 403 | unquote_header(Rest, [C | Acc]). 404 | 405 | %% @spec record_to_proplist(Record, Fields) -> proplist() 406 | %% @doc calls record_to_proplist/3 with a default TypeKey of '__record' 407 | record_to_proplist(Record, Fields) -> 408 | record_to_proplist(Record, Fields, '__record'). 409 | 410 | %% @spec record_to_proplist(Record, Fields, TypeKey) -> proplist() 411 | %% @doc Return a proplist of the given Record with each field in the 412 | %% Fields list set as a key with the corresponding value in the Record. 413 | %% TypeKey is the key that is used to store the record type 414 | %% Fields should be obtained by calling record_info(fields, record_type) 415 | %% where record_type is the record type of Record 416 | record_to_proplist(Record, Fields, TypeKey) 417 | when tuple_size(Record) - 1 =:= length(Fields) -> 418 | lists:zip([TypeKey | Fields], tuple_to_list(Record)). 419 | 420 | 421 | shell_quote([], Acc) -> 422 | lists:reverse([$\" | Acc]); 423 | shell_quote([C | Rest], Acc) when C =:= $\" orelse C =:= $\` orelse 424 | C =:= $\\ orelse C =:= $\$ -> 425 | shell_quote(Rest, [C, $\\ | Acc]); 426 | shell_quote([C | Rest], Acc) -> 427 | shell_quote(Rest, [C | Acc]). 428 | 429 | %% @spec parse_qvalues(string()) -> [qvalue()] | invalid_qvalue_string 430 | %% @type qvalue() = {media_type() | encoding() , float()}. 431 | %% @type media_type() = string(). 432 | %% @type encoding() = string(). 433 | %% 434 | %% @doc Parses a list (given as a string) of elements with Q values associated 435 | %% to them. Elements are separated by commas and each element is separated 436 | %% from its Q value by a semicolon. Q values are optional but when missing 437 | %% the value of an element is considered as 1.0. A Q value is always in the 438 | %% range [0.0, 1.0]. A Q value list is used for example as the value of the 439 | %% HTTP "Accept" and "Accept-Encoding" headers. 440 | %% 441 | %% Q values are described in section 2.9 of the RFC 2616 (HTTP 1.1). 442 | %% 443 | %% Example: 444 | %% 445 | %% parse_qvalues("gzip; q=0.5, deflate, identity;q=0.0") -> 446 | %% [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] 447 | %% 448 | parse_qvalues(QValuesStr) -> 449 | try 450 | lists:map( 451 | fun(Pair) -> 452 | [Type | Params] = string:tokens(Pair, ";"), 453 | NormParams = normalize_media_params(Params), 454 | {Q, NonQParams} = extract_q(NormParams), 455 | {string:join([string:strip(Type) | NonQParams], ";"), Q} 456 | end, 457 | string:tokens(string:to_lower(QValuesStr), ",") 458 | ) 459 | catch 460 | _Type:_Error -> 461 | invalid_qvalue_string 462 | end. 463 | 464 | normalize_media_params(Params) -> 465 | {ok, Re} = re:compile("\\s"), 466 | normalize_media_params(Re, Params, []). 467 | 468 | normalize_media_params(_Re, [], Acc) -> 469 | lists:reverse(Acc); 470 | normalize_media_params(Re, [Param | Rest], Acc) -> 471 | NormParam = re:replace(Param, Re, "", [global, {return, list}]), 472 | normalize_media_params(Re, Rest, [NormParam | Acc]). 473 | 474 | extract_q(NormParams) -> 475 | {ok, KVRe} = re:compile("^([^=]+)=([^=]+)$"), 476 | {ok, QRe} = re:compile("^((?:0|1)(?:\\.\\d{1,3})?)$"), 477 | extract_q(KVRe, QRe, NormParams, []). 478 | 479 | extract_q(_KVRe, _QRe, [], Acc) -> 480 | {1.0, lists:reverse(Acc)}; 481 | extract_q(KVRe, QRe, [Param | Rest], Acc) -> 482 | case re:run(Param, KVRe, [{capture, [1, 2], list}]) of 483 | {match, [Name, Value]} -> 484 | case Name of 485 | "q" -> 486 | {match, [Q]} = re:run(Value, QRe, [{capture, [1], list}]), 487 | QVal = case Q of 488 | "0" -> 489 | 0.0; 490 | "1" -> 491 | 1.0; 492 | Else -> 493 | list_to_float(Else) 494 | end, 495 | case QVal < 0.0 orelse QVal > 1.0 of 496 | false -> 497 | {QVal, lists:reverse(Acc) ++ Rest} 498 | end; 499 | _ -> 500 | extract_q(KVRe, QRe, Rest, [Param | Acc]) 501 | end 502 | end. 503 | 504 | %% @spec pick_accepted_encodings([qvalue()], [encoding()], encoding()) -> 505 | %% [encoding()] 506 | %% 507 | %% @doc Determines which encodings specified in the given Q values list are 508 | %% valid according to a list of supported encodings and a default encoding. 509 | %% 510 | %% The returned list of encodings is sorted, descendingly, according to the 511 | %% Q values of the given list. The last element of this list is the given 512 | %% default encoding unless this encoding is explicitly or implicitily 513 | %% marked with a Q value of 0.0 in the given Q values list. 514 | %% Note: encodings with the same Q value are kept in the same order as 515 | %% found in the input Q values list. 516 | %% 517 | %% This encoding picking process is described in section 14.3 of the 518 | %% RFC 2616 (HTTP 1.1). 519 | %% 520 | %% Example: 521 | %% 522 | %% pick_accepted_encodings( 523 | %% [{"gzip", 0.5}, {"deflate", 1.0}], 524 | %% ["gzip", "identity"], 525 | %% "identity" 526 | %% ) -> 527 | %% ["gzip", "identity"] 528 | %% 529 | pick_accepted_encodings(AcceptedEncs, SupportedEncs, DefaultEnc) -> 530 | SortedQList = lists:reverse( 531 | lists:sort(fun({_, Q1}, {_, Q2}) -> Q1 < Q2 end, AcceptedEncs) 532 | ), 533 | {Accepted, Refused} = lists:foldr( 534 | fun({E, Q}, {A, R}) -> 535 | case Q > 0.0 of 536 | true -> 537 | {[E | A], R}; 538 | false -> 539 | {A, [E | R]} 540 | end 541 | end, 542 | {[], []}, 543 | SortedQList 544 | ), 545 | Refused1 = lists:foldr( 546 | fun(Enc, Acc) -> 547 | case Enc of 548 | "*" -> 549 | lists:subtract(SupportedEncs, Accepted) ++ Acc; 550 | _ -> 551 | [Enc | Acc] 552 | end 553 | end, 554 | [], 555 | Refused 556 | ), 557 | Accepted1 = lists:foldr( 558 | fun(Enc, Acc) -> 559 | case Enc of 560 | "*" -> 561 | lists:subtract(SupportedEncs, Accepted ++ Refused1) ++ Acc; 562 | _ -> 563 | [Enc | Acc] 564 | end 565 | end, 566 | [], 567 | Accepted 568 | ), 569 | Accepted2 = case lists:member(DefaultEnc, Accepted1) of 570 | true -> 571 | Accepted1; 572 | false -> 573 | Accepted1 ++ [DefaultEnc] 574 | end, 575 | [E || E <- Accepted2, lists:member(E, SupportedEncs), 576 | not lists:member(E, Refused1)]. 577 | 578 | make_io(Atom) when is_atom(Atom) -> 579 | atom_to_list(Atom); 580 | make_io(Integer) when is_integer(Integer) -> 581 | integer_to_list(Integer); 582 | make_io(Io) when is_list(Io); is_binary(Io) -> 583 | Io. 584 | 585 | %% 586 | %% Tests 587 | %% 588 | -ifdef(TEST). 589 | -include_lib("eunit/include/eunit.hrl"). 590 | 591 | make_io_test() -> 592 | ?assertEqual( 593 | <<"atom">>, 594 | iolist_to_binary(make_io(atom))), 595 | ?assertEqual( 596 | <<"20">>, 597 | iolist_to_binary(make_io(20))), 598 | ?assertEqual( 599 | <<"list">>, 600 | iolist_to_binary(make_io("list"))), 601 | ?assertEqual( 602 | <<"binary">>, 603 | iolist_to_binary(make_io(<<"binary">>))), 604 | ok. 605 | 606 | -record(test_record, {field1=f1, field2=f2}). 607 | record_to_proplist_test() -> 608 | ?assertEqual( 609 | [{'__record', test_record}, 610 | {field1, f1}, 611 | {field2, f2}], 612 | record_to_proplist(#test_record{}, record_info(fields, test_record))), 613 | ?assertEqual( 614 | [{'typekey', test_record}, 615 | {field1, f1}, 616 | {field2, f2}], 617 | record_to_proplist(#test_record{}, 618 | record_info(fields, test_record), 619 | typekey)), 620 | ok. 621 | 622 | shell_quote_test() -> 623 | ?assertEqual( 624 | "\"foo \\$bar\\\"\\`' baz\"", 625 | shell_quote("foo $bar\"`' baz")), 626 | ok. 627 | 628 | cmd_port_test_spool(Port, Acc) -> 629 | receive 630 | {Port, eof} -> 631 | Acc; 632 | {Port, {data, {eol, Data}}} -> 633 | cmd_port_test_spool(Port, ["\n", Data | Acc]); 634 | {Port, Unknown} -> 635 | throw({unknown, Unknown}) 636 | after 1000 -> 637 | throw(timeout) 638 | end. 639 | 640 | cmd_port_test() -> 641 | Port = cmd_port(["echo", "$bling$ `word`!"], 642 | [eof, stream, {line, 4096}]), 643 | Res = try lists:append(lists:reverse(cmd_port_test_spool(Port, []))) 644 | after catch port_close(Port) 645 | end, 646 | self() ! {Port, wtf}, 647 | try cmd_port_test_spool(Port, []) 648 | catch throw:{unknown, wtf} -> ok 649 | end, 650 | try cmd_port_test_spool(Port, []) 651 | catch throw:timeout -> ok 652 | end, 653 | ?assertEqual( 654 | "$bling$ `word`!\n", 655 | Res). 656 | 657 | cmd_test() -> 658 | ?assertEqual( 659 | "$bling$ `word`!\n", 660 | cmd(["echo", "$bling$ `word`!"])), 661 | ok. 662 | 663 | cmd_string_test() -> 664 | ?assertEqual( 665 | "\"echo\" \"\\$bling\\$ \\`word\\`!\"", 666 | cmd_string(["echo", "$bling$ `word`!"])), 667 | ok. 668 | 669 | cmd_status_test() -> 670 | ?assertEqual( 671 | {0, <<"$bling$ `word`!\n">>}, 672 | cmd_status(["echo", "$bling$ `word`!"])), 673 | ok. 674 | 675 | 676 | parse_header_test() -> 677 | ?assertEqual( 678 | {"multipart/form-data", [{"boundary", "AaB03x"}]}, 679 | parse_header("multipart/form-data; boundary=AaB03x")), 680 | %% This tests (currently) intentionally broken behavior 681 | ?assertEqual( 682 | {"multipart/form-data", 683 | [{"b", ""}, 684 | {"cgi", "is"}, 685 | {"broken", "true\"e"}]}, 686 | parse_header("multipart/form-data;b=;cgi=\"i\\s;broken=true\"e;=z;z")), 687 | ok. 688 | 689 | guess_mime_test() -> 690 | "text/plain" = guess_mime(""), 691 | "text/plain" = guess_mime(".text"), 692 | "application/zip" = guess_mime(".zip"), 693 | "application/zip" = guess_mime("x.zip"), 694 | "text/html" = guess_mime("x.html"), 695 | "application/xhtml+xml" = guess_mime("x.xhtml"), 696 | ok. 697 | 698 | path_split_test() -> 699 | {"", "foo/bar"} = path_split("/foo/bar"), 700 | {"foo", "bar"} = path_split("foo/bar"), 701 | {"bar", ""} = path_split("bar"), 702 | ok. 703 | 704 | urlsplit_test() -> 705 | {"", "", "/foo", "", "bar?baz"} = urlsplit("/foo#bar?baz"), 706 | {"http", "host:port", "/foo", "", "bar?baz"} = 707 | urlsplit("http://host:port/foo#bar?baz"), 708 | {"http", "host", "", "", ""} = urlsplit("http://host"), 709 | {"", "", "/wiki/Category:Fruit", "", ""} = 710 | urlsplit("/wiki/Category:Fruit"), 711 | ok. 712 | 713 | urlsplit_path_test() -> 714 | {"/foo/bar", "", ""} = urlsplit_path("/foo/bar"), 715 | {"/foo", "baz", ""} = urlsplit_path("/foo?baz"), 716 | {"/foo", "", "bar?baz"} = urlsplit_path("/foo#bar?baz"), 717 | {"/foo", "", "bar?baz#wibble"} = urlsplit_path("/foo#bar?baz#wibble"), 718 | {"/foo", "bar", "baz"} = urlsplit_path("/foo?bar#baz"), 719 | {"/foo", "bar?baz", "baz"} = urlsplit_path("/foo?bar?baz#baz"), 720 | ok. 721 | 722 | urlunsplit_test() -> 723 | "/foo#bar?baz" = urlunsplit({"", "", "/foo", "", "bar?baz"}), 724 | "http://host:port/foo#bar?baz" = 725 | urlunsplit({"http", "host:port", "/foo", "", "bar?baz"}), 726 | ok. 727 | 728 | urlunsplit_path_test() -> 729 | "/foo/bar" = urlunsplit_path({"/foo/bar", "", ""}), 730 | "/foo?baz" = urlunsplit_path({"/foo", "baz", ""}), 731 | "/foo#bar?baz" = urlunsplit_path({"/foo", "", "bar?baz"}), 732 | "/foo#bar?baz#wibble" = urlunsplit_path({"/foo", "", "bar?baz#wibble"}), 733 | "/foo?bar#baz" = urlunsplit_path({"/foo", "bar", "baz"}), 734 | "/foo?bar?baz#baz" = urlunsplit_path({"/foo", "bar?baz", "baz"}), 735 | ok. 736 | 737 | join_test() -> 738 | ?assertEqual("foo,bar,baz", 739 | join(["foo", "bar", "baz"], $,)), 740 | ?assertEqual("foo,bar,baz", 741 | join(["foo", "bar", "baz"], ",")), 742 | ?assertEqual("foo bar", 743 | join([["foo", " bar"]], ",")), 744 | ?assertEqual("foo bar,baz", 745 | join([["foo", " bar"], "baz"], ",")), 746 | ?assertEqual("foo", 747 | join(["foo"], ",")), 748 | ?assertEqual("foobarbaz", 749 | join(["foo", "bar", "baz"], "")), 750 | ?assertEqual("foo" ++ [<<>>] ++ "bar" ++ [<<>>] ++ "baz", 751 | join(["foo", "bar", "baz"], <<>>)), 752 | ?assertEqual("foobar" ++ [<<"baz">>], 753 | join(["foo", "bar", <<"baz">>], "")), 754 | ?assertEqual("", 755 | join([], "any")), 756 | ok. 757 | 758 | quote_plus_test() -> 759 | "foo" = quote_plus(foo), 760 | "1" = quote_plus(1), 761 | "1.1" = quote_plus(1.1), 762 | "foo" = quote_plus("foo"), 763 | "foo+bar" = quote_plus("foo bar"), 764 | "foo%0A" = quote_plus("foo\n"), 765 | "foo%0A" = quote_plus("foo\n"), 766 | "foo%3B%26%3D" = quote_plus("foo;&="), 767 | "foo%3B%26%3D" = quote_plus(<<"foo;&=">>), 768 | ok. 769 | 770 | unquote_test() -> 771 | ?assertEqual("foo bar", 772 | unquote("foo+bar")), 773 | ?assertEqual("foo bar", 774 | unquote("foo%20bar")), 775 | ?assertEqual("foo\r\n", 776 | unquote("foo%0D%0A")), 777 | ?assertEqual("foo\r\n", 778 | unquote(<<"foo%0D%0A">>)), 779 | ok. 780 | 781 | urlencode_test() -> 782 | "foo=bar&baz=wibble+%0D%0A&z=1" = urlencode([{foo, "bar"}, 783 | {"baz", "wibble \r\n"}, 784 | {z, 1}]), 785 | ok. 786 | 787 | parse_qs_test() -> 788 | ?assertEqual( 789 | [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}], 790 | parse_qs("foo=bar&baz=wibble+%0D%0a&z=1")), 791 | ?assertEqual( 792 | [{"", "bar"}, {"baz", "wibble \r\n"}, {"z", ""}], 793 | parse_qs("=bar&baz=wibble+%0D%0a&z=")), 794 | ?assertEqual( 795 | [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}], 796 | parse_qs(<<"foo=bar&baz=wibble+%0D%0a&z=1">>)), 797 | ?assertEqual( 798 | [], 799 | parse_qs("")), 800 | ?assertEqual( 801 | [{"foo", ""}, {"bar", ""}, {"baz", ""}], 802 | parse_qs("foo;bar&baz")), 803 | ok. 804 | 805 | partition_test() -> 806 | {"foo", "", ""} = partition("foo", "/"), 807 | {"foo", "/", "bar"} = partition("foo/bar", "/"), 808 | {"foo", "/", ""} = partition("foo/", "/"), 809 | {"", "/", "bar"} = partition("/bar", "/"), 810 | {"f", "oo/ba", "r"} = partition("foo/bar", "oo/ba"), 811 | ok. 812 | 813 | safe_relative_path_test() -> 814 | "foo" = safe_relative_path("foo"), 815 | "foo/" = safe_relative_path("foo/"), 816 | "foo" = safe_relative_path("foo/bar/.."), 817 | "bar" = safe_relative_path("foo/../bar"), 818 | "bar/" = safe_relative_path("foo/../bar/"), 819 | "" = safe_relative_path("foo/.."), 820 | "" = safe_relative_path("foo/../"), 821 | undefined = safe_relative_path("/foo"), 822 | undefined = safe_relative_path("../foo"), 823 | undefined = safe_relative_path("foo/../.."), 824 | undefined = safe_relative_path("foo//"), 825 | ok. 826 | 827 | parse_qvalues_test() -> 828 | [] = parse_qvalues(""), 829 | [{"identity", 0.0}] = parse_qvalues("identity;q=0"), 830 | [{"identity", 0.0}] = parse_qvalues("identity ;q=0"), 831 | [{"identity", 0.0}] = parse_qvalues(" identity; q =0 "), 832 | [{"identity", 0.0}] = parse_qvalues("identity ; q = 0"), 833 | [{"identity", 0.0}] = parse_qvalues("identity ; q= 0.0"), 834 | [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( 835 | "gzip,deflate,identity;q=0.0" 836 | ), 837 | [{"deflate", 1.0}, {"gzip", 1.0}, {"identity", 0.0}] = parse_qvalues( 838 | "deflate,gzip,identity;q=0.0" 839 | ), 840 | [{"gzip", 1.0}, {"deflate", 1.0}, {"gzip", 1.0}, {"identity", 0.0}] = 841 | parse_qvalues("gzip,deflate,gzip,identity;q=0"), 842 | [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( 843 | "gzip, deflate , identity; q=0.0" 844 | ), 845 | [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( 846 | "gzip; q=1, deflate;q=1.0, identity;q=0.0" 847 | ), 848 | [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( 849 | "gzip; q=0.5, deflate;q=1.0, identity;q=0" 850 | ), 851 | [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( 852 | "gzip; q=0.5, deflate , identity;q=0.0" 853 | ), 854 | [{"gzip", 0.5}, {"deflate", 0.8}, {"identity", 0.0}] = parse_qvalues( 855 | "gzip; q=0.5, deflate;q=0.8, identity;q=0.0" 856 | ), 857 | [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 1.0}] = parse_qvalues( 858 | "gzip; q=0.5,deflate,identity" 859 | ), 860 | [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 1.0}, {"identity", 1.0}] = 861 | parse_qvalues("gzip; q=0.5,deflate,identity, identity "), 862 | [{"text/html;level=1", 1.0}, {"text/plain", 0.5}] = 863 | parse_qvalues("text/html;level=1, text/plain;q=0.5"), 864 | [{"text/html;level=1", 0.3}, {"text/plain", 1.0}] = 865 | parse_qvalues("text/html;level=1;q=0.3, text/plain"), 866 | [{"text/html;level=1", 0.3}, {"text/plain", 1.0}] = 867 | parse_qvalues("text/html; level = 1; q = 0.3, text/plain"), 868 | [{"text/html;level=1", 0.3}, {"text/plain", 1.0}] = 869 | parse_qvalues("text/html;q=0.3;level=1, text/plain"), 870 | invalid_qvalue_string = parse_qvalues("gzip; q=1.1, deflate"), 871 | invalid_qvalue_string = parse_qvalues("gzip; q=0.5, deflate;q=2"), 872 | invalid_qvalue_string = parse_qvalues("gzip, deflate;q=AB"), 873 | invalid_qvalue_string = parse_qvalues("gzip; q=2.1, deflate"), 874 | invalid_qvalue_string = parse_qvalues("gzip; q=0.1234, deflate"), 875 | invalid_qvalue_string = parse_qvalues("text/html;level=1;q=0.3, text/html;level"), 876 | ok. 877 | 878 | pick_accepted_encodings_test() -> 879 | ["identity"] = pick_accepted_encodings( 880 | [], 881 | ["gzip", "identity"], 882 | "identity" 883 | ), 884 | ["gzip", "identity"] = pick_accepted_encodings( 885 | [{"gzip", 1.0}], 886 | ["gzip", "identity"], 887 | "identity" 888 | ), 889 | ["identity"] = pick_accepted_encodings( 890 | [{"gzip", 0.0}], 891 | ["gzip", "identity"], 892 | "identity" 893 | ), 894 | ["gzip", "identity"] = pick_accepted_encodings( 895 | [{"gzip", 1.0}, {"deflate", 1.0}], 896 | ["gzip", "identity"], 897 | "identity" 898 | ), 899 | ["gzip", "identity"] = pick_accepted_encodings( 900 | [{"gzip", 0.5}, {"deflate", 1.0}], 901 | ["gzip", "identity"], 902 | "identity" 903 | ), 904 | ["identity"] = pick_accepted_encodings( 905 | [{"gzip", 0.0}, {"deflate", 0.0}], 906 | ["gzip", "identity"], 907 | "identity" 908 | ), 909 | ["gzip"] = pick_accepted_encodings( 910 | [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}], 911 | ["gzip", "identity"], 912 | "identity" 913 | ), 914 | ["gzip", "deflate", "identity"] = pick_accepted_encodings( 915 | [{"gzip", 1.0}, {"deflate", 1.0}], 916 | ["gzip", "deflate", "identity"], 917 | "identity" 918 | ), 919 | ["gzip", "deflate"] = pick_accepted_encodings( 920 | [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}], 921 | ["gzip", "deflate", "identity"], 922 | "identity" 923 | ), 924 | ["deflate", "gzip", "identity"] = pick_accepted_encodings( 925 | [{"gzip", 0.2}, {"deflate", 1.0}], 926 | ["gzip", "deflate", "identity"], 927 | "identity" 928 | ), 929 | ["deflate", "deflate", "gzip", "identity"] = pick_accepted_encodings( 930 | [{"gzip", 0.2}, {"deflate", 1.0}, {"deflate", 1.0}], 931 | ["gzip", "deflate", "identity"], 932 | "identity" 933 | ), 934 | ["deflate", "gzip", "gzip", "identity"] = pick_accepted_encodings( 935 | [{"gzip", 0.2}, {"deflate", 1.0}, {"gzip", 1.0}], 936 | ["gzip", "deflate", "identity"], 937 | "identity" 938 | ), 939 | ["gzip", "deflate", "gzip", "identity"] = pick_accepted_encodings( 940 | [{"gzip", 0.2}, {"deflate", 0.9}, {"gzip", 1.0}], 941 | ["gzip", "deflate", "identity"], 942 | "identity" 943 | ), 944 | [] = pick_accepted_encodings( 945 | [{"*", 0.0}], 946 | ["gzip", "deflate", "identity"], 947 | "identity" 948 | ), 949 | ["gzip", "deflate", "identity"] = pick_accepted_encodings( 950 | [{"*", 1.0}], 951 | ["gzip", "deflate", "identity"], 952 | "identity" 953 | ), 954 | ["gzip", "deflate", "identity"] = pick_accepted_encodings( 955 | [{"*", 0.6}], 956 | ["gzip", "deflate", "identity"], 957 | "identity" 958 | ), 959 | ["gzip"] = pick_accepted_encodings( 960 | [{"gzip", 1.0}, {"*", 0.0}], 961 | ["gzip", "deflate", "identity"], 962 | "identity" 963 | ), 964 | ["gzip", "deflate"] = pick_accepted_encodings( 965 | [{"gzip", 1.0}, {"deflate", 0.6}, {"*", 0.0}], 966 | ["gzip", "deflate", "identity"], 967 | "identity" 968 | ), 969 | ["deflate", "gzip"] = pick_accepted_encodings( 970 | [{"gzip", 0.5}, {"deflate", 1.0}, {"*", 0.0}], 971 | ["gzip", "deflate", "identity"], 972 | "identity" 973 | ), 974 | ["gzip", "identity"] = pick_accepted_encodings( 975 | [{"deflate", 0.0}, {"*", 1.0}], 976 | ["gzip", "deflate", "identity"], 977 | "identity" 978 | ), 979 | ["gzip", "identity"] = pick_accepted_encodings( 980 | [{"*", 1.0}, {"deflate", 0.0}], 981 | ["gzip", "deflate", "identity"], 982 | "identity" 983 | ), 984 | ok. 985 | 986 | -endif. 987 | -------------------------------------------------------------------------------- /test/prop_restc.erl: -------------------------------------------------------------------------------- 1 | -module(prop_restc). 2 | -include_lib("proper/include/proper.hrl"). 3 | 4 | %%%%%%%%%%%%%%%%%% 5 | %%% Properties %%% 6 | %%%%%%%%%%%%%%%%%% 7 | prop_contruct_url_3() -> 8 | ?FORALL({S,H,P,Q}, {scheme(), host(), path(), query_elements()}, 9 | begin 10 | BaseUrlBin = <>, 11 | BaseUrl = binary_to_list(BaseUrlBin), 12 | Path = binary_to_list(P), 13 | Query = lists:map(fun({K, V}) -> 14 | {binary_to_list(K), binary_to_list(V)} 15 | end, Q), 16 | Oracle = construct_url_oracle(BaseUrl, Path, Query), 17 | RestC0 = restc:construct_url(BaseUrl, Path, Query, [return_binary]), 18 | 19 | RestC = case P of 20 | P when P =:= <<>>; P =:= <<"/">> -> 21 | {match, [[Left, Right]]} = 22 | re:run(RestC0, 23 | <<"(https?://.*)/(\\??.*)">>, 24 | [global, {capture, all_but_first, binary}]), 25 | <>; 26 | _ -> RestC0 27 | end, 28 | %% use lowercase since Oracle returns uppercase percent encoded and 29 | %% implementation uses lowercase percent encoded 30 | string:lowercase(Oracle) =:= string:lowercase(binary_to_list(RestC)) 31 | end). 32 | 33 | %%%%%%%%%%%%%%% 34 | %%% Helpers %%% 35 | %%%%%%%%%%%%%%% 36 | 37 | construct_url_oracle(SchemeNetloc, Path, Query) when is_binary(SchemeNetloc) -> 38 | construct_url_oracle(binary_to_list(SchemeNetloc), Path, Query); 39 | construct_url_oracle(SchemeNetloc, Path, Query) when is_binary(Path) -> 40 | construct_url_oracle(SchemeNetloc, binary_to_list(Path), Query); 41 | construct_url_oracle(SchemeNetloc, Path, Query) when is_list(SchemeNetloc), 42 | is_list(Path) -> 43 | {S, N, P1, _, _} = mochiweb_util:urlsplit(SchemeNetloc), 44 | {_, _, P2, _, _} = mochiweb_util:urlsplit(Path), 45 | P = path_cat(P1, P2), 46 | urlunsplit(S, N, P, Query). 47 | 48 | urlunsplit(S, N, P, Query) -> 49 | Q = mochiweb_util:urlencode(Query), 50 | mochiweb_util:urlunsplit({S, N, P, Q, []}). 51 | 52 | path_cat(P1, P2) -> 53 | UL = lists:append(path_fix(P1), path_fix(P2)), 54 | ["/"++U || U <- UL]. 55 | 56 | path_fix(S) -> 57 | PS = mochiweb_util:path_split(S), 58 | path_fix(PS, []). 59 | 60 | path_fix({[], []}, Acc) -> 61 | lists:reverse(Acc); 62 | path_fix({[], T}, Acc) -> 63 | path_fix(mochiweb_util:path_split(T), Acc); 64 | path_fix({H, T}, Acc) -> 65 | path_fix(mochiweb_util:path_split(T), [H|Acc]). 66 | 67 | %%%%%%%%%%%%%%%%%% 68 | %%% Generators %%% 69 | %%%%%%%%%%%%%%%%%% 70 | scheme() -> 71 | oneof([<<"http">>, <<"https">>]). 72 | 73 | host() -> 74 | oneof([<<"localhost:9839">>, <<"kivra.com">>, <<"api.kivra.com">>, 75 | <<"kivra:80">>, <<"kivra">>, <<"1.0.2.3:47">>, <<"1.9.0.2">>, 76 | <<"kivra.com/user">>]). 77 | path() -> 78 | ?LET(PathElems, list(non_empty(utf8_filtered())), 79 | begin 80 | iolist_to_binary(lists:join(<<"/">>, PathElems)) 81 | end). 82 | 83 | utf8_filtered() -> 84 | ?LET(V, utf8(), 85 | binary:replace(V, [<<"#">>, <<"/">>, <<"?">>, 86 | %% To avoid mochiweb_util:urlsplit bugs 87 | <<"+">>, <<":">> 88 | ], <<>>, [global])). 89 | 90 | query_elements() -> 91 | list({utf8(), utf8()}). 92 | 93 | %%%_* Emacs ============================================================ 94 | %%% Local Variables: 95 | %%% allout-layout: t 96 | %%% erlang-indent-level: 2 97 | %%% End: 98 | -------------------------------------------------------------------------------- /test/prop_restc_body.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Verifies that encoding/decoding results in the same value. 2 | %%% 3 | %%% To make it a bit easier to test this only tests non-empty binary keys and 4 | %%% values, as percent encoding changes the output otherwise (as it should). 5 | %%% @end 6 | -module(prop_restc_body). 7 | -include_lib("proper/include/proper.hrl"). 8 | 9 | prop_json_as_maps() -> 10 | ?FORALL( 11 | Object, 12 | object(), 13 | Object =:= 14 | restc_body:decode( 15 | <<"application/json">>, 16 | restc_body:encode(json, Object), 17 | [return_maps] 18 | ) 19 | ). 20 | 21 | prop_json_as_proplists() -> 22 | ?FORALL( 23 | Object, 24 | proplist(), 25 | Object =:= 26 | restc_body:decode( 27 | <<"application/json">>, 28 | restc_body:encode(json, Object), 29 | [] 30 | ) 31 | ). 32 | 33 | prop_percent_as_maps() -> 34 | ?FORALL( 35 | Object, 36 | object(), 37 | Object =:= 38 | restc_body:decode( 39 | <<"application/x-www-form-urlencoded">>, 40 | restc_body:encode(percent, Object), 41 | [return_maps] 42 | ) 43 | ). 44 | 45 | prop_percent_as_proplists() -> 46 | ?FORALL( 47 | Object, 48 | proplist(), 49 | Object =:= 50 | restc_body:decode( 51 | <<"application/x-www-form-urlencoded">>, 52 | restc_body:encode(percent, Object), 53 | [] 54 | ) 55 | ). 56 | 57 | %%%%%%%%%%%%%%%%%% 58 | %%% Generators %%% 59 | %%%%%%%%%%%%%%%%%% 60 | object() -> 61 | map(non_empty_text(), non_empty_text()). 62 | 63 | proplist() -> 64 | list({non_empty_text(), non_empty_text()}). 65 | 66 | non_empty_text() -> 67 | non_empty(utf8()). 68 | -------------------------------------------------------------------------------- /test/restc_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%%_* Module declaration =============================================== 2 | -module(restc_SUITE). 3 | -compile([export_all]). 4 | 5 | -include_lib("stdlib/include/assert.hrl"). 6 | 7 | all() -> 8 | [ {group, upstream_pipethrough} 9 | , {group, expect_status} 10 | , {group, retries} 11 | , {group, request_body_encoding} 12 | , {group, response_body_decoding} 13 | , {group, accept_header_and_type} 14 | ]. 15 | 16 | groups() -> 17 | [{ upstream_pipethrough, 18 | [ upstream_returning_status_code__making_request__returns_the_same_status_code 19 | , upstream_returning_headers__making_request__returns_the_same_headers 20 | , upstream_returning_error__making_request__returns_the_same_error 21 | ]} 22 | ,{ expect_status, 23 | [ no_expected_status__making_request__returns_upstream_status 24 | , upstream_returning_unexpected_status__making_request__returns_error 25 | , upstream_returning_unexpected_status__making_request__returns_the_unexpected_status 26 | ]} 27 | ,{ retries, 28 | [ no_options__making_request__tries_only_once 29 | , two_retries_in_options__making_request__calls_three_times 30 | , retries_not_succeeding__making_request__returns_error 31 | , retries_eventually_succeeding__making_request__returns_result 32 | ]} 33 | ,{ request_body_encoding, 34 | [ type_is_json__making_request__sends_json_encoded_body 35 | , type_is_percent__making_request__sends_percent_encoded_body 36 | , type_is_xml__making_request__sends_xml_encoded_body 37 | , method_is_post__making_request__body_is_encoded 38 | , method_is_put__making_request__body_is_encoded 39 | , method_is_patch__making_request__body_is_encoded 40 | , method_is_something_else__making_request__body_is_empty_list 41 | ]} 42 | ,{ response_body_decoding, 43 | [ no_content_type_returned__making_json_request__decode_response_as_json 44 | , content_type_returned__making_json_request__decode_response_as_content_type 45 | , return_maps 46 | ]} 47 | ,{ accept_header_and_type, 48 | [ type_is_json__making_request_with_no_headers__accept_header_is_json 49 | , type_is_xml__making_request_with_no_headers__accept_header_is_xml 50 | , type_is_percent__making_request_with_no_headers__accept_header_is_json 51 | , type_is_png__making_request_with_no_headers__accept_header_is_png 52 | , type_is_json__making_request_with_xml_accept_header__accept_header_overrides_type 53 | ]} 54 | ]. 55 | 56 | init_per_testcase(_TestCase, Config) -> 57 | meck:new(hackney), 58 | Config. 59 | end_per_testcase(_TestCase, _Config) -> meck:unload(hackney). 60 | 61 | %%%_ * Tests - given, when, then --------------------------------------- 62 | upstream_returning_status_code__making_request__returns_the_same_status_code(_Config) -> 63 | mock_hackney_success(200), 64 | ExpectedStatus = 200, 65 | 66 | {ok, ActualStatus, _, _} = restc:request(<<"http://any_url.com">>), 67 | 68 | ?assertEqual(ExpectedStatus, ActualStatus). 69 | 70 | upstream_returning_headers__making_request__returns_the_same_headers(_Config) -> 71 | ExpectedHeaders = [{<<"Content-Type">>, <<"text/html">>}], 72 | mock_hackney_success(200, ExpectedHeaders, <<>>), 73 | 74 | {ok, _, ActualHeaders, _} = restc:request(<<"http://any_url.com">>), 75 | 76 | ?assertEqual(ExpectedHeaders, ActualHeaders). 77 | 78 | upstream_returning_error__making_request__returns_the_same_error(_Config) -> 79 | mock_hackney_error(some_error), 80 | ExpectedError = some_error, 81 | 82 | {error, ActualError} = restc:request(<<"http://any_url.com">>), 83 | 84 | ?assertEqual(ExpectedError, ActualError). 85 | 86 | no_expected_status__making_request__returns_upstream_status(_Config) -> 87 | mock_hackney_success(201), 88 | ExpectedStatus = [], 89 | ExpectedStatusCode = 201, 90 | 91 | {ok, ActualStatus, _, _} = restc:request(get, <<"http://any_url.com">>, ExpectedStatus), 92 | 93 | ?assertMatch(ExpectedStatusCode, ActualStatus). 94 | 95 | upstream_returning_unexpected_status__making_request__returns_error(_Config) -> 96 | mock_hackney_success(500), 97 | 98 | Result = restc:request(get, <<"http://any_url.com">>, [200]), 99 | 100 | ?assertMatch({error, _, _, _}, Result). 101 | 102 | upstream_returning_unexpected_status__making_request__returns_the_unexpected_status(_Config) -> 103 | mock_hackney_success(500, [], <<>>), 104 | ExpectedStatus = 500, 105 | 106 | {error, ActualStatus, _, _} = restc:request(get, <<"http://any_url.com">>, [200]), 107 | 108 | ?assertEqual(ExpectedStatus, ActualStatus). 109 | 110 | no_options__making_request__tries_only_once(_Config) -> 111 | mock_hackney_success(500), 112 | ExpectedCalls = 1, 113 | 114 | restc:request(get, <<"http://any_url.com">>, [200]), 115 | 116 | ?assertEqual(ExpectedCalls, meck:num_calls(hackney, request, '_')). 117 | 118 | two_retries_in_options__making_request__calls_three_times(_Config) -> 119 | mock_hackney_success(500), 120 | Options = [{retries, 2}], 121 | ExpectedCalls = 3, 122 | 123 | restc:request(get, json, <<"http://any_url.com">>, [200], [], [], Options), 124 | 125 | ?assertEqual(ExpectedCalls, meck:num_calls(hackney, request, '_')). 126 | 127 | retries_not_succeeding__making_request__returns_error(_Config) -> 128 | mock_hackney_success(500), 129 | Options = [{retries, 2}], 130 | 131 | Result = restc:request(get, json, <<"http://any_url.com">>, [200], [], [], Options), 132 | 133 | ?assertMatch({error, _, _, _}, Result). 134 | 135 | retries_eventually_succeeding__making_request__returns_result(_Config) -> 136 | mock_hackney_eventual_success(200, 2), 137 | Options = [{retries, 2}], 138 | ExpectedResult = {ok, 200, [], []}, 139 | 140 | ActualResult = restc:request(get, json, <<"http://any_url.com">>, [200], [], [], Options), 141 | 142 | ?assertEqual(ExpectedResult, ActualResult). 143 | 144 | type_is_json__making_request__sends_json_encoded_body(_Config) -> 145 | mock_hackney_success(200), 146 | Body = [{<<"any">>, <<"data">>}], 147 | 148 | restc:request(post, json, <<"http://any_url.com">>, [200], [], Body), 149 | 150 | EncodedBody = meck:capture(first, hackney, request, '_', 4, '_'), 151 | ?assert(jsx:is_json(EncodedBody)). 152 | 153 | type_is_percent__making_request__sends_percent_encoded_body(_Config) -> 154 | mock_hackney_success(200), 155 | Body = [{<<"any">>, <<"data">>}], 156 | ExpectedBody = list_to_binary(mochiweb_util:urlencode(Body)), 157 | 158 | restc:request(post, percent, <<"http://any_url.com">>, [200], [], Body), 159 | 160 | ActualBody = meck:capture(first, hackney, request, '_', 4, '_'), 161 | ?assertEqual(ExpectedBody, ActualBody). 162 | 163 | type_is_xml__making_request__sends_xml_encoded_body(_Config) -> 164 | mock_hackney_success(200), 165 | Body = [{any, [{something, ["data"]}]}], 166 | ExpectedBody = lists:flatten(xmerl:export_simple(Body, xmerl_xml)), 167 | 168 | restc:request(post, xml, <<"http://any_url.com">>, [200], [], Body), 169 | 170 | ActualBody = meck:capture(first, hackney, request, '_', 4, '_'), 171 | ?assertEqual(ExpectedBody, ActualBody). 172 | 173 | method_is_post__making_request__body_is_encoded(_Config) -> 174 | mock_hackney_success(200), 175 | meck:new(jsx, [passthrough]), 176 | 177 | restc:request(post, json, <<"http://any_url.com">>, [200], [], [{<<"any">>, <<"data">>}]), 178 | 179 | ?assert(meck:called(jsx, encode, '_')). 180 | 181 | method_is_put__making_request__body_is_encoded(_Config) -> 182 | mock_hackney_success(200), 183 | meck:new(jsx, [passthrough]), 184 | 185 | restc:request(put, json, <<"http://any_url.com">>, [200], [], [{<<"any">>, <<"data">>}]), 186 | 187 | ?assert(meck:called(jsx, encode, '_')). 188 | 189 | method_is_patch__making_request__body_is_encoded(_Config) -> 190 | mock_hackney_success(200), 191 | meck:new(jsx, [passthrough]), 192 | 193 | restc:request(patch, json, <<"http://any_url.com">>, [200], [], [{<<"any">>, <<"data">>}]), 194 | 195 | ?assert(meck:called(jsx, encode, '_')). 196 | 197 | method_is_something_else__making_request__body_is_empty_list(_Config) -> 198 | mock_hackney_success(200), 199 | ExpectedBody = [], 200 | 201 | restc:request(delete, json, <<"http://any_url.com">>, [200], [], [{<<"any">>, <<"data">>}]), 202 | 203 | ActualBody = meck:capture(first, hackney, request, '_', 4, '_'), 204 | ?assertEqual(ExpectedBody, ActualBody). 205 | 206 | no_content_type_returned__making_json_request__decode_response_as_json(_Config) -> 207 | ExpectedResponseBody = [{<<"any">>, <<"data">>}], 208 | mock_hackney_success(200, [], jsx:encode(ExpectedResponseBody)), 209 | 210 | {ok, _, _, ActualResponseBody} = restc:request(get, <<"http://any_url.com">>), 211 | 212 | ?assertEqual(ExpectedResponseBody, ActualResponseBody). 213 | 214 | content_type_returned__making_json_request__decode_response_as_content_type(_Config) -> 215 | ResponseBody = [{any, [{something, ["data"]}]}], 216 | EncodedResponseBody = list_to_binary(lists:flatten(xmerl:export_simple(ResponseBody, xmerl_xml))), 217 | ExpectedResponseBody = {"any", [], [{"something", [], ["data"]}]}, 218 | mock_hackney_success(200, [{<<"Content-Type">>, <<"application/xml">>}], EncodedResponseBody), 219 | 220 | {ok, _, _, ActualResponseBody} = restc:request(get, <<"http://any_url.com">>), 221 | 222 | ?assertEqual(ExpectedResponseBody, ActualResponseBody). 223 | 224 | return_maps(_Config) -> 225 | ResponseBody = #{<<"first_level">> => #{<<"second_level">> => [<<"a">>, <<"b">>]}}, 226 | EncodedResponseBody = jsx:encode(ResponseBody), 227 | mock_hackney_success(200, [{<<"Content-Type">>, <<"application/json">>}], EncodedResponseBody), 228 | 229 | {ok, _, _, ActualResponseBody} = 230 | restc:request(get, na, <<"http://any_url.com">>, [], [], <<>>, [return_maps]), 231 | 232 | ?assertEqual(ResponseBody, ActualResponseBody). 233 | 234 | type_is_json__making_request_with_no_headers__accept_header_is_json(_Config) -> 235 | mock_hackney_success(200), 236 | 237 | restc:request(get, json, <<"http://any_url.com">>, [200]), 238 | 239 | ActualHeaders = meck:capture(first, hackney, request, '_', 3, '_'), 240 | ?assertMatch([{<<"accept">>, <<"application/json", _/binary>>}, _], ActualHeaders). 241 | 242 | type_is_xml__making_request_with_no_headers__accept_header_is_xml(_Config) -> 243 | mock_hackney_success(200), 244 | 245 | restc:request(get, xml, <<"http://any_url.com">>, [200]), 246 | 247 | ActualHeaders = meck:capture(first, hackney, request, '_', 3, '_'), 248 | ?assertMatch([{<<"accept">>, <<"application/xml", _/binary>>}, _], ActualHeaders). 249 | 250 | type_is_percent__making_request_with_no_headers__accept_header_is_json(_Config) -> 251 | mock_hackney_success(200), 252 | 253 | restc:request(get, percent, <<"http://any_url.com">>, [200]), 254 | 255 | ActualHeaders = meck:capture(first, hackney, request, '_', 3, '_'), 256 | ?assertMatch([{<<"accept">>, <<"application/json", _/binary>>}, _], ActualHeaders). 257 | 258 | type_is_png__making_request_with_no_headers__accept_header_is_png(_Config) -> 259 | mock_hackney_success(200), 260 | 261 | restc:request(get, png, <<"http://any_url.com">>, [200]), 262 | 263 | ActualHeaders = meck:capture(first, hackney, request, '_', 3, '_'), 264 | ?assertMatch([{<<"accept">>, <<"image/png", _/binary>>}, _], ActualHeaders). 265 | 266 | type_is_json__making_request_with_xml_accept_header__accept_header_overrides_type(_Config) -> 267 | mock_hackney_success(200), 268 | Uri = <<"http://any_url.com">>, 269 | Headers = [{<<"Accept">>, <<"application/xml">>}], 270 | restc:request(get, json, Uri, [200], Headers), 271 | 272 | ActualHeaders = meck:capture(first, hackney, request, '_', 3, '_'), 273 | ?assertMatch([{<<"accept">>, <<"application/xml", _/binary>>}, _], ActualHeaders). 274 | 275 | %%%_ * Helpers --------------------------------------------------------- 276 | mock_hackney_success(Code) -> mock_hackney_success(Code, [], <<>>). 277 | 278 | mock_hackney_success(Code, Headers, Body) -> 279 | meck:expect(hackney, request, ['_', '_', '_', '_', '_'], meck:val({ok, Code, Headers, client})), 280 | meck:expect(hackney, body, fun(client) -> {ok, Body} end). 281 | 282 | mock_hackney_error(Error) -> 283 | meck:expect(hackney, request, ['_', '_', '_', '_', '_'], meck:val({error, Error})). 284 | 285 | mock_hackney_eventual_success(Code, AmountOfErrors) -> 286 | ErrorCalls = error_calls(AmountOfErrors), 287 | meck:expect(hackney, request, ['_', '_', '_', '_', '_'], 288 | meck:seq(ErrorCalls ++ [{ok, Code, [], client}])), 289 | meck:expect(hackney, body, fun(client) -> {ok, <<>>} end). 290 | 291 | error_calls(0) -> []; 292 | error_calls(N) -> error_calls(N, []). 293 | error_calls(0, Acc) -> Acc; 294 | error_calls(N, Acc) -> 295 | error_calls(N-1, [{error, some_error}|Acc]). 296 | --------------------------------------------------------------------------------