From 56c320572ff8514c066ad16390de9f5b36a53846 Mon Sep 17 00:00:00 2001
From: "Gavin M. Roy"
Date: Thu, 21 Jan 2016 23:04:34 -0500
Subject: [PATCH] Implement the initial API
---
.editorconfig | 10 ++
.gitignore | 54 +------
Makefile | 32 ----
README.md | 132 +---------------
doc/edoc-info | 3 +
doc/erlang.png | Bin 0 -> 2109 bytes
doc/index.html | 17 ++
doc/modules-frame.html | 12 ++
doc/overview-summary.html | 16 ++
doc/stylesheet.css | 55 +++++++
doc/urilib.html | 135 ++++++++++++++++
include/urilib.hrl | 1 +
rebar.config | 2 +
src/urilib.erl | 325 +++++++++++++++++++++++---------------
test/urilib_tests.erl | 212 ++++++++++++++++---------
15 files changed, 591 insertions(+), 415 deletions(-)
create mode 100644 .editorconfig
delete mode 100644 Makefile
create mode 100644 doc/edoc-info
create mode 100644 doc/erlang.png
create mode 100644 doc/index.html
create mode 100644 doc/modules-frame.html
create mode 100644 doc/overview-summary.html
create mode 100644 doc/stylesheet.css
create mode 100644 doc/urilib.html
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..af54ab1
--- /dev/null
+++ b/.editorconfig
@@ -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
diff --git a/.gitignore b/.gitignore
index 2a0834c..2967aab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,62 +1,12 @@
-# Created by .ignore support plugin (hsz.mobi)
-### Erlang template
.eunit
deps
*.o
-*.beam
*.plt
erl_crash.dump
-ebin
-rel/example_project
-.concrete/DEV_MODE
.rebar
TEST*.xml
_build
-
-### 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:
+.idea
*.ipr
*.iws
-
-## 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
+rebar.lock
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 4db9b37..0000000
--- a/Makefile
+++ /dev/null
@@ -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
diff --git a/README.md b/README.md
index f1bd240..7ac1c82 100644
--- a/README.md
+++ b/README.md
@@ -9,137 +9,9 @@ Example Usage
```erlang
-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]).
-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]).
```
-
-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
----
-
-
-
-### build/1 ###
-```erlang
-build(Uri::Value) -> URI
-```
-Value = #uri{} | #url{}
URI = string()
-
-Returns a URI from the record passed in.
-
-
-
-### decode/1 ###
-
-```erlang
-decode(Value) -> DecodedValue
-```
-
-Value = string()
DecodeValue = string()
-
-Decode a percent encoded string value.
-
-
-
-### decode_plus/1 ###
-
-```erlang
-decode_plus(Value) -> DecodedValue
-```
-
-Value = string()
DecodeValue = string()
-
-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.
-
-
-
-### encode/1 ###
-
-```erlang
-encode(Value) -> EncodedValue
-```
-
-Value = string()
EncodedValue = string()
-
-Percent encode a string value.
-
-
-
-### encode_plus/1 ###
-
-```erlang
-encode_plus(Value) -> EncodedValue
-```
-
-Value = string()
EncodedValue = string()
-
-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.
-
-
-
-### parse_uri/1 ###
-
-```erlang
-parse_uri(URI) -> ParsedURI
-```
-
-URI = string()
ParsedURI = #uri{}
-
-Parse a URI string returning the parsed data as a record
-
-
-
-### parse_url/1 ###
-
-```erlang
-parse_url(URL) -> ParsedURL
-```
-
-URI = string()
ParsedURL = #url{}
-
-Parse a URL string returning the parsed data as a record
-
diff --git a/doc/edoc-info b/doc/edoc-info
new file mode 100644
index 0000000..0f929d6
--- /dev/null
+++ b/doc/edoc-info
@@ -0,0 +1,3 @@
+%% encoding: UTF-8
+{application,urilib}.
+{modules,[urilib]}.
diff --git a/doc/erlang.png b/doc/erlang.png
new file mode 100644
index 0000000000000000000000000000000000000000..987a618e2403af895bfaf8c2f929e3a4f3746659
GIT binary patch
literal 2109
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
literal 0
HcmV?d00001
diff --git a/doc/index.html b/doc/index.html
new file mode 100644
index 0000000..98834bc
--- /dev/null
+++ b/doc/index.html
@@ -0,0 +1,17 @@
+
+
+
+urllib
+
+
+
+
+
+
+This page uses frames
+Your browser does not accept frames.
+ You should go to the non-frame version instead.
+
+
+
+
\ No newline at end of file
diff --git a/doc/modules-frame.html b/doc/modules-frame.html
new file mode 100644
index 0000000..7b729c7
--- /dev/null
+++ b/doc/modules-frame.html
@@ -0,0 +1,12 @@
+
+
+
+urllib
+
+
+
+Modules
+
+
+
\ No newline at end of file
diff --git a/doc/overview-summary.html b/doc/overview-summary.html
new file mode 100644
index 0000000..abc2d0d
--- /dev/null
+++ b/doc/overview-summary.html
@@ -0,0 +1,16 @@
+
+
+
+
+urllib
+
+
+
+
+urllib
+
+
+
+Generated by EDoc, Jan 21 2016, 22:59:50.
+
+
diff --git a/doc/stylesheet.css b/doc/stylesheet.css
new file mode 100644
index 0000000..ab170c0
--- /dev/null
+++ b/doc/stylesheet.css
@@ -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
+}
diff --git a/doc/urilib.html b/doc/urilib.html
new file mode 100644
index 0000000..f047562
--- /dev/null
+++ b/doc/urilib.html
@@ -0,0 +1,135 @@
+
+
+
+
+Module urilib
+
+
+
+
+
+
+Module urilib
+urilib is a RFC-3986 URI Library for Erlang.
+Copyright © 2016
+
+Authors: Gavin M. Roy (gavinmroy@gmail.com ).
+
+urilib is a RFC-3986 URI Library for Erlang
+
+
+
+
authority() = {userinfo() , host() , tcp_port() }
+
+
+
+
fragment() = string() | undefined
+
+
+
+
host() = string()
+
+
+
+
password() = string() | undefined
+
+
+
+
path() = string()
+
+
+
+
query() = [tuple() | string()] | undefined
+
+
+
+
scheme() = http | https | atom()
+
+
+
+
tcp_port() = integer()
+
+
+
+
uri() = {scheme() , authority() , path() , query() , fragment() }
+
+
+
+
url() =
+ {scheme() ,
+ username() ,
+ password() ,
+ host() ,
+ tcp_port() ,
+ path() ,
+ query() ,
+ fragment() }
+
+
+
+
userinfo() = {username() , password() } | undefined
+
+
+
+
username() = string() | undefined
+
+
+
+
+
+
+
+
+Build a URI
+
+
+
+
parse(Value :: string()) -> uri()
+
Parse a URI
+
+
+
+
parse(Value :: string(), Return :: uri | url) -> uri()
+
Parse a URI, returning the result as either a uri()
or url()
.
+
+
+
+
percent_decode(Value :: string()) -> string()
+
Decode a percent encoded string value.
+
+
+
+
percent_encode(Value :: string()) -> string()
+
Percent encode a string value.
+
+
+
+
plus_decode(Value :: string()) -> string()
+
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.
+
+
+
+
plus_encode(Value :: string()) -> string()
+
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.
+
+
+
+Generated by EDoc, Jan 21 2016, 22:59:50.
+
+
diff --git a/include/urilib.hrl b/include/urilib.hrl
index 9100c5d..6bf56d2 100644
--- a/include/urilib.hrl
+++ b/include/urilib.hrl
@@ -1,6 +1,7 @@
%% =============================================================================
%% @author Gavin M. Roy
%% @copyright 2016
+%% @doc urilib is a RFC-3986 URI Library for Erlang
%% @end
%% =============================================================================
diff --git a/rebar.config b/rebar.config
index 180de7e..b38f5ff 100644
--- a/rebar.config
+++ b/rebar.config
@@ -4,6 +4,8 @@
{clean_files, ["*.eunit", "ebin/*.beam"]}.
{dialyzer, [{plt_extra_apps, [kernel, stdlib, erts, inets, edoc]}]}.
{erl_opts, [fail_on_warning]}.
+{edoc_opts, [{includes, "include"}, {title, "urllib"}, {pretty_printer, erl_pp}]}.
{eunit_exclude_deps, true}.
{eunit_opts, [verbose, {skip_deps, true}]}.
+{lib_dirs,["deps"]}.
{minimum_otp_vsn, "17.5"}.
diff --git a/src/urilib.erl b/src/urilib.erl
index 6cba678..c07a34d 100644
--- a/src/urilib.erl
+++ b/src/urilib.erl
@@ -7,12 +7,25 @@
-module(urilib).
-export([build/1,
- parse_uri/1,
- parse_url/1,
- encode/1,
- encode_plus/1,
- decode/1,
- decode_plus/1]).
+ parse/1,
+ parse/2,
+ percent_decode/1,
+ percent_encode/1,
+ plus_decode/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").
@@ -21,189 +34,253 @@
-compile(export_all).
-endif.
--spec build(#uri{} | #url{}) -> string().
-%% @spec build(Value) -> URI
-%% where
-%% Value = #uri{} | #url{}
-%% URI = string()
-%% @doc Returns a URI from the record passed in.
-%%
-%% @end
-build(#uri{scheme=Scheme, userinfo=UserInfo, authority=Authority,
- path=Path, query=QArgs, fragment=Fragment}) ->
- U1 = url_add_scheme(Scheme),
- U2 = url_maybe_add_user(UserInfo, U1),
- U3 = url_add_host_and_port(Scheme,
- Authority#authority.host,
- Authority#authority.port, U2),
- U4 = url_add_path(Path, U3),
- U5 = url_maybe_add_qargs(QArgs, U4),
- url_maybe_add_fragment(Fragment, U5).
+-type scheme() :: http | https | atom().
+-type host() :: string().
+-type tcp_port() :: integer().
+-type username() :: string() | undefined.
+-type password() :: string() | undefined.
+-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 parse_uri(string()) -> #uri{}.
-%% @spec parse_uri(URI) -> ParsedURI
-%% where
-%% URI = string()
-%% ParsedURI = #uri{}
-%% @doc Parse a URI string returning the parsed data as a record
+-spec build(Value :: uri() | url()) -> string().
+%% @doc Build a URI
%% @end
-parse_uri(URI) ->
- case http_uri:parse(URI, [{scheme_defaults, http_uri:scheme_defaults()}, {fragment, true}]) of
+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);
+
+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).
+
+
+-spec parse(string()) -> uri().
+%% @doc Parse a URI
+%% @end
+parse(Value) ->
+ case http_uri:parse(Value, [{scheme_defaults, http_uri:scheme_defaults()}, {fragment, true}]) of
{ok, {Scheme, UserInfo, Host, Port, Path, Query, Fragment}} ->
- #uri{scheme=Scheme,
- userinfo=parse_userinfo(UserInfo),
- authority=#authority{host=Host,
- port=Port},
- path=Path,
- query=parse_query(Query),
- fragment=Fragment};
- {error, Reason} -> {error, Reason}
+ {Scheme, {parse_userinfo(UserInfo), Host, Port}, Path, parse_query(Query), parse_fragment(Fragment)};
+ {error, Reason} -> {error, Reason}
end.
--spec parse_url(string()) -> #url{}.
-%% @spec parse_url(URL) -> ParsedURL
-%% where
-%% URI = string()
-%% ParsedURL = #url{}
-%% @doc Parse a URL string returning the parsed data as a record
+-spec parse(string(), Return :: uri | url) -> uri().
+%% @doc Parse a URI, returning the result as either a {@type uri()} or {@type url()}.
%% @end
-parse_url(URL) ->
- case http_uri:parse(URL, [{scheme_defaults, http_uri:scheme_defaults()}, {fragment, true}]) of
+parse(Value, uri) ->
+ 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}} ->
- User = parse_userinfo(UserInfo),
- #url{scheme=Scheme,
- username=User#userinfo.username,
- password=User#userinfo.password,
- host=Host,
- port=Port,
- path=Path,
- query=parse_query(Query),
- fragment=Fragment};
- {error, Reason} -> {error, Reason}
+ {Username, Password} = parse_userinfo(UserInfo),
+ {Scheme, Username, Password, Host, Port, Path, parse_query(Query), parse_fragment(Fragment)};
+ {error, Reason} -> {error, Reason}
end.
--spec encode(string()) -> string().
-%% @spec encode(Value) -> EncodedValue
-%% where
-%% Value = string()
-%% EncodedValue = string()
+-spec percent_encode(string()) -> string().
%% @doc Percent encode a string value.
%% @end
-encode(Value) ->
+percent_encode(Value) ->
edoc_lib:escape_uri(Value).
--spec encode_plus(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()
+-spec percent_decode(string()) -> string().
%% @doc Decode a percent encoded string value.
%% @end
--spec decode(string()) -> string().
-decode(Value) ->
+percent_decode(Value) ->
http_uri:decode(Value).
--spec decode_plus(string()) -> string().
-%% @spec decode_plus(Value) -> DecodedValue
-%% where
-%% Value = string()
-%% DecodeValue = string()
+-spec plus_encode(string()) -> 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
+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.
%%
%% Note: The use of plus for space is defined in RFC-1630 but does not appear
%% in RFC-3986.
%% @end
-decode_plus(Value) ->
+plus_decode(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
-parse_query([]) -> [];
parse_query(Query) ->
- case re:split(Query, "[&|?]", [{return, list}]) of
- [""] -> [];
- QArgs -> [split_query_arg(Arg) || Arg <- QArgs, Arg =/= []]
- end.
+ QArgs = re:split(Query, "[&|?]", [{return, list}]),
+ parse_query_result([split_query_arg(Arg) || Arg <- QArgs, Arg =/= []]).
--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
-parse_userinfo([]) -> #userinfo{};
parse_userinfo(Value) ->
- case string:tokens(Value, ":") of
- [User, Password] -> #userinfo{username=User, password=Password};
- [User] -> #userinfo{username=User}
- end.
+ parse_userinfo_result(string:tokens(Value, ":")).
--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
split_query_arg(Argument) ->
- [K, V] = string:tokens(Argument, "="),
- {K, V}.
+ case string:tokens(Argument, "=") of
+ [K, V] -> {plus_decode(K), plus_decode(V)};
+ [Value] -> plus_decode(Value)
+ end.
+-spec url_add_scheme(atom()) -> string().
%% @private
+url_add_scheme(undefined) ->
+ "http://";
+
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
-url_maybe_add_user([], URL) -> URL;
-url_maybe_add_user(User, URL) ->
- string:concat(URL, string:concat(User, "@")).
+url_maybe_add_userinfo([], [], URL) ->
+ URL;
+
+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
+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) ->
- 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) ->
- string:concat(URL, Host);
+ string:concat(URL, Host);
+
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
+url_add_path(undefined, URL) ->
+ string:concat(URL, "/");
+
url_add_path(Path, URL) ->
- Escaped = string:join([edoc_lib:escape_uri(P) || P <- string:tokens(Path, "/")], "/"),
- string:join([URL, Escaped], "/").
+ Escaped = string:join([url_escape_path_segment(P) || P <- string:tokens(Path, "/")], "/"),
+ string:join([URL, Escaped], "/").
+-spec url_escape_path_segment(string()) -> string().
%% @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) ->
- QStr = string:join([string:join([encode_plus(K), encode_plus(V)], "=") || {K,V} <- QArgs], "&"),
- string:join([URL, QStr], "?").
+ QStr = string:join([url_maybe_encode_query_arg(Arg) || Arg <- QArgs], "&"),
+ string:join([URL, QStr], "?").
+-spec url_maybe_encode_query_arg(tuple() | string()) -> string().
%% @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(Value, URL) ->
- Fragment = case string:left(Value, 1) of
- "#" -> edoc_lib:escape_uri(string:sub_string(Value, 2));
- _ -> edoc_lib:escape_uri(Value)
- end,
- string:join([URL, Fragment], "#").
+ Fragment = case string:left(Value, 1) of
+ "#" -> edoc_lib:escape_uri(string:sub_string(Value, 2));
+ _ -> edoc_lib:escape_uri(Value)
+ end,
+ string:join([URL, Fragment], "#").
diff --git a/test/urilib_tests.erl b/test/urilib_tests.erl
index ba2b539..9b7134f 100644
--- a/test/urilib_tests.erl
+++ b/test/urilib_tests.erl
@@ -4,94 +4,152 @@
-include("urilib.hrl").
-decode_test() ->
- Value = "foo%2fbar%20baz",
- Expect = "foo/bar baz",
- ?assertEqual(Expect, urilib:decode(Value)).
+build_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)).
-decode_plus_test() ->
- Value = "foo/bar+baz",
- Expect = "foo/bar baz",
- ?assertEqual(Expect, urilib:decode_plus(Value)).
+build_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)).
-encode1_test() ->
- Value = "foo/bar baz",
- Expect = "foo%2fbar%20baz",
- ?assertEqual(Expect, urilib:encode(Value)).
+build_variation3_test() ->
+ Params = {https, {undefined, "www.google.com", 443}, "/search", undefined, undefined},
+ Expect = "https://www.google.com/search",
+ ?assertEqual(Expect, urilib:build(Params)).
-encode1_unicode_test() ->
- Value = "foo/bar✈baz",
- Expect = "foo%2fbar%c0%88baz",
- ?assertEqual(Expect, urilib:encode(Value)).
+build_variation4_test() ->
+ Params = {https, {undefined, "www.google.com", 443}, "/search", ["foo"], undefined},
+ Expect = "https://www.google.com/search?foo",
+ ?assertEqual(Expect, urilib:build(Params)).
-encode_plus1_test() ->
- Value = "foo/bar baz",
- Expect = "foo%2fbar+baz",
- ?assertEqual(Expect, urilib:encode_plus(Value)).
+build_variation5_test() ->
+ Params = {https, {undefined, "www.google.com", undefined}, "/search", ["foo"], undefined},
+ Expect = "https://www.google.com/search?foo",
+ ?assertEqual(Expect, urilib:build(Params)).
-parse_uri_variation1_test() ->
- URI = "amqp://guest:rabbitmq@rabbitmq:5672/%2f?heartbeat=5",
- Expect = #uri{scheme=amqp,
- userinfo=#userinfo{username="guest",
- password="rabbitmq"},
- authority=#authority{host="rabbitmq", port=5672},
- path="/%2f",
- query=[{"heartbeat", "5"}],
- fragment=[]},
- ?assertEqual(Expect, urilib:parse_uri(URI)).
+build_variation6_test() ->
+ Params = {http, {undefined, "www.google.com", undefined}, "/search", ["foo"], undefined},
+ Expect = "http://www.google.com/search?foo",
+ ?assertEqual(Expect, urilib:build(Params)).
-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",
- Expect = #uri{scheme=http,
- userinfo=#userinfo{},
- authority=#authority{host="www.google.com", port=80},
- path="/search",
- query=[{"foo", "bar"}],
- fragment="#baz"},
- ?assertEqual(Expect, urilib:parse_uri(URI)).
+ Expect = {http, {undefined, "www.google.com", 80}, "/search", [{"foo", "bar"}], "#baz"},
+ ?assertEqual(Expect, urilib:parse(URI)).
-parse_uri_variation3_test() ->
+parse_variation3_test() ->
URI = "https://www.google.com/search",
- Expect = #uri{scheme=https,
- userinfo=#userinfo{},
- authority=#authority{host="www.google.com", port=443},
- path="/search",
- query=[],
- fragment=[]},
- ?assertEqual(Expect, urilib:parse_uri(URI)).
+ Expect = {https, {undefined, "www.google.com", 443}, "/search", undefined, undefined},
+ ?assertEqual(Expect, urilib:parse(URI)).
+
+parse_variation4_test() ->
+ URI = "https://www.google.com/search?foo",
+ Expect = {https, {undefined, "www.google.com", 443}, "/search", ["foo"], undefined},
+ ?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() ->
- URL = "amqp://guest:rabbitmq@rabbitmq:5672/%2f?heartbeat=5",
- Expect = #url{scheme=amqp,
- username="guest",
- password="rabbitmq",
- host="rabbitmq",
- port=5672,
- path="/%2f",
- query=[{"heartbeat", "5"}],
- fragment=[]},
- ?assertEqual(Expect, urilib:parse_url(URL)).
+ URI = "amqp://guest:password@rabbitmq:5672/%2f?heartbeat=5&foo=bar&baz+corgie=qux+grault",
+ Expect = {amqp, "guest", "password", "rabbitmq", 5672, "/%2f",
+ [{"heartbeat", "5"}, {"foo", "bar"}, {"baz corgie", "qux grault"}],
+ undefined},
+ ?assertEqual(Expect, urilib:parse(URI, url)).
parse_url_variation2_test() ->
- URL = "http://www.google.com/search?foo=bar#baz",
- Expect = #url{scheme=http,
- username=undefined,
- password=undefined,
- host="www.google.com",
- port=80,
- path="/search",
- query=[{"foo", "bar"}],
- fragment="#baz"},
- ?assertEqual(Expect, urilib:parse_url(URL)).
+ URI = "amqp://guest@rabbitmq:5672/%2f?heartbeat=5&foo=bar&baz+corgie=qux+grault#foo",
+ Expect = {amqp, "guest", undefined, "rabbitmq", 5672, "/%2f",
+ [{"heartbeat", "5"}, {"foo", "bar"}, {"baz corgie", "qux grault"}],
+ "#foo"},
+ ?assertEqual(Expect, urilib:parse(URI, url)).
-parse_url_variation3_test() ->
- URL = "https://www.google.com/search",
- Expect = #url{scheme=https,
- username=undefined,
- password=undefined,
- host="www.google.com",
- port=443,
- path="/search",
- query=[],
- fragment=[]},
- ?assertEqual(Expect, urilib:parse_url(URL)).
+percent_decode_test() ->
+ Value = "foo%2fbar%20baz",
+ Expect = "foo/bar baz",
+ ?assertEqual(Expect, urilib:percent_decode(Value)).
+
+plus_decode_test() ->
+ Value = "foo/bar+baz",
+ Expect = "foo/bar baz",
+ ?assertEqual(Expect, urilib:plus_decode(Value)).
+
+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)).