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 -``` - - -Returns a URI from the record passed in. - - - -### decode/1 ### - -```erlang -decode(Value) -> DecodedValue -``` - - - -Decode a percent encoded string value. - - - -### decode_plus/1 ### - -```erlang -decode_plus(Value) -> DecodedValue -``` - - - -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 -``` - - - -Percent encode a string value. - - - -### encode_plus/1 ### - -```erlang -encode_plus(Value) -> EncodedValue -``` - - - -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 -``` - - - -Parse a URI string returning the parsed data as a record - - - -### parse_url/1 ### - -```erlang -parse_url(URL) -> ParsedURL -``` - - - -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 + + + + + + +<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> + + + \ 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

+ +
urilib
+ + \ 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).

+ +

Description

urilib is a RFC-3986 URI Library for Erlang +

Data Types

+ +

authority()

+

authority() = {userinfo(), host(), tcp_port()}

+ + +

fragment()

+

fragment() = string() | undefined

+ + +

host()

+

host() = string()

+ + +

password()

+

password() = string() | undefined

+ + +

path()

+

path() = string()

+ + +

query()

+

query() = [tuple() | string()] | undefined

+ + +

scheme()

+

scheme() = http | https | atom()

+ + +

tcp_port()

+

tcp_port() = integer()

+ + +

uri()

+

uri() = {scheme(), authority(), path(), query(), fragment()}

+ + +

url()

+

url() = 
+    {scheme(),
+     username(),
+     password(),
+     host(),
+     tcp_port(),
+     path(),
+     query(),
+     fragment()}

+ + +

userinfo()

+

userinfo() = {username(), password()} | undefined

+ + +

username()

+

username() = string() | undefined

+ + +

Function Index

+ + + + + + + +
build/1Build a URI.
parse/1Parse a URI.
parse/2Parse a URI, returning the result as either a uri() or url().
percent_decode/1Decode a percent encoded string value.
percent_encode/1Percent encode a string value.
plus_decode/1Decode a percent encoded string value that uses pluses for spaces.
plus_encode/1Percent encode a string value similar to encode/1, but encodes spaces with a + plus (+) instead of %20.
+ +

Function Details

+ +

build/1

+
+

build(Value :: uri() | url()) -> string()

+

Build a URI

+ +

parse/1

+
+

parse(Value :: string()) -> uri()

+

Parse a URI

+ +

parse/2

+
+

parse(Value :: string(), Return :: uri | url) -> uri()

+

Parse a URI, returning the result as either a uri() or url().

+ +

percent_decode/1

+
+

percent_decode(Value :: string()) -> string()

+

Decode a percent encoded string value.

+ +

percent_encode/1

+
+

percent_encode(Value :: string()) -> string()

+

Percent encode a string value.

+ +

plus_decode/1

+
+

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/1

+
+

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)).