Implement the initial API

This commit is contained in:
Gavin M. Roy 2016-01-21 23:04:34 -05:00
parent 66d066ed7e
commit 56c320572f
15 changed files with 591 additions and 415 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true
[*.{erl,hrl,src}]
indent_style = space
indent_size = 4
[*.{yml}]
indent_style = space
indent_size = 2

54
.gitignore vendored
View file

@ -1,62 +1,12 @@
# Created by .ignore support plugin (hsz.mobi)
### Erlang template
.eunit .eunit
deps deps
*.o *.o
*.beam
*.plt *.plt
erl_crash.dump erl_crash.dump
ebin
rel/example_project
.concrete/DEV_MODE
.rebar .rebar
TEST*.xml TEST*.xml
_build _build
.idea
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
*.iml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle:
# .idea/gradle.xml
# .idea/libraries
# Mongo Explorer plugin:
# .idea/mongoSettings.xml
## File-based project format:
*.ipr *.ipr
*.iws *.iws
rebar.lock
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties

View file

@ -1,32 +0,0 @@
PROJECT=urilib
REBAR=bin/rebar
all: get-deps compile
build-plt:
@dialyzer --build_plt --output_plt ~/.$(PROJECT).plt \
--apps kernel stdlib erts inets edoc
clean:
@( $(REBAR) clean )
compile:
@( $(REBAR) compile )
dialyze:
@dialyzer ebin/*.beam --plt ~/.$(PROJECT).plt
doc:
@echo "Running rebar doc..."
@$(REBAR) skip_deps=true doc
eunit:
@echo "Running rebar eunit..."
@$(REBAR) skip_deps=true eunit
get-deps:
@( $(REBAR) get-deps )
test: all eunit
.PHONY: dialyze doc eunit

132
README.md
View file

@ -9,137 +9,9 @@ Example Usage
```erlang ```erlang
-include_lib("urilib.h"). -include_lib("urilib.h").
URI = urilib:parse_uri("http://foo:bar@www.google.com/search?baz=qux#corgie"), URI = urilib:parse("http://foo:bar@www.google.com/search?baz=qux#corgie"),
io:format("Parsed URI: ~p~n", [URI]). io:format("Parsed URI: ~p~n", [URI]).
URL = urllib:build(#url{scheme=http, host="www.google.com", path="/search", query=[{"foo", "bar"}], fragment="baz"}), URL = urllib:build({http, undefined, undefined, "www.google.com", undefined, "/search", [{"foo", "bar"}], "baz"}),
io:format("Built URL: ~s~n", [URL]). io:format("Built URL: ~s~n", [URL]).
``` ```
Records
-------
#### authority ####
```erlang
#{host :: string(), port :: integer()}).
```
#### userinfo ####
```erlang
#{username :: string(), password :: string()}).
```
#### uri ####
```erlang
#{scheme :: atom(),
userinfo :: #userinfo{},
authority :: #authority{},
path :: string(),
query :: list(),
fragment :: string()}).
```
#### url ####
```erlang
#{scheme :: atom(),
username :: string(),
password :: string(),
host :: string(),
port :: integer(),
path :: string(),
query :: list(),
fragment :: string()}).
```
API
---
<a name="build-1"></a>
### build/1 ###
```erlang
build(Uri::Value) -> URI
```
<ul class="definitions"><li><code>Value = #uri{} | #url{}</code></li><li><code>URI = string()</code></li></ul>
Returns a URI from the record passed in.
<a name="decode-1"></a>
### decode/1 ###
```erlang
decode(Value) -> DecodedValue
```
<ul class="definitions"><li><code>Value = string()</code></li><li><code>DecodeValue = string()</code></li></ul>
Decode a percent encoded string value.
<a name="decode_plus-1"></a>
### decode_plus/1 ###
```erlang
decode_plus(Value) -> DecodedValue
```
<ul class="definitions"><li><code>Value = string()</code></li><li><code>DecodeValue = string()</code></li></ul>
Decode a percent encoded string value that uses pluses for spaces.
Note: The use of plus for space is defined in RFC-1630 but does not appear
in RFC-3986.
<a name="encode-1"></a>
### encode/1 ###
```erlang
encode(Value) -> EncodedValue
```
<ul class="definitions"><li><code>Value = string()</code></li><li><code>EncodedValue = string()</code></li></ul>
Percent encode a string value.
<a name="encode_plus-1"></a>
### encode_plus/1 ###
```erlang
encode_plus(Value) -> EncodedValue
```
<ul class="definitions"><li><code>Value = string()</code></li><li><code>EncodedValue = string()</code></li></ul>
Percent encode a string value similar to encode/1, but encodes spaces with a
plus (+) instead of %20. This function can be used for encoding query arguments.
Note: The use of plus for space is defined in RFC-1630 but does not appear
in RFC-3986.
<a name="parse_uri-1"></a>
### parse_uri/1 ###
```erlang
parse_uri(URI) -> ParsedURI
```
<ul class="definitions"><li><code>URI = string()</code></li><li><code>ParsedURI = #uri{}</code></li></ul>
Parse a URI string returning the parsed data as a record
<a name="parse_url-1"></a>
### parse_url/1 ###
```erlang
parse_url(URL) -> ParsedURL
```
<ul class="definitions"><li><code>URI = string()</code></li><li><code>ParsedURL = #url{}</code></li></ul>
Parse a URL string returning the parsed data as a record

3
doc/edoc-info Normal file
View file

@ -0,0 +1,3 @@
%% encoding: UTF-8
{application,urilib}.
{modules,[urilib]}.

BIN
doc/erlang.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

17
doc/index.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>urllib</title>
</head>
<frameset cols="20%,80%">
<frame src="modules-frame.html" name="modulesFrame" title="">
<frame src="overview-summary.html" name="overviewFrame" title="">
<noframes>
<h2>This page uses frames</h2>
<p>Your browser does not accept frames.
<br>You should go to the <a href="overview-summary.html">non-frame version</a> instead.
</p>
</noframes>
</frameset>
</html>

12
doc/modules-frame.html Normal file
View file

@ -0,0 +1,12 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>urllib</title>
<link rel="stylesheet" type="text/css" href="stylesheet.css" title="EDoc">
</head>
<body bgcolor="white">
<h2 class="indextitle">Modules</h2>
<table width="100%" border="0" summary="list of modules">
<tr><td><a href="urilib.html" target="overviewFrame" class="module">urilib</a></td></tr></table>
</body>
</html>

16
doc/overview-summary.html Normal file
View file

@ -0,0 +1,16 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>urllib</title>
<link rel="stylesheet" type="text/css" href="stylesheet.css" title="EDoc">
</head>
<body bgcolor="white">
<div class="navbar"><a name="#navbar_top"></a><table width="100%" border="0" cellspacing="0" cellpadding="2" summary="navigation bar"><tr><td><a href="overview-summary.html" target="overviewFrame">Overview</a></td><td><a href="http://www.erlang.org/"><img src="erlang.png" align="right" border="0" alt="erlang logo"></a></td></tr></table></div>
<h1>urllib</h1>
<hr>
<div class="navbar"><a name="#navbar_bottom"></a><table width="100%" border="0" cellspacing="0" cellpadding="2" summary="navigation bar"><tr><td><a href="overview-summary.html" target="overviewFrame">Overview</a></td><td><a href="http://www.erlang.org/"><img src="erlang.png" align="right" border="0" alt="erlang logo"></a></td></tr></table></div>
<p><i>Generated by EDoc, Jan 21 2016, 22:59:50.</i></p>
</body>
</html>

55
doc/stylesheet.css Normal file
View file

@ -0,0 +1,55 @@
/* standard EDoc style sheet */
body {
font-family: Verdana, Arial, Helvetica, sans-serif;
margin-left: .25in;
margin-right: .2in;
margin-top: 0.2in;
margin-bottom: 0.2in;
color: #000000;
background-color: #ffffff;
}
h1,h2 {
margin-left: -0.2in;
}
div.navbar {
background-color: #add8e6;
padding: 0.2em;
}
h2.indextitle {
padding: 0.4em;
background-color: #add8e6;
}
h3.function,h3.typedecl {
background-color: #add8e6;
padding-left: 1em;
}
div.spec {
margin-left: 2em;
background-color: #eeeeee;
}
a.module {
text-decoration:none
}
a.module:hover {
background-color: #eeeeee;
}
ul.definitions {
list-style-type: none;
}
ul.index {
list-style-type: none;
background-color: #eeeeee;
}
/*
* Minor style tweaks
*/
ul {
list-style-type: square;
}
table {
border-collapse: collapse;
}
td {
padding: 3
}

135
doc/urilib.html Normal file
View file

@ -0,0 +1,135 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Module urilib</title>
<link rel="stylesheet" type="text/css" href="stylesheet.css" title="EDoc">
</head>
<body bgcolor="white">
<div class="navbar"><a name="#navbar_top"></a><table width="100%" border="0" cellspacing="0" cellpadding="2" summary="navigation bar"><tr><td><a href="overview-summary.html" target="overviewFrame">Overview</a></td><td><a href="http://www.erlang.org/"><img src="erlang.png" align="right" border="0" alt="erlang logo"></a></td></tr></table></div>
<hr>
<h1>Module urilib</h1>
<ul class="index"><li><a href="#description">Description</a></li><li><a href="#types">Data Types</a></li><li><a href="#index">Function Index</a></li><li><a href="#functions">Function Details</a></li></ul>urilib is a RFC-3986 URI Library for Erlang.
<p>Copyright © 2016</p>
<p><b>Authors:</b> Gavin M. Roy (<a href="mailto:gavinmroy@gmail.com"><tt>gavinmroy@gmail.com</tt></a>).</p>
<h2><a name="description">Description</a></h2>urilib is a RFC-3986 URI Library for Erlang
<h2><a name="types">Data Types</a></h2>
<h3 class="typedecl"><a name="type-authority">authority()</a></h3>
<p><pre>authority() = {<a href="#type-userinfo">userinfo()</a>, <a href="#type-host">host()</a>, <a href="#type-tcp_port">tcp_port()</a>}</pre></p>
<h3 class="typedecl"><a name="type-fragment">fragment()</a></h3>
<p><pre>fragment() = string() | undefined</pre></p>
<h3 class="typedecl"><a name="type-host">host()</a></h3>
<p><pre>host() = string()</pre></p>
<h3 class="typedecl"><a name="type-password">password()</a></h3>
<p><pre>password() = string() | undefined</pre></p>
<h3 class="typedecl"><a name="type-path">path()</a></h3>
<p><pre>path() = string()</pre></p>
<h3 class="typedecl"><a name="type-query">query()</a></h3>
<p><pre>query() = [tuple() | string()] | undefined</pre></p>
<h3 class="typedecl"><a name="type-scheme">scheme()</a></h3>
<p><pre>scheme() = http | https | atom()</pre></p>
<h3 class="typedecl"><a name="type-tcp_port">tcp_port()</a></h3>
<p><pre>tcp_port() = integer()</pre></p>
<h3 class="typedecl"><a name="type-uri">uri()</a></h3>
<p><pre>uri() = {<a href="#type-scheme">scheme()</a>, <a href="#type-authority">authority()</a>, <a href="#type-path">path()</a>, <a href="#type-query">query()</a>, <a href="#type-fragment">fragment()</a>}</pre></p>
<h3 class="typedecl"><a name="type-url">url()</a></h3>
<p><pre>url() =
{<a href="#type-scheme">scheme()</a>,
<a href="#type-username">username()</a>,
<a href="#type-password">password()</a>,
<a href="#type-host">host()</a>,
<a href="#type-tcp_port">tcp_port()</a>,
<a href="#type-path">path()</a>,
<a href="#type-query">query()</a>,
<a href="#type-fragment">fragment()</a>}</pre></p>
<h3 class="typedecl"><a name="type-userinfo">userinfo()</a></h3>
<p><pre>userinfo() = {<a href="#type-username">username()</a>, <a href="#type-password">password()</a>} | undefined</pre></p>
<h3 class="typedecl"><a name="type-username">username()</a></h3>
<p><pre>username() = string() | undefined</pre></p>
<h2><a name="index">Function Index</a></h2>
<table width="100%" border="1" cellspacing="0" cellpadding="2" summary="function index"><tr><td valign="top"><a href="#build-1">build/1</a></td><td>Build a URI.</td></tr>
<tr><td valign="top"><a href="#parse-1">parse/1</a></td><td>Parse a URI.</td></tr>
<tr><td valign="top"><a href="#parse-2">parse/2</a></td><td>Parse a URI, returning the result as either a <code><a href="#type-uri">uri()</a></code> or <code><a href="#type-url">url()</a></code>.</td></tr>
<tr><td valign="top"><a href="#percent_decode-1">percent_decode/1</a></td><td>Decode a percent encoded string value.</td></tr>
<tr><td valign="top"><a href="#percent_encode-1">percent_encode/1</a></td><td>Percent encode a string value.</td></tr>
<tr><td valign="top"><a href="#plus_decode-1">plus_decode/1</a></td><td>Decode a percent encoded string value that uses pluses for spaces.</td></tr>
<tr><td valign="top"><a href="#plus_encode-1">plus_encode/1</a></td><td>Percent encode a string value similar to encode/1, but encodes spaces with a
plus (<code>+</code>) instead of <code>%20</code>.</td></tr>
</table>
<h2><a name="functions">Function Details</a></h2>
<h3 class="function"><a name="build-1">build/1</a></h3>
<div class="spec">
<p><pre>build(Value :: <a href="#type-uri">uri()</a> | <a href="#type-url">url()</a>) -&gt; string()</pre></p>
</div><p>Build a URI</p>
<h3 class="function"><a name="parse-1">parse/1</a></h3>
<div class="spec">
<p><pre>parse(Value :: string()) -&gt; <a href="#type-uri">uri()</a></pre></p>
</div><p>Parse a URI</p>
<h3 class="function"><a name="parse-2">parse/2</a></h3>
<div class="spec">
<p><pre>parse(Value :: string(), Return :: uri | url) -&gt; <a href="#type-uri">uri()</a></pre></p>
</div><p>Parse a URI, returning the result as either a <code><a href="#type-uri">uri()</a></code> or <code><a href="#type-url">url()</a></code>.</p>
<h3 class="function"><a name="percent_decode-1">percent_decode/1</a></h3>
<div class="spec">
<p><pre>percent_decode(Value :: string()) -&gt; string()</pre></p>
</div><p>Decode a percent encoded string value.</p>
<h3 class="function"><a name="percent_encode-1">percent_encode/1</a></h3>
<div class="spec">
<p><pre>percent_encode(Value :: string()) -&gt; string()</pre></p>
</div><p>Percent encode a string value.</p>
<h3 class="function"><a name="plus_decode-1">plus_decode/1</a></h3>
<div class="spec">
<p><pre>plus_decode(Value :: string()) -&gt; string()</pre></p>
</div><p><p>Decode a percent encoded string value that uses pluses for spaces.</p>
Note: The use of plus for space is defined in RFC-1630 but does not appear
in RFC-3986.</p>
<h3 class="function"><a name="plus_encode-1">plus_encode/1</a></h3>
<div class="spec">
<p><pre>plus_encode(Value :: string()) -&gt; string()</pre></p>
</div><p><p>Percent encode a string value similar to encode/1, but encodes spaces with a
plus (<code>+</code>) instead of <code>%20</code>. This function can be used for encoding query arguments.</p>
Note: The use of plus for space is defined in RFC-1630 but does not appear in RFC-3986.</p>
<hr>
<div class="navbar"><a name="#navbar_bottom"></a><table width="100%" border="0" cellspacing="0" cellpadding="2" summary="navigation bar"><tr><td><a href="overview-summary.html" target="overviewFrame">Overview</a></td><td><a href="http://www.erlang.org/"><img src="erlang.png" align="right" border="0" alt="erlang logo"></a></td></tr></table></div>
<p><i>Generated by EDoc, Jan 21 2016, 22:59:50.</i></p>
</body>
</html>

View file

@ -1,6 +1,7 @@
%% ============================================================================= %% =============================================================================
%% @author Gavin M. Roy <gavinmroy@gmail.com> %% @author Gavin M. Roy <gavinmroy@gmail.com>
%% @copyright 2016 %% @copyright 2016
%% @doc urilib is a RFC-3986 URI Library for Erlang
%% @end %% @end
%% ============================================================================= %% =============================================================================

View file

@ -4,6 +4,8 @@
{clean_files, ["*.eunit", "ebin/*.beam"]}. {clean_files, ["*.eunit", "ebin/*.beam"]}.
{dialyzer, [{plt_extra_apps, [kernel, stdlib, erts, inets, edoc]}]}. {dialyzer, [{plt_extra_apps, [kernel, stdlib, erts, inets, edoc]}]}.
{erl_opts, [fail_on_warning]}. {erl_opts, [fail_on_warning]}.
{edoc_opts, [{includes, "include"}, {title, "urllib"}, {pretty_printer, erl_pp}]}.
{eunit_exclude_deps, true}. {eunit_exclude_deps, true}.
{eunit_opts, [verbose, {skip_deps, true}]}. {eunit_opts, [verbose, {skip_deps, true}]}.
{lib_dirs,["deps"]}.
{minimum_otp_vsn, "17.5"}. {minimum_otp_vsn, "17.5"}.

View file

@ -7,12 +7,25 @@
-module(urilib). -module(urilib).
-export([build/1, -export([build/1,
parse_uri/1, parse/1,
parse_url/1, parse/2,
encode/1, percent_decode/1,
encode_plus/1, percent_encode/1,
decode/1, plus_decode/1,
decode_plus/1]). plus_encode/1]).
-export_type([scheme/0,
host/0,
tcp_port/0,
username/0,
password/0,
userinfo/0,
authority/0,
path/0,
query/0,
fragment/0,
uri/0,
url/0]).
-include("urilib.hrl"). -include("urilib.hrl").
@ -21,185 +34,249 @@
-compile(export_all). -compile(export_all).
-endif. -endif.
-spec build(#uri{} | #url{}) -> string(). -type scheme() :: http | https | atom().
%% @spec build(Value) -> URI -type host() :: string().
%% where -type tcp_port() :: integer().
%% Value = #uri{} | #url{} -type username() :: string() | undefined.
%% URI = string() -type password() :: string() | undefined.
%% @doc Returns a URI from the record passed in. -type userinfo() :: {username(), password()} | undefined.
%% -type authority() :: {userinfo(), host(), tcp_port()}.
-type path() :: string().
-type query() :: [tuple() | string()] | undefined.
-type fragment() :: string() | undefined.
-type uri() :: {scheme(), authority(), path(), query(), fragment()}.
-type url() :: {scheme(), username(), password(), host(), tcp_port(), path(), query(), fragment()}.
-spec build(Value :: uri() | url()) -> string().
%% @doc Build a URI
%% @end %% @end
build(#uri{scheme=Scheme, userinfo=UserInfo, authority=Authority, build({Scheme, {undefined, Host, Port}, Path, Query, Fragment}) ->
path=Path, query=QArgs, fragment=Fragment}) -> build({Scheme, {{undefined, undefined}, Host, Port}, Path, Query, Fragment});
build({Scheme, {{Username, Password}, Host, Port}, Path, Query, Fragment}) ->
U1 = url_add_scheme(Scheme), U1 = url_add_scheme(Scheme),
U2 = url_maybe_add_user(UserInfo, U1), U2 = url_maybe_add_userinfo(Username, Password, U1),
U3 = url_add_host_and_port(Scheme, U3 = url_add_host_and_port(Scheme, Host, Port, U2),
Authority#authority.host,
Authority#authority.port, U2),
U4 = url_add_path(Path, U3), U4 = url_add_path(Path, U3),
U5 = url_maybe_add_qargs(QArgs, U4), U5 = url_maybe_add_qargs(Query, U4),
url_maybe_add_fragment(Fragment, U5);
build({Scheme, undefined, Host, Port, Path, Query, Fragment}) ->
build({Scheme, undefined, undefined, Host, Port, Path, Query, Fragment});
build({Scheme, Username, Password, Host, Port, Path, Query, Fragment}) ->
U1 = url_add_scheme(Scheme),
U2 = url_maybe_add_userinfo(Username, Password, U1),
U3 = url_add_host_and_port(Scheme, Host, Port, U2),
U4 = url_add_path(Path, U3),
U5 = url_maybe_add_qargs(Query, U4),
url_maybe_add_fragment(Fragment, U5). url_maybe_add_fragment(Fragment, U5).
-spec parse_uri(string()) -> #uri{}. -spec parse(string()) -> uri().
%% @spec parse_uri(URI) -> ParsedURI %% @doc Parse a URI
%% where
%% URI = string()
%% ParsedURI = #uri{}
%% @doc Parse a URI string returning the parsed data as a record
%% @end %% @end
parse_uri(URI) -> parse(Value) ->
case http_uri:parse(URI, [{scheme_defaults, http_uri:scheme_defaults()}, {fragment, true}]) of case http_uri:parse(Value, [{scheme_defaults, http_uri:scheme_defaults()}, {fragment, true}]) of
{ok, {Scheme, UserInfo, Host, Port, Path, Query, Fragment}} -> {ok, {Scheme, UserInfo, Host, Port, Path, Query, Fragment}} ->
#uri{scheme=Scheme, {Scheme, {parse_userinfo(UserInfo), Host, Port}, Path, parse_query(Query), parse_fragment(Fragment)};
userinfo=parse_userinfo(UserInfo),
authority=#authority{host=Host,
port=Port},
path=Path,
query=parse_query(Query),
fragment=Fragment};
{error, Reason} -> {error, Reason} {error, Reason} -> {error, Reason}
end. end.
-spec parse_url(string()) -> #url{}. -spec parse(string(), Return :: uri | url) -> uri().
%% @spec parse_url(URL) -> ParsedURL %% @doc Parse a URI, returning the result as either a {@type uri()} or {@type url()}.
%% where
%% URI = string()
%% ParsedURL = #url{}
%% @doc Parse a URL string returning the parsed data as a record
%% @end %% @end
parse_url(URL) -> parse(Value, uri) ->
case http_uri:parse(URL, [{scheme_defaults, http_uri:scheme_defaults()}, {fragment, true}]) of parse(Value);
parse(Value, url) ->
case http_uri:parse(Value, [{scheme_defaults, http_uri:scheme_defaults()}, {fragment, true}]) of
{ok, {Scheme, UserInfo, Host, Port, Path, Query, Fragment}} -> {ok, {Scheme, UserInfo, Host, Port, Path, Query, Fragment}} ->
User = parse_userinfo(UserInfo), {Username, Password} = parse_userinfo(UserInfo),
#url{scheme=Scheme, {Scheme, Username, Password, Host, Port, Path, parse_query(Query), parse_fragment(Fragment)};
username=User#userinfo.username,
password=User#userinfo.password,
host=Host,
port=Port,
path=Path,
query=parse_query(Query),
fragment=Fragment};
{error, Reason} -> {error, Reason} {error, Reason} -> {error, Reason}
end. end.
-spec encode(string()) -> string(). -spec percent_encode(string()) -> string().
%% @spec encode(Value) -> EncodedValue
%% where
%% Value = string()
%% EncodedValue = string()
%% @doc Percent encode a string value. %% @doc Percent encode a string value.
%% @end %% @end
encode(Value) -> percent_encode(Value) ->
edoc_lib:escape_uri(Value). edoc_lib:escape_uri(Value).
-spec encode_plus(string()) -> string(). -spec percent_decode(string()) -> string().
%% @spec encode_plus(Value) -> EncodedValue
%% where
%% Value = string()
%% EncodedValue = string()
%% @doc Percent encode a string value similar to encode/1, but encodes spaces with a
%% plus (+) instead of %20. This function can be used for encoding query arguments.
%%
%% Note: The use of plus for space is defined in RFC-1630 but does not appear
%% in RFC-3986.
%% @end
encode_plus(Value) ->
string:join([edoc_lib:escape_uri(V) || V <- string:tokens(Value, " ")], "+").
%% @spec decode(Value) -> DecodedValue
%% where
%% Value = string()
%% DecodeValue = string()
%% @doc Decode a percent encoded string value. %% @doc Decode a percent encoded string value.
%% @end %% @end
-spec decode(string()) -> string(). percent_decode(Value) ->
decode(Value) ->
http_uri:decode(Value). http_uri:decode(Value).
-spec decode_plus(string()) -> string(). -spec plus_encode(string()) -> string().
%% @spec decode_plus(Value) -> DecodedValue %% @doc Percent encode a string value similar to encode/1, but encodes spaces with a
%% where %% plus (`+') instead of `%20'. This function can be used for encoding query arguments.
%% Value = string() %%
%% DecodeValue = string() %% Note: The use of plus for space is defined in RFC-1630 but does not appear in RFC-3986.
%% @end
plus_encode(Value) ->
string:join([edoc_lib:escape_uri(V) || V <- string:tokens(Value, " ")], "+").
-spec plus_decode(string()) -> string().
%% @doc Decode a percent encoded string value that uses pluses for spaces. %% @doc Decode a percent encoded string value that uses pluses for spaces.
%% %%
%% Note: The use of plus for space is defined in RFC-1630 but does not appear %% Note: The use of plus for space is defined in RFC-1630 but does not appear
%% in RFC-3986. %% in RFC-3986.
%% @end %% @end
decode_plus(Value) -> plus_decode(Value) ->
string:join([http_uri:decode(V) || V <- string:tokens(Value, "+")], " "). string:join([http_uri:decode(V) || V <- string:tokens(Value, "+")], " ").
-spec parse_query(string()) -> []. %% Private Functions
-spec parse_fragment(string()) -> string() | undefined.
%% @private
parse_fragment([]) ->
undefined;
parse_fragment(Value) ->
Value.
-spec parse_query(string()) -> [tuple() | string()] | undefined.
%% @private %% @private
parse_query([]) -> [];
parse_query(Query) -> parse_query(Query) ->
case re:split(Query, "[&|?]", [{return, list}]) of QArgs = re:split(Query, "[&|?]", [{return, list}]),
[""] -> []; parse_query_result([split_query_arg(Arg) || Arg <- QArgs, Arg =/= []]).
QArgs -> [split_query_arg(Arg) || Arg <- QArgs, Arg =/= []]
end.
-spec parse_userinfo(string()) -> #userinfo{}. -spec parse_query_result(string()) -> [tuple() | string()] | undefined.
%% @private
parse_query_result([]) ->
undefined;
parse_query_result(QArgs) ->
QArgs.
-spec parse_userinfo(string()) -> userinfo().
%% @private %% @private
parse_userinfo([]) -> #userinfo{};
parse_userinfo(Value) -> parse_userinfo(Value) ->
case string:tokens(Value, ":") of parse_userinfo_result(string:tokens(Value, ":")).
[User, Password] -> #userinfo{username=User, password=Password};
[User] -> #userinfo{username=User}
end.
-spec split_query_arg(string()) -> {string(), string()}. -spec parse_userinfo_result(list()) -> userinfo().
%% @private
parse_userinfo_result([User, Password]) ->
{User, Password};
parse_userinfo_result([User]) ->
{User, undefined};
parse_userinfo_result([]) ->
undefined.
-spec split_query_arg(string()) -> {string(), string()} | undefined.
%% @private %% @private
split_query_arg(Argument) -> split_query_arg(Argument) ->
[K, V] = string:tokens(Argument, "="), case string:tokens(Argument, "=") of
{K, V}. [K, V] -> {plus_decode(K), plus_decode(V)};
[Value] -> plus_decode(Value)
end.
-spec url_add_scheme(atom()) -> string().
%% @private %% @private
url_add_scheme(undefined) ->
"http://";
url_add_scheme(Scheme) -> url_add_scheme(Scheme) ->
string:concat(atom_to_list(Scheme), "://"). string:concat(atom_to_list(Scheme), "://").
-spec url_maybe_add_userinfo(username(), password(), string()) -> string().
%% @private %% @private
url_maybe_add_user([], URL) -> URL; url_maybe_add_userinfo([], [], URL) ->
url_maybe_add_user(User, URL) -> URL;
string:concat(URL, string:concat(User, "@")).
url_maybe_add_userinfo(undefined, undefined, URL) ->
URL;
url_maybe_add_userinfo(Username, [], URL) ->
url_maybe_add_userinfo(Username, undefined, URL);
url_maybe_add_userinfo(Username, undefined, URL) ->
string:concat(URL, string:concat(Username, "@"));
url_maybe_add_userinfo(Username, Password, URL) ->
string:concat(URL, string:concat(string:join([Username, Password], ":"), "@")).
-spec url_add_host_and_port(scheme(), host(), tcp_port(), string()) -> string().
%% @private %% @private
url_add_host_and_port(undefined, Host, undefined, URL) ->
string:concat(URL, Host);
url_add_host_and_port(http, Host, undefined, URL) ->
string:concat(URL, Host);
url_add_host_and_port(http, Host, 80, URL) -> url_add_host_and_port(http, Host, 80, URL) ->
string:concat(URL, Host); string:concat(URL, Host);
url_add_host_and_port(https, Host, undefined, URL) ->
string:concat(URL, Host);
%% @private
url_add_host_and_port(https, Host, 443, URL) -> url_add_host_and_port(https, Host, 443, URL) ->
string:concat(URL, Host); string:concat(URL, Host);
url_add_host_and_port(_, Host, Port, URL) -> url_add_host_and_port(_, Host, Port, URL) ->
string:concat(URL, string:join([Host, integer_to_list(Port)], ":")). string:concat(URL, string:join([Host, integer_to_list(Port)], ":")).
-spec url_add_path(path(), string()) -> string().
%% @private %% @private
url_add_path(undefined, URL) ->
string:concat(URL, "/");
url_add_path(Path, URL) -> url_add_path(Path, URL) ->
Escaped = string:join([edoc_lib:escape_uri(P) || P <- string:tokens(Path, "/")], "/"), Escaped = string:join([url_escape_path_segment(P) || P <- string:tokens(Path, "/")], "/"),
string:join([URL, Escaped], "/"). string:join([URL, Escaped], "/").
-spec url_escape_path_segment(string()) -> string().
%% @private %% @private
url_maybe_add_qargs([], URL) -> URL; url_escape_path_segment(Value) ->
edoc_lib:escape_uri(http_uri:decode(Value)).
-spec url_maybe_add_qargs(query(), string()) -> string().
%% @private
url_maybe_add_qargs(undefined, URL) ->
URL;
url_maybe_add_qargs([], URL) ->
URL;
url_maybe_add_qargs(QArgs, URL) -> url_maybe_add_qargs(QArgs, URL) ->
QStr = string:join([string:join([encode_plus(K), encode_plus(V)], "=") || {K,V} <- QArgs], "&"), QStr = string:join([url_maybe_encode_query_arg(Arg) || Arg <- QArgs], "&"),
string:join([URL, QStr], "?"). string:join([URL, QStr], "?").
-spec url_maybe_encode_query_arg(tuple() | string()) -> string().
%% @private %% @private
url_maybe_encode_query_arg({K, V}) ->
string:join([plus_encode(K), plus_encode(V)], "=");
url_maybe_encode_query_arg(V) ->
plus_encode(V).
-spec url_maybe_add_fragment(fragment(), string()) -> string().
%% @private
url_maybe_add_fragment(undefined, URL) -> URL;
url_maybe_add_fragment([], URL) -> URL; url_maybe_add_fragment([], URL) -> URL;
url_maybe_add_fragment(Value, URL) -> url_maybe_add_fragment(Value, URL) ->
Fragment = case string:left(Value, 1) of Fragment = case string:left(Value, 1) of

View file

@ -4,94 +4,152 @@
-include("urilib.hrl"). -include("urilib.hrl").
decode_test() -> build_variation1_test() ->
Value = "foo%2fbar%20baz", Params = {amqp, {{"guest", "password"}, "rabbitmq", 5672}, "/%2f", [{"heartbeat", "5"}], undefined},
Expect = "foo/bar baz", Expect = "amqp://guest:password@rabbitmq:5672/%2f?heartbeat=5",
?assertEqual(Expect, urilib:decode(Value)). ?assertEqual(Expect, urilib:build(Params)).
decode_plus_test() -> build_variation2_test() ->
Value = "foo/bar+baz", Params = {http, {undefined, "www.google.com", 80}, "/search", [{"foo", "bar"}], "#baz"},
Expect = "foo/bar baz", Expect = "http://www.google.com/search?foo=bar#baz",
?assertEqual(Expect, urilib:decode_plus(Value)). ?assertEqual(Expect, urilib:build(Params)).
encode1_test() -> build_variation3_test() ->
Value = "foo/bar baz", Params = {https, {undefined, "www.google.com", 443}, "/search", undefined, undefined},
Expect = "foo%2fbar%20baz", Expect = "https://www.google.com/search",
?assertEqual(Expect, urilib:encode(Value)). ?assertEqual(Expect, urilib:build(Params)).
encode1_unicode_test() -> build_variation4_test() ->
Value = "foo/bar✈baz", Params = {https, {undefined, "www.google.com", 443}, "/search", ["foo"], undefined},
Expect = "foo%2fbar%c0%88baz", Expect = "https://www.google.com/search?foo",
?assertEqual(Expect, urilib:encode(Value)). ?assertEqual(Expect, urilib:build(Params)).
encode_plus1_test() -> build_variation5_test() ->
Value = "foo/bar baz", Params = {https, {undefined, "www.google.com", undefined}, "/search", ["foo"], undefined},
Expect = "foo%2fbar+baz", Expect = "https://www.google.com/search?foo",
?assertEqual(Expect, urilib:encode_plus(Value)). ?assertEqual(Expect, urilib:build(Params)).
parse_uri_variation1_test() -> build_variation6_test() ->
URI = "amqp://guest:rabbitmq@rabbitmq:5672/%2f?heartbeat=5", Params = {http, {undefined, "www.google.com", undefined}, "/search", ["foo"], undefined},
Expect = #uri{scheme=amqp, Expect = "http://www.google.com/search?foo",
userinfo=#userinfo{username="guest", ?assertEqual(Expect, urilib:build(Params)).
password="rabbitmq"},
authority=#authority{host="rabbitmq", port=5672},
path="/%2f",
query=[{"heartbeat", "5"}],
fragment=[]},
?assertEqual(Expect, urilib:parse_uri(URI)).
parse_uri_variation2_test() -> build_variation7_test() ->
Params = {undefined, {undefined, "www.google.com", undefined}, "/search", ["foo"], undefined},
Expect = "http://www.google.com/search?foo",
?assertEqual(Expect, urilib:build(Params)).
build_variation8_test() ->
Params = {undefined, {undefined, "www.google.com", undefined}, undefined, ["foo"], undefined},
Expect = "http://www.google.com/?foo",
?assertEqual(Expect, urilib:build(Params)).
build_variation9_test() ->
Params = {undefined, {undefined, "www.google.com", undefined}, undefined, [], ""},
Expect = "http://www.google.com/",
?assertEqual(Expect, urilib:build(Params)).
build_variation10_test() ->
Params = {undefined, {undefined, "www.google.com", undefined}, undefined, [], "foo"},
Expect = "http://www.google.com/#foo",
?assertEqual(Expect, urilib:build(Params)).
build_url_variation1_test() ->
Params = {amqp, "guest", "password", "rabbitmq", 5672, "/%2f", [{"heartbeat", "5"}], undefined},
Expect = "amqp://guest:password@rabbitmq:5672/%2f?heartbeat=5",
?assertEqual(Expect, urilib:build(Params)).
build_url_variation2_test() ->
Params = {http, undefined, "www.google.com", 80, "/search", [{"foo", "bar"}], "#baz"},
Expect = "http://www.google.com/search?foo=bar#baz",
?assertEqual(Expect, urilib:build(Params)).
build_url_variation3_test() ->
Params = {https, undefined, "www.google.com", 443, "/search", undefined, undefined},
Expect = "https://www.google.com/search",
?assertEqual(Expect, urilib:build(Params)).
build_url_variation4_test() ->
Params = {https, undefined, "www.google.com", 443, "/search", ["foo"], undefined},
Expect = "https://www.google.com/search?foo",
?assertEqual(Expect, urilib:build(Params)).
build_url_variation5_test() ->
Params = {https, "", "", "www.google.com", 443, "/search", ["foo"], undefined},
Expect = "https://www.google.com/search?foo",
?assertEqual(Expect, urilib:build(Params)).
build_url_variation6_test() ->
Params = {https, "bar", "", "www.google.com", 443, "/search", ["foo"], undefined},
Expect = "https://bar@www.google.com/search?foo",
?assertEqual(Expect, urilib:build(Params)).
build_url_variation7_test() ->
Params = {https, "bar", undefined, "www.google.com", 443, "/search", ["foo"], undefined},
Expect = "https://bar@www.google.com/search?foo",
?assertEqual(Expect, urilib:build(Params)).
parse_variation1_test() ->
URI = "amqp://guest:password@rabbitmq:5672/%2f?heartbeat=5",
Expect = {amqp, {{"guest", "password"}, "rabbitmq", 5672}, "/%2f", [{"heartbeat", "5"}], undefined},
?assertEqual(Expect, urilib:parse(URI)).
parse_variation2_test() ->
URI = "http://www.google.com/search?foo=bar#baz", URI = "http://www.google.com/search?foo=bar#baz",
Expect = #uri{scheme=http, Expect = {http, {undefined, "www.google.com", 80}, "/search", [{"foo", "bar"}], "#baz"},
userinfo=#userinfo{}, ?assertEqual(Expect, urilib:parse(URI)).
authority=#authority{host="www.google.com", port=80},
path="/search",
query=[{"foo", "bar"}],
fragment="#baz"},
?assertEqual(Expect, urilib:parse_uri(URI)).
parse_uri_variation3_test() -> parse_variation3_test() ->
URI = "https://www.google.com/search", URI = "https://www.google.com/search",
Expect = #uri{scheme=https, Expect = {https, {undefined, "www.google.com", 443}, "/search", undefined, undefined},
userinfo=#userinfo{}, ?assertEqual(Expect, urilib:parse(URI)).
authority=#authority{host="www.google.com", port=443},
path="/search", parse_variation4_test() ->
query=[], URI = "https://www.google.com/search?foo",
fragment=[]}, Expect = {https, {undefined, "www.google.com", 443}, "/search", ["foo"], undefined},
?assertEqual(Expect, urilib:parse_uri(URI)). ?assertEqual(Expect, urilib:parse(URI)).
parse_uri_test() ->
URI = "amqp://guest:password@rabbitmq:5672/%2f?heartbeat=5",
Expect = {amqp, {{"guest", "password"}, "rabbitmq", 5672}, "/%2f",
[{"heartbeat", "5"}], undefined},
?assertEqual(Expect, urilib:parse(URI, uri)).
parse_url_variation1_test() -> parse_url_variation1_test() ->
URL = "amqp://guest:rabbitmq@rabbitmq:5672/%2f?heartbeat=5", URI = "amqp://guest:password@rabbitmq:5672/%2f?heartbeat=5&foo=bar&baz+corgie=qux+grault",
Expect = #url{scheme=amqp, Expect = {amqp, "guest", "password", "rabbitmq", 5672, "/%2f",
username="guest", [{"heartbeat", "5"}, {"foo", "bar"}, {"baz corgie", "qux grault"}],
password="rabbitmq", undefined},
host="rabbitmq", ?assertEqual(Expect, urilib:parse(URI, url)).
port=5672,
path="/%2f",
query=[{"heartbeat", "5"}],
fragment=[]},
?assertEqual(Expect, urilib:parse_url(URL)).
parse_url_variation2_test() -> parse_url_variation2_test() ->
URL = "http://www.google.com/search?foo=bar#baz", URI = "amqp://guest@rabbitmq:5672/%2f?heartbeat=5&foo=bar&baz+corgie=qux+grault#foo",
Expect = #url{scheme=http, Expect = {amqp, "guest", undefined, "rabbitmq", 5672, "/%2f",
username=undefined, [{"heartbeat", "5"}, {"foo", "bar"}, {"baz corgie", "qux grault"}],
password=undefined, "#foo"},
host="www.google.com", ?assertEqual(Expect, urilib:parse(URI, url)).
port=80,
path="/search",
query=[{"foo", "bar"}],
fragment="#baz"},
?assertEqual(Expect, urilib:parse_url(URL)).
parse_url_variation3_test() -> percent_decode_test() ->
URL = "https://www.google.com/search", Value = "foo%2fbar%20baz",
Expect = #url{scheme=https, Expect = "foo/bar baz",
username=undefined, ?assertEqual(Expect, urilib:percent_decode(Value)).
password=undefined,
host="www.google.com", plus_decode_test() ->
port=443, Value = "foo/bar+baz",
path="/search", Expect = "foo/bar baz",
query=[], ?assertEqual(Expect, urilib:plus_decode(Value)).
fragment=[]},
?assertEqual(Expect, urilib:parse_url(URL)). percent_encode_test() ->
Value = "foo/bar baz",
Expect = "foo%2fbar%20baz",
?assertEqual(Expect, urilib:percent_encode(Value)).
percent_encode_unicode_test() ->
Value = "foo/bar✈baz",
Expect = "foo%2fbar%c0%88baz",
?assertEqual(Expect, urilib:percent_encode(Value)).
plus_encode_test() ->
Value = "foo/bar baz",
Expect = "foo%2fbar+baz",
?assertEqual(Expect, urilib:plus_encode(Value)).