aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--LICENSE2
-rw-r--r--Makefile42
-rw-r--r--README.asciidoc29
-rw-r--r--README.md30
-rw-r--r--doc/src/guide/book.asciidoc16
-rw-r--r--doc/src/guide/connect.asciidoc119
-rw-r--r--doc/src/guide/http.asciidoc367
-rw-r--r--doc/src/guide/introduction.asciidoc (renamed from guide/introduction.md)25
-rw-r--r--doc/src/guide/protocols.asciidoc119
-rw-r--r--doc/src/guide/start.asciidoc67
-rw-r--r--doc/src/guide/websocket.asciidoc100
-rw-r--r--doc/src/manual/gun.asciidoc578
-rw-r--r--doc/src/manual/gun_app.asciidoc24
-rw-r--r--erlang.mk1329
-rw-r--r--guide/connect.md96
-rw-r--r--guide/http.md227
-rw-r--r--guide/protocols.md79
-rw-r--r--guide/toc.md11
-rw-r--r--guide/websocket.md85
-rw-r--r--manual/gun.md340
-rw-r--r--manual/gun_app.md22
-rw-r--r--manual/toc.md7
-rw-r--r--rebar.config2
-rw-r--r--src/gun.app.src2
-rw-r--r--src/gun.erl91
-rw-r--r--src/gun_app.erl2
-rw-r--r--src/gun_http.erl207
-rw-r--r--src/gun_spdy.erl38
-rw-r--r--src/gun_sup.erl2
-rw-r--r--src/gun_ws.erl125
-rw-r--r--test/gun_ct_hook.erl21
-rw-r--r--test/twitter_SUITE.erl36
-rw-r--r--test/ws_SUITE.erl176
-rw-r--r--test/ws_SUITE_data/server.json7
34 files changed, 3204 insertions, 1219 deletions
diff --git a/LICENSE b/LICENSE
index e43ab78..5ea629c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2013-2014, Loïc Hoguin <[email protected]>
+Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
diff --git a/Makefile b/Makefile
index 9d8a30f..8a806cc 100644
--- a/Makefile
+++ b/Makefile
@@ -4,15 +4,51 @@ PROJECT = gun
# Options.
-CT_SUITES = twitter
+CT_OPTS += -pa test -ct_hooks gun_ct_hook [] -boot start_sasl
PLT_APPS = ssl
# Dependencies.
DEPS = cowlib ranch
-dep_cowlib = pkg://cowlib master
-dep_ranch = pkg://ranch master
+dep_cowlib = git https://github.com/ninenines/cowlib 1.3.0
+
+TEST_DEPS = ct_helper
+dep_ct_helper = git https://github.com/extend/ct_helper.git master
# Standard targets.
include erlang.mk
+
+# AsciiDoc.
+
+.PHONY: asciidoc asciidoc-guide asciidoc-manual clean-asciidoc
+
+MAN_INSTALL_PATH ?= /usr/local/share/man
+MAN_SECTIONS ?= 3 7
+
+asciidoc: clean-asciidoc asciidoc-guide asciidoc-manual
+
+asciidoc-guide:
+ a2x -v -f pdf doc/src/guide/book.asciidoc && mv doc/src/guide/book.pdf doc/guide.pdf
+ a2x -v -f chunked doc/src/guide/book.asciidoc && mv doc/src/guide/book.chunked/ doc/html/
+
+asciidoc-manual:
+ for f in doc/src/manual/*.asciidoc ; do \
+ a2x -v -f manpage $$f ; \
+ done
+ for s in $(MAN_SECTIONS); do \
+ mkdir -p doc/man$$s/ ; \
+ mv doc/src/manual/*.$$s doc/man$$s/ ; \
+ gzip doc/man$$s/*.$$s ; \
+ done
+
+clean:: clean-asciidoc
+
+clean-asciidoc:
+ $(gen_verbose) rm -rf doc/html/ doc/guide.pdf doc/man3/ doc/man7/
+
+install-docs:
+ for s in $(MAN_SECTIONS); do \
+ mkdir -p $(MAN_INSTALL_PATH)/man$$s/ ; \
+ install -g 0 -o 0 -m 0644 doc/man$$s/*.gz $(MAN_INSTALL_PATH)/man$$s/ ; \
+ done
diff --git a/README.asciidoc b/README.asciidoc
new file mode 100644
index 0000000..7715978
--- /dev/null
+++ b/README.asciidoc
@@ -0,0 +1,29 @@
+= Gun
+
+Gun is an Erlang HTTP client with support for HTTP/1.1, SPDY and Websocket.
+
+== Goals
+
+Gun aims to provide an *easy to use* client compatible with
+HTTP, SPDY and Websocket.
+
+Gun is *always connected*. It will maintain a permanent
+connection to the server, reopening it as soon as the server
+closes it, saving time for the requests that come in.
+
+All connections are *supervised* automatically, allowing
+developers to focus on writing their code without worrying.
+
+== Sponsors
+
+The project is currently sponsored by
+https://kato.im[Kato.im] and https://sameroom.io[Sameroom].
+
+The SPDY implementation was sponsored by
+http://www.leofs.org[LeoFS Cloud Storage].
+
+== Support
+
+* Official IRC Channel: #ninenines on irc.freenode.net
+* http://lists.ninenines.eu[Mailing Lists]
+* http://ninenines.eu/support[Commercial Support]
diff --git a/README.md b/README.md
deleted file mode 100644
index 1acabfb..0000000
--- a/README.md
+++ /dev/null
@@ -1,30 +0,0 @@
-Gun
-===
-
-Gun is an asynchronous SPDY, HTTP and Websocket client.
-
-Goals
------
-
-Gun aims to provide an **easy to use** client compatible with
-HTTP, SPDY and Websocket.
-
-Gun is **always connected**. It will maintain a permanent
-connection to the server, reopening it as soon as the server
-closes it, saving time for the requests that come in.
-
-All connections are **supervised** automatically, allowing
-the developer to focus on writing his code without worrying.
-
-Sponsors
---------
-
-The development of the Gun project is sponsored
-by [LeoFS Cloud Storage](http://www.leofs.org).
-
-Support
--------
-
- * Official IRC Channel: #ninenines on irc.freenode.net
- * [Mailing Lists](http://lists.ninenines.eu)
- * [Commercial Support](http://ninenines.eu/support)
diff --git a/doc/src/guide/book.asciidoc b/doc/src/guide/book.asciidoc
new file mode 100644
index 0000000..7e6a441
--- /dev/null
+++ b/doc/src/guide/book.asciidoc
@@ -0,0 +1,16 @@
+// a2x: --dblatex-opts "-P latex.output.revhistory=0 -P doc.publisher.show=0 -P index.numbered=0"
+// a2x: -d book --attribute tabsize=4
+
+= Gun User Guide
+
+include::introduction.asciidoc[Introduction]
+
+include::start.asciidoc[Starting and stopping]
+
+include::protocols.asciidoc[Supported protocols]
+
+include::connect.asciidoc[Connection]
+
+include::http.asciidoc[Using HTTP]
+
+include::websocket.asciidoc[Using Websocket]
diff --git a/doc/src/guide/connect.asciidoc b/doc/src/guide/connect.asciidoc
new file mode 100644
index 0000000..e1ad56e
--- /dev/null
+++ b/doc/src/guide/connect.asciidoc
@@ -0,0 +1,119 @@
+== Connection
+
+This chapter describes how to open, monitor and close
+a connection using the Gun client.
+
+=== Gun connections
+
+Gun is designed with the SPDY and Websocket protocols in mind.
+They are built for long-running connections that allow concurrent
+exchange of data, either in the form of request/responses for
+SPDY or in the form of messages for Websocket.
+
+A Gun connection is an Erlang process that manages a socket to
+a remote endpoint. This Gun connection is owned by a user
+process that is called the _owner_ of the connection, and is
+managed by the supervision tree of the `gun` application.
+
+The owner process communicates with the Gun connection
+by calling functions from the module `gun`. All functions
+perform their respective operations asynchronously. The Gun
+connection will send Erlang messages to the owner process
+whenever needed.
+
+When the remote endpoint closes the connection, Gun attempts
+to reconnect automatically.
+
+=== Opening a new connection
+
+The `gun:open/{2,3}` function must be used to open a connection.
+
+.Opening a connection to example.org on port 443
+
+[source,erlang]
+{ok, ConnPid} = gun:open("example.org", 443).
+
+@todo open/3
+@todo make opts a map
+
+If the port given is 80, Gun will attempt to connect using
+TCP and use the HTTP/1.1 protocol. For any other port, TLS
+will be used. The NPN extension for TLS allows Gun to select
+SPDY automatically if the server supports it. Otherwise,
+HTTP/1.1 will be used.
+
+@todo more about defaults
+
+=== Monitoring the connection process
+
+@todo Gun should detect the owner process being killed
+
+Because software errors are unavoidable, it is important to
+detect when the Gun process crashes. It is also important
+to detect when it exits normally. Erlang provides two ways
+to do that: links and monitors.
+
+Gun leaves you the choice as to which one will be used.
+However, if you use the `gun:await/{2,3}` or `gun:await_body/{2,3}`
+functions, a monitor may be used for you to avoid getting
+stuck waiting for a message that will never come.
+
+If you choose to monitor yourself you can do it on a permanent
+basis rather than on every message you will receive, saving
+resources. Indeed, the `gun:await/{3,4}` and `gun:await_body/{3,4}`
+functions both accept a monitor argument if you have one already.
+
+.Monitoring the connection process
+
+[source,erlang]
+{ok, ConnPid} = gun:open("example.org", 443).
+MRef = monitor(process, ConnPid).
+
+This monitor reference can be kept and used until the connection
+process exits.
+
+.Handling `DOWN` messages
+
+[source,erlang]
+receive
+ %% Receive Gun messages here...
+ {DOWN', Mref, process, ConnPid, Reason} ->
+ error_logger:error_msg("Oops!"),
+ exit(Reason);
+end.
+
+What to do when you receive a `DOWN` message is entirely up to you.
+
+=== Closing the connection abruptly
+
+The connection can be stopped abruptly at any time by calling
+the `gun:close/1` function.
+
+.Immediate closing of the connection
+
+[source,erlang]
+gun:close(ConnPid).
+
+The process is stopped immediately without having a chance to
+perform the protocol's closing handshake, if any.
+
+=== Closing the connection gracefully
+
+The connection can also be stopped gracefully by calling the
+`gun:shutdown/1` function.
+
+.Graceful shutdown of the connection
+
+[source,erlang]
+gun:shutdown(ConnPid).
+
+Gun will refuse any new requests or messages after you call
+this function. It will however continue to send you messages
+for existing streams until they are all completed.
+
+For example if you performed a GET request just before calling
+`gun:shutdown/1`, you will still receive the response before
+Gun closes the connection.
+
+If you set a monitor beforehand, you will receive a message
+when the connection has been closed.
diff --git a/doc/src/guide/http.asciidoc b/doc/src/guide/http.asciidoc
new file mode 100644
index 0000000..5a69fa1
--- /dev/null
+++ b/doc/src/guide/http.asciidoc
@@ -0,0 +1,367 @@
+== HTTP
+
+This chapter describes how to use the Gun client for
+communicating with an HTTP/1.1 or SPDY server.
+
+=== Streams
+
+Every time a request is initiated, Gun creates a _stream_.
+A _stream reference_ uniquely identifies a set of request and
+response(s) and must be used to perform additional operations
+with a stream or to identify its messages.
+
+Stream references use the Erlang _reference_ data type and
+are therefore unique.
+
+Streams can be canceled at any time. This will stop any further
+messages from being sent to the owner process. Depending on
+its capabilities, the server will also be instructed to cancel
+the request.
+
+Canceling a stream may result in Gun dropping the connection
+temporarily, to avoid uploading or downloading data that will
+not be used.
+
+.Cancelling a stream
+[source,erlang]
+gun:cancel(ConnPid, StreamRef).
+
+=== Sending requests
+
+Gun provides many convenient functions for performing common
+operations, like GET, POST or DELETE. It also provides a
+general purpose function in case you need other methods.
+
+The availability of these methods on the server can vary
+depending on the software used but also on a per-resource
+basis.
+
+Gun will automatically set a few headers depending on the
+method used. For all methods however it will set the host
+header if it has not been provided in the request arguments.
+
+This section focuses on the act of sending a request. The
+handling of responses will be explained further on.
+
+==== GET and HEAD
+
+Use `gun:get/{2,3}` to request a resource.
+
+.GET "/organizations/ninenines"
+
+[source,erlang]
+StreamRef = gun:get(ConnPid, "/organizations/ninenines").
+
+.GET "/organizations/ninenines" with custom headers
+
+[source,erlang]
+StreamRef = gun:get(ConnPid, "/organizations/ninenines", [
+ {<<"accept">>, "application/json"},
+ {<<"user-agent">>, "revolver/1.0"}
+]).
+
+Note that the list of headers has the field name as a binary.
+The field value is iodata, which is either a binary or an
+iolist.
+
+Use `gun:head/{2,3}` if you don't need the response body.
+
+.HEAD "/organizations/ninenines"
+
+[source,erlang]
+StreamRef = gun:head(ConnPid, "/organizations/ninenines").
+
+.HEAD "/organizations/ninenines" with custom headers
+
+[source,erlang]
+StreamRef = gun:head(ConnPid, "/organizations/ninenines", [
+ {<<"accept">>, "application/json"},
+ {<<"user-agent">>, "revolver/1.0"}
+]).
+
+It is not possible to send a request body with a GET or HEAD
+request.
+
+==== POST, PUT and PATCH
+
+HTTP defines three methods to create or update a resource.
+
+POST is generally used when the resource identifier (URI) isn't known
+in advance when creating the resource. POST can also be used to
+replace an existing resource, although PUT is more appropriate
+in that situation.
+
+PUT creates or replaces a resource identified by the URI.
+
+PATCH provides instructions on how to modify the resource.
+
+Both POST and PUT send the entire resource representation in their
+request body. The PATCH method can be used when this is not
+desirable. The request body of a PATCH method may be a partial
+representation or a list of instructions on how to update the
+resource.
+
+The `gun:post/4`, `gun:put/4` and `gun:patch/4` functions
+take a body as their fourth argument. These functions do
+not require any body-specific header to be set, although
+it is always recommended to set the content-type header.
+Gun will set the other headers automatically.
+
+In this and the following examples in this section, `gun:post`
+can be replaced by `gun:put` or `gun:patch` for performing
+a PUT or PATCH request, respectively.
+
+.POST "/organizations/ninenines"
+
+[source,erlang]
+Body = "{\"msg\": \"Hello world!\"}",
+StreamRef = gun:post(ConnPid, "/organizations/ninenines", [
+ {<<"content-type">>, "application/json"}
+], Body).
+
+The `gun:post/3`, `gun:put/3` and `gun:patch/3` functions
+do not take a body in their arguments. If a body is to be
+provided later on, using the `gun:data/4` function, then
+the request headers must indicate this. This can be done
+by setting the content-length or content-type request
+headers. If these headers are not set then Gun will assume
+the request has no body.
+
+It is recommended to send the content-length header if you
+know it in advance, although this is not required. If it
+is not set, HTTP/1.1 will use the chunked transfer-encoding,
+and SPDY will continue normally as it is chunked by design.
+
+@todo Stop relying on transfer encoding header in Gun
+
+@todo SPDY needs to remove invalid headers, and to detect
+a body if content-length is set
+
+.POST "/organizations/ninenines" with delayed body
+
+[source,erlang]
+Body = "{\"msg\": \"Hello world!\"}",
+StreamRef = gun:post(ConnPid, "/organizations/ninenines", [
+ {<<"content-length">>, integer_to_binary(length(Body))},
+ {<<"content-type">>, "application/json"}
+]),
+gun:data(ConnPid, StreamRef, fin, Body).
+
+The atom `fin` indicates this is the last chunk of data to
+be sent. You can call the `gun:data/4` function as many
+times as needed until you have sent the entire body. The
+last call must use `fin` and all the previous calls must
+use `nofin`. The last chunk may be empty.
+
+@todo what to do about empty chunk, ignore?
+
+.Streaming the request body
+
+[source,erlang]
+----
+sendfile(ConnPid, StreamRef, Filepath) ->
+ {ok, IoDevice} = file:open(Filepath, [read, binary, raw]),
+ do_sendfile(ConnPid, StreamRef, IoDevice).
+
+do_sendfile(ConnPid, StreamRef, IoDevice) ->
+ case file:read(IoDevice, 8000) of
+ eof ->
+ gun:data(ConnPid, StreamRef, fin, <<>>),
+ file:close(IoDevice);
+ {ok, Bin} ->
+ gun:data(ConnPid, StreamRef, nofin, Bin),
+ do_sendfile(ConnPid, StreamRef, IoDevice)
+ end.
+----
+
+==== DELETE
+
+Use `gun:delete/{2,3}` to delete a resource.
+
+.DELETE "/organizations/ninenines"
+
+[source,erlang]
+StreamRef = gun:delete(ConnPid, "/organizations/ninenines").
+
+.DELETE "/organizations/ninenines" with custom headers
+
+[source,erlang]
+StreamRef = gun:delete(ConnPid, "/organizations/ninenines", [
+ {<<"user-agent">>, "revolver/1.0"}
+]).
+
+==== OPTIONS
+
+Use `gun:options/{2,3}` to request information about a resource.
+
+.OPTIONS "/organizations/ninenines"
+
+[source,erlang]
+StreamRef = gun:options(ConnPid, "/organizations/ninenines").
+
+.OPTIONS "/organizations/ninenines" with custom headers
+
+[source,erlang]
+StreamRef = gun:options(ConnPid, "/organizations/ninenines", [
+ {<<"user-agent">>, "revolver/1.0"}
+]).
+
+You can also use this function to request information about
+the server itself.
+
+.OPTIONS "*"
+
+[source,erlang]
+StreamRef = gun:options(ConnPid, "*").
+
+==== Requests with an arbitrary method
+
+The `gun:request/{4,5}` function can be used to send requests
+with a configurable method name. It is mostly useful when you
+need a method that Gun does not understand natively.
+
+.Example of a TRACE request
+
+[source,erlang]
+gun:request(ConnPid, "TRACE", "/", [
+ {<<"max-forwards">>, "30"}
+]).
+
+=== Processing responses
+
+All data received from the server is sent to the owner
+process as a message. First a `gun_response` message is sent,
+followed by zero or more `gun_data` messages. If something goes wrong,
+a `gun_error` message is sent instead.
+
+The response message will inform you whether there will be
+data messages following. If it contains `fin` there will be
+no data messages. If it contains `nofin` then one or more data
+messages will follow.
+
+When using SPDY this value is sent with the frame and simply
+passed on in the message. When using HTTP/1.1 however Gun must
+guess whether data will follow by looking at the response headers.
+
+You can receive messages directly, or you can use the _await_
+functions to let Gun receive them for you.
+
+.Receiving a response using receive
+
+[source,erlang]
+----
+print_body(ConnPid, MRef) ->
+ StreamRef = gun:get(ConnPid, "/"),
+ receive
+ {gun_response, ConnPid, StreamRef, fin, Status, Headers} ->
+ no_data;
+ {gun_response, ConnPid, StreamRef, nofin, Status, Headers} ->
+ receive_data(ConnPid, MRef, StreamRef);
+ {'DOWN', MRef, process, ConnPid, Reason} ->
+ error_logger:error_msg("Oops!"),
+ exit(Reason)
+ after 1000 ->
+ exit(timeout)
+ end.
+
+receive_data(ConnPid, MRef, StreamRef) ->
+ receive
+ {gun_data, ConnPid, StreamRef, nofin, Data} ->
+ io:format("~s~n", [Data]),
+ receive_data(ConnPid, MRef, StreamRef);
+ {gun_data, ConnPid, StreamRef, fin, Data} ->
+ io:format("~s~n", [Data]);
+ {'DOWN', MRef, process, ConnPid, Reason} ->
+ error_logger:error_msg("Oops!"),
+ exit(Reason)
+ after 1000 ->
+ exit(timeout)
+ end.
+----
+
+While it may seem verbose, using messages like this has the
+advantage of never locking your process, allowing you to
+easily debug your code. It also allows you to start more than
+one connection and concurrently perform queries on all of them
+at the same time.
+
+You can also use Gun in a synchronous manner by using the _await_
+functions.
+
+The `gun:await/{2,3,4}` function will wait until it receives
+a response to, a pushed resource related to, or data from
+the given stream.
+
+When calling `gun:await/{2,3}` and not passing a monitor
+reference, one is automatically created for you for the
+duration of the call.
+
+The `gun:await_body/{2,3,4}` works similarly, but returns the
+body received. Both functions can be combined to receive the
+response and its body sequentially.
+
+.Receiving a response using await
+
+[source,erlang]
+StreamRef = gun:get(ConnPid, "/"),
+case gun:await(ConnPid, StreamRef) of
+ {response, fin, Status, Headers} ->
+ no_data;
+ {response, nofin, Status, Headers} ->
+ {ok, Body} = gun:await_body(ConnPid, StreamRef),
+ io:format("~s~n", [Body])
+end.
+
+=== Handling streams pushed by the server
+
+The SPDY protocol allows the server to push more than one
+resource for every request. It will start sending those
+extra resources before it starts sending the response itself,
+so Gun will send you `gun_push` messages before `gun_response`
+when that happens.
+
+You can safely choose to ignore `gun_push` messages, or
+you can handle them. If you do, you can either receive the
+messages directly or use _await_ functions.
+
+The `gun_push` message contains both the new stream reference
+and the stream reference of the original request.
+
+.Receiving a pushed response using receive
+
+[source,erlang]
+receive
+ {gun_push, ConnPid, OriginalStreamRef, PushedStreamRef,
+ Method, Host, Path, Headers} ->
+ enjoy()
+end.
+
+If you use the `gun:await/{2,3,4}` function, however, Gun
+will use the original reference to identify the message but
+will return a tuple that doesn't contain it.
+
+.Receiving a pushed response using await
+
+[source,erlang]
+{push, PushedStreamRef, Method, Host, Path, Headers}
+ = gun:await(ConnPid, OriginalStreamRef).
+
+The `PushedStreamRef` variable can then be used with `gun:await_body/{2,3,4}`
+if needed.
+
+=== Flushing unwanted messages
+
+Gun provides the function `gun:flush/1` to quickly get rid
+of unwanted messages sitting in the process mailbox. You
+can use it to get rid of all messages related to a connection,
+or just the messages related to a stream.
+
+.Flush all messages from a Gun connection
+
+[source,erlang]
+gun:flush(ConnPid).
+
+.Flush all messages from a specific stream
+
+[source,erlang]
+gun:flush(StreamRef).
diff --git a/guide/introduction.md b/doc/src/guide/introduction.asciidoc
index ca417ec..ade3d80 100644
--- a/guide/introduction.md
+++ b/doc/src/guide/introduction.asciidoc
@@ -1,35 +1,28 @@
-Introduction
-============
+== Introduction
-Purpose
--------
+Gun is an Erlang HTTP client with support for HTTP/1.1, SPDY and Websocket.
-Gun is an asynchronous SPDY, HTTP and Websocket client.
+=== Prerequisites
-Prerequisites
--------------
-
-Knowledge of Erlang, but also of the HTTP, SPDY and Websocket
+Knowledge of Erlang, but also of the HTTP/1.1, SPDY and Websocket
protocols is required in order to read this guide.
-Supported platforms
--------------------
+=== Supported platforms
Gun is tested and supported on Linux.
-Gun is developed for Erlang R16B+.
+Gun is developed for Erlang 17+.
Gun may be compiled on earlier Erlang versions with small source code
-modifications but there is no guarantee that it will work as expected.
+modifications but there is no guarantee that it will work as intended.
-Conventions
------------
+=== Conventions
In the HTTP protocol, the method name is case sensitive. All standard
method names are uppercase.
Header names are case insensitive. Gun converts all the header names
to lowercase, and expects your application to provide lowercase header
-names also.
+names.
The same applies to any other case insensitive value.
diff --git a/doc/src/guide/protocols.asciidoc b/doc/src/guide/protocols.asciidoc
new file mode 100644
index 0000000..2180c5b
--- /dev/null
+++ b/doc/src/guide/protocols.asciidoc
@@ -0,0 +1,119 @@
+== Supported protocols
+
+This chapter describes the protocols supported and the
+operations available to them.
+
+=== HTTP/1.1
+
+HTTP/1.1 is a text request-response protocol. The client
+sends a request, the server sends back a response.
+
+Gun provides convenience functions for performing GET, HEAD,
+OPTIONS, POST, PATCH, PUT, and DELETE requests. All these
+functions are aliases of `gun:request/{4,5}` for each respective
+methods. Gun also provides a `gun:data/4` function for streaming
+the request body.
+
+Gun will send a `gun_response` message for every response
+received, followed by zero or more `gun_data` messages for
+the response body. If something goes wrong, a `gun_error`
+will be sent instead.
+
+Gun provides convenience functions for dealing with messages.
+The `gun:await/{2,3,4}` function waits for a response to the given
+request, and the `gun:await_body/{2,3,4}` function for the
+response's body. The `gun:flush/1` function can be used to clear all
+messages related to a request or a connection from the mailbox
+of the process.
+
+The function `gun:cancel/2` can be used to silence the
+response to a request previously sent if it is no longer
+needed. When using HTTP/1.1 there is no multiplexing so
+Gun will have to receive the response fully before any
+other response can be received.
+
+Finally, Gun can upgrade an HTTP/1.1 connection to Websocket.
+It provides the `gun:ws_upgrade/{2,3,4}` function for that
+purpose. A `gun_ws_upgrade` message will be sent on success;
+a `gun_response` message otherwise.
+
+=== SPDY
+
+SPDY is a binary protocol based on HTTP, compatible with
+the HTTP semantics, that reduces the complexity of parsing
+requests and responses, compresses the HTTP headers and
+allows the server to push multiple responses to a single
+request.
+
+The SPDY interface is very similar to HTTP/1.1, so this
+section instead focuses on the differences in the interface
+for the two protocols.
+
+Because a SPDY server can push multiple responses to a
+single request, Gun might send `gun_push` messages for
+every push received. They can be ignored safely if they
+are not needed.
+
+The `gun:cancel/2` function will use the SPDY stream
+cancellation mechanism which allows Gun to inform the
+server to stop sending a response for this particular
+request, saving resources.
+
+It is not possible to upgrade a SPDY connection to Websocket
+due to protocol limitations.
+
+=== Websocket
+
+Websocket is a binary protocol built on top of HTTP that
+allows asynchronous concurrent communication between the
+client and the server. A Websocket server can push data to
+the client at any time.
+
+Websocket is only available as a connection upgrade over
+an HTTP/1.1 connection.
+
+Once the Websocket connection is established, the only
+operation available on this connection is sending Websocket
+frames using `gun:ws_send/2`.
+
+Gun will send a `gun_ws` message for every frame received.
+
+=== Summary
+
+The two following tables summarize the supported operations
+and the messages Gun sends depending on the connection's
+current protocol.
+
+.Supported operations per protocol
+[cols="<,3*^",options="header"]
+|===
+| Operation | HTTP/1.1 | SPDY | Websocket
+| delete | yes | yes | no
+| get | yes | yes | no
+| head | yes | yes | no
+| options | yes | yes | no
+| patch | yes | yes | no
+| post | yes | yes | no
+| put | yes | yes | no
+| request | yes | yes | no
+| data | yes | yes | no
+| await | yes | yes | no
+| await_body | yes | yes | no
+| flush | yes | yes | no
+| cancel | yes | yes | no
+| ws_upgrade | yes | no | no
+| ws_send | no | no | yes
+|===
+
+.Messages sent per protocol
+[cols="<,3*^",options="header"]
+|===
+| Message | HTTP/1.1 | SPDY | Websocket
+| gun_push | no | yes | no
+| gun_response | yes | yes | no
+| gun_data | yes | yes | no
+| gun_error (StreamRef) | yes | yes | no
+| gun_error | yes | yes | yes
+| gun_ws_upgrade | yes | no | no
+| gun_ws | no | no | yes
+|===
diff --git a/doc/src/guide/start.asciidoc b/doc/src/guide/start.asciidoc
new file mode 100644
index 0000000..6d93e2e
--- /dev/null
+++ b/doc/src/guide/start.asciidoc
@@ -0,0 +1,67 @@
+== Starting and stopping
+
+This chapter describes how to start and stop the Gun application.
+
+=== Setting up
+
+Before Gun can be used it needs to be in Erlang's `ERL_LIBS` path variable.
+If you use `erlang.mk` or a similar build tool, you only need to specify
+Gun as a dependency to your application and the tool will take care
+of downloading Gun and setting up paths.
+
+With `erlang.mk` this is done by adding `gun` to the `DEPS` variable
+in your Makefile.
+
+.Adding Gun as an erlang.mk dependency
+
+[source,make]
+DEPS = gun
+
+=== Starting
+
+Gun is an _OTP application_. It needs to be started before you can
+use it.
+
+.Starting Gun in an Erlang shell
+
+[source,erlang]
+----
+1> application:ensure_all_started(gun).
+{ok,[ranch,crypto,cowlib,asn1,public_key,ssl,gun]}
+----
+
+=== Stopping
+
+You can stop Gun using the `application:stop/1` function, however
+only Gun will be stopped. This is the equivalent of `application:start/1`.
+The `application_ensure_all_started/1` function has no equivalent for
+stopping all applications.
+
+.Stopping Gun
+
+[source,erlang]
+application:stop(gun).
+
+=== Using Gun with releases
+
+An _OTP release_ starts applications automatically. All you need
+to do is to set up your application resource file so that Gun can
+be included in the release. The application resource file can be
+found in `ebin/your_application.app`, or in `src/your_application.app.src`
+if you are using a build tool like `erlang.mk`.
+
+The key you need to change is the `applications` key. By default
+it only includes `kernel` and `stdlib`. You need to add `gun` to
+that list.
+
+.Adding Gun to the application resource file
+
+[source,erlang]
+{applications, [
+ kernel,
+ stdlib,
+ gun
+]}
+
+Do not put an extra comma at the end, the comma is a separator
+between the elements of the list.
diff --git a/doc/src/guide/websocket.asciidoc b/doc/src/guide/websocket.asciidoc
new file mode 100644
index 0000000..011e457
--- /dev/null
+++ b/doc/src/guide/websocket.asciidoc
@@ -0,0 +1,100 @@
+== Websocket
+
+This chapter describes how to use the Gun client for
+communicating with a Websocket server.
+
+@todo recovering from connection failure
+reconnecting to Websocket etc.
+
+=== HTTP upgrade
+
+Websocket is a protocol built on top of HTTP. To use Websocket,
+you must first request for the connection to be upgraded. Only
+HTTP/1.1 connections can be upgraded to Websocket, so you might
+need to restrict the protocol to HTTP/1.1 if you are planning
+to use Websocket over TLS.
+
+@todo add option to disable specific protocols
+
+You must use the `gun_ws:upgrade/{2,3}` function to upgrade
+to Websocket. This function can be called anytime after connection,
+so you can send HTTP requests before upgrading to Websocket.
+
+.Upgrade to Websocket
+
+[source,erlang]
+gun:ws_upgrade(ConnPid, "/websocket").
+
+Gun will set all the necessary headers for performing the
+Websocket upgrade, but you can specify additional headers
+if needed. For example you can request a custom sub-protocol.
+
+.Upgrade to Websocket and request a protocol
+
+[source,erlang]
+gun:ws_upgrade(ConnPid, "/websocket", [
+ {<<"sec-websocket-protocol">>, "mychat"}
+]).
+
+The success or failure of this operation will be sent as a
+message.
+
+@todo hmm we want the headers to be sent in the gun_ws_upgrade ok message too
+
+[source,erlang]
+receive
+ {gun_ws_upgrade, ConnPid, ok} ->
+ upgrade_success(ConnPid);
+ {gun_ws_upgrade, ConnPid, error, IsFin, Status, Headers} ->
+ exit({ws_upgrade_failed, Status, Headers});
+ %% More clauses here as needed.
+after 1000 ->
+ exit(timeout);
+end.
+
+=== Sending data
+
+Once the Websocket upgrade has completed successfully, you no
+longer have access to functions for performing requests. You
+can only send and receive Websocket messages.
+
+Use `gun:ws_send/2` to send one or more messages to the server.
+
+@todo Implement sending of N frames
+
+.Send a text frame
+
+[source,erlang]
+gun:ws_send(ConnPid, {text, "Hello!"}).
+
+.Send a text frame, a binary frame and then close the connection
+
+[source,erlang]
+gun:ws_send(ConnPid, [
+ {text, "Hello!"},
+ {binary, BinaryValue},
+ close
+]).
+
+Note that if you send a close frame, Gun will close the connection
+cleanly and will not attempt to reconnect afterwards, similar to
+calling `gun:shutdown/1`.
+
+=== Receiving data
+
+Gun sends an Erlang message to the owner process for every
+Websocket message it receives.
+
+[source,erlang]
+receive
+ {gun_ws, ConnPid, Frame} ->
+ handle_frame(ConnPid, Frame)
+end.
+
+@todo auto ping has not been implemented yet
+
+Gun will automatically send ping messages to the server to keep
+the connection alive, however if the connection dies and Gun has
+to reconnect it will not upgrade to Websocket automatically, you
+need to perform the operation when you receive the `gun_error`
+message.
diff --git a/doc/src/manual/gun.asciidoc b/doc/src/manual/gun.asciidoc
new file mode 100644
index 0000000..14641ee
--- /dev/null
+++ b/doc/src/manual/gun.asciidoc
@@ -0,0 +1,578 @@
+= gun(3)
+
+== Name
+
+gun - asynchronous HTTP client
+
+== Description
+
+The `gun` module provides an asynchronous interface for
+connecting and communicating with Web servers over SPDY,
+HTTP or Websocket.
+
+== Types
+
+=== opts() = [opt()]
+
+Configuration for the connection.
+
+@todo Should be a map.
+
+With opt():
+
+keepalive => pos_integer()::
+ Time between pings in milliseconds.
+ Defaults to 5000.
+retry => non_neg_integer()::
+ Number of times Gun will try to reconnect on failure before giving up.
+ Defaults to 5.
+retry_timeout => pos_integer()::
+ Time between retries in milliseconds.
+ Defaults to 5000.
+type => ssl | tcp | tcp_spdy::
+ Whether to use SSL, plain TCP (for HTTP/Websocket) or SPDY over TCP.
+ The default varies depending on the port used. Port 443 defaults
+ to ssl. Port 6121 defaults to tcp_spdy (@todo). All other ports
+ default to tcp. (@todo)
+
+@todo We want to separate protocol and transport options.
+
+@todo We need to document Websocket options.
+
+== Messages
+
+Calling functions from this module may result in the following
+messages being sent.
+
+=== {gun_push, ConnPid, StreamRef, NewStreamRef, URI, Headers}
+
+ConnPid = pid():: The pid of the Gun connection process.
+StreamRef = reference():: Identifier of the stream initiated by the owner process.
+NewStreamRef = reference():: Identifier of the stream being pushed.
+URI = binary():: URI of the resource.
+Headers = [{binary(), binary()}]:: Headers @todo
+
+A resource pushed alongside an HTTP response.
+
+This message can only be sent when the protocol is SPDY.
+
+@todo we probably want a function to know what protocol we connected
+@todo with or perhaps a message on connect that tells us that
+
+@todo I fear we also need the scheme; resource is identified by URI
+@todo Perhaps we really should send the URI entirely, because cache
+@todo relies on URI to work and this feature is for caching...
+@todo Not sure why Method is there, spec says it is only for GET
+
+=== {gun_response, ConnPid, StreamRef, IsFin, Status, Headers}
+
+ConnPid = pid():: The pid of the Gun connection process.
+StreamRef = reference():: Identifier of the stream initiated by the owner process.
+IsFin = fin | nofin:: Whether this message terminates the response.
+Status = binary():: Status line for the response.
+Headers = [{binary(), binary()}]:: Headers sent with the response.
+
+A response to an HTTP request.
+
+=== {gun_data, ConnPid, StreamRef, IsFin, Data}
+
+ConnPid = pid():: The pid of the Gun connection process.
+StreamRef = reference():: Identifier of the stream this data belongs to.
+IsFin = fin | nofin:: Whether this message terminates the response.
+Data = binary():: Data from the stream.
+
+Data associated with a stream.
+
+The stream in question can be either one initiated by the owner
+process or a stream initiated by the server through the push
+mechanism. In any case a `gun_response` or a `gun_push` message
+will be sent before any `gun_data` message.
+
+=== {gun_error, ConnPid, StreamRef, Reason}
+
+ConnPid = pid():: The pid of the Gun connection process.
+StreamRef = reference():: Identifier of the stream this error relates to.
+Reason = any():: Error reason.
+
+Stream-specific error.
+
+=== {gun_error, ConnPid, Reason}
+
+ConnPid = pid():: The pid of the Gun connection process.
+Reason = any():: Error reason.
+
+General error.
+
+=== {gun_ws_upgrade, ConnPid, ok}
+
+ConnPid = pid():: The pid of the Gun connection process.
+
+Successful upgrade to the Websocket protocol.
+
+@todo Yeah we need the headers.
+
+=== {gun_ws_upgrade, ConnPid, error, IsFin, Status, Headers}
+
+ConnPid = pid():: The pid of the Gun connection process.
+IsFin = fin | nofin:: Whether this message terminates the response.
+Status = binary():: Status line for the response.
+Headers = [{binary(), binary()}]:: Headers sent with the response.
+
+Failed upgrade to the Websocket protocol.
+
+=== {gun_ws, ConnPid, Frame}
+
+ConnPid = pid():: The pid of the Gun connection process.
+Frame = @todo:: Frame.
+
+Websocket frame.
+
+== Exports
+
+=== open(Host, Port) -> open(Host, Port, [])
+
+Alias of `gun:open/3`.
+
+=== open(Host, Port, Opts) -> {ok, ConnPid} | {error, Reason}
+
+Host = inet:hostname():: Host to connect to.
+Port = inet:port_number():: Port to connect to.
+Opts = opts():: Options for this connection.
+ConnPid = pid():: The pid of the Gun connection process.
+Reason = any():: Error reason. @todo really any?
+
+Open a connection to the given host and port.
+
+=== close(ConnPid) -> ok
+
+ConnPid = pid():: The pid of the Gun connection process.
+
+Brutally close the connection.
+
+=== shutdown(ConnPid) -> ok
+
+ConnPid = pid():: The pid of the Gun connection process.
+
+Gracefully close the connection.
+
+A monitor can be used to be notified when the connection is
+effectively closed.
+
+=== delete(ConnPid, Path) -> delete(ConnPid, Path, [])
+
+Alias of `gun:delete/3`.
+
+=== delete(ConnPid, Path, Headers) -> StreamRef
+
+ConnPid = pid():: The pid of the Gun connection process.
+Path = iodata():: Path to the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+StreamRef = reference():: Identifier of the stream for this request.
+
+Delete a resource.
+
+=== get(ConnPid, Path) -> get(ConnPid, Path, [])
+
+Alias of `gun:get/3`.
+
+=== get(ConnPid, Path, Headers) -> StreamRef
+
+ConnPid = pid():: The pid of the Gun connection process.
+Path = iodata():: Path to the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+StreamRef = reference():: Identifier of the stream for this request.
+
+Get a resource.
+
+=== head(ConnPid, Path) -> head(ConnPid, Path, [])
+
+Alias of `gun:head/3`.
+
+=== head(ConnPid, Path, Headers) -> StreamRef
+
+ConnPid = pid():: The pid of the Gun connection process.
+Path = iodata():: Path to the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+StreamRef = reference():: Identifier of the stream for this request.
+
+Get headers of a resource.
+
+This function performs the same operation as `get/{2,3}` except
+the server will not send the resource representation, only the
+response's status line and headers.
+
+While servers should send the same headers they would if the
+request was a GET, like `content-length`, it is not always
+the case and differences may exist.
+
+=== options(ConnPid, Path) -> options(ConnPid, Path, [])
+
+Alias of `gun:options/3`.
+
+=== options(ConnPid, Path, Headers) -> StreamRef
+
+ConnPid = pid():: The pid of the Gun connection process.
+Path = iodata():: Path to the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+StreamRef = reference():: Identifier of the stream for this request.
+
+Obtain information about the capabilities of the server or of a resource.
+
+The special path `"*"` can be used to obtain information about
+the server as a whole. Any other path will return information
+about the resource only.
+
+=== patch(ConnPid, Path, Headers) -> StreamRef
+
+ConnPid = pid():: The pid of the Gun connection process.
+Path = iodata():: Path to the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+StreamRef = reference():: Identifier of the stream for this request.
+
+Request that a set of changes be applied to the resource.
+
+This function expects either `content-length` or `content-type`
+to be set to know a body is going to be sent afterwards.
+Gun will assume the request has no body otherwise. It is
+highly recommended to set both when possible.
+
+The body sent in this request should be a patch document
+with instructions on how to update the resource.
+
+You can use the `gun:data/4` function to send the body, if any.
+
+=== patch(ConnPid, Path, Headers, Body) -> StreamRef
+
+ConnPid = pid():: The pid of the Gun connection process.
+Path = iodata():: Path to the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+Body = iodata():: Body of the request.
+StreamRef = reference():: Identifier of the stream for this request.
+
+Request that a set of changes be applied to the resource.
+
+It is highly recommended to set the `content-type` header
+to inform the server what media type the body contains.
+Gun will automatically set the `content-length` header.
+
+The body sent in this request should be a patch document
+with instructions on how to update the resource.
+
+The complete request is sent when calling this function.
+It is not possible to stream more of the body after
+calling it.
+
+=== post(ConnPid, Path, Headers) -> StreamRef
+
+ConnPid = pid():: The pid of the Gun connection process.
+Path = iodata():: Path to the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+StreamRef = reference():: Identifier of the stream for this request.
+
+Process the enclosed representation according to the resource's own semantics.
+
+This function expects either `content-length` or `content-type`
+to be set to know a body is going to be sent afterwards.
+Gun will assume the request has no body otherwise. It is
+highly recommended to set both when possible.
+
+The body sent in this request will be processed
+according to the resource's own semantics. A new
+resource may be created as a result, and may be
+located at a different URI.
+
+You can use the `gun:data/4` function to send the body, if any.
+
+=== post(ConnPid, Path, Headers, Body) -> StreamRef
+
+ConnPid = pid():: The pid of the Gun connection process.
+Path = iodata():: Path to the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+Body = iodata():: Body of the request.
+StreamRef = reference():: Identifier of the stream for this request.
+
+Process the enclosed representation according to the resource's own semantics.
+
+It is highly recommended to set the `content-type` header
+to inform the server what media type the body contains.
+Gun will automatically set the `content-length` header.
+
+The body sent in this request will be processed
+according to the resource's own semantics. A new
+resource may be created as a result, and may be
+located at a different URI.
+
+The complete request is sent when calling this function.
+It is not possible to stream more of the body after
+calling it.
+
+=== put(ConnPid, Path, Headers) -> StreamRef
+
+ConnPid = pid():: The pid of the Gun connection process.
+Path = iodata():: Path to the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+StreamRef = reference():: Identifier of the stream for this request.
+
+Create or replace a resource.
+
+The body of the request is the entire representation of the resource.
+
+This function expects either `content-length` or `content-type`
+to be set to know a body is going to be sent afterwards.
+Gun will assume the request has no body otherwise. It is
+highly recommended to set both when possible.
+
+You can use the `gun:data/4` function to send the body, if any.
+
+=== put(ConnPid, Path, Headers, Body) -> StreamRef
+
+ConnPid = pid():: The pid of the Gun connection process.
+Path = iodata():: Path to the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+Body = iodata():: Body of the request.
+StreamRef = reference():: Identifier of the stream for this request.
+
+Create or replace a resource.
+
+The body of the request is the entire representation of the resource.
+
+It is highly recommended to set the `content-type` header
+to inform the server what media type the body contains.
+Gun will automatically set the `content-length` header.
+
+The complete request is sent when calling this function.
+It is not possible to stream more of the body after
+calling it.
+
+=== request(ConnPid, Method, Path, Headers) -> StreamRef
+
+ConnPid = pid():: The pid of the Gun connection process.
+Method = iodata():: Request method.
+Path = iodata():: Path of the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+StreamRef = reference():: Identifier of the stream for this request.
+
+Perform the given request.
+
+This is a general purpose function that should only be used
+when existing method-specific functions don't apply.
+
+This function expects either `content-length` or `content-type`
+to be set to know a body is going to be sent afterwards.
+Gun will assume the request has no body otherwise. It is
+highly recommended to set both when possible.
+
+You can use the `gun:data/4` function to send the body, if any.
+
+=== request(ConnPid, Method, Path, Headers, Body) -> StreamRef
+
+ConnPid = pid():: The pid of the Gun connection process.
+Method = iodata():: Request method.
+Path = iodata():: Path of the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+Body = iodata():: Body of the request.
+StreamRef = reference():: Identifier of the stream for this request.
+
+Perform the given request.
+
+This is a general purpose function that should only be used
+when existing method-specific functions don't apply.
+
+It is highly recommended to set the `content-type` header
+to inform the server what media type the body contains.
+Gun will automatically set the `content-length` header.
+
+The complete request is sent when calling this function.
+It is not possible to stream more of the body after
+calling it.
+
+=== data(ConnPid, StreamRef, IsFin, Data) -> ok
+
+ConnPid = pid():: The pid of the Gun connection process.
+StreamRef = reference():: Identifier of the stream this data belongs to.
+IsFin = fin | nofin:: Whether this message terminates the request.
+Data = iodata():: Data to be sent with the request.
+
+Stream the body of a request.
+
+@todo empty chunks
+
+This function can only be used if the request identified by
+`StreamRef` came with headers indicating the presence of a
+body and that body not being given when creating the request.
+
+All calls to this function must use `nofin` except for the
+last which must use `fin` to indicate the end of the request
+body.
+
+Empty data is allowed regardless of the value of `IsFin`.
+Gun will not send empty data chunks unless required to
+indicate the request body is finished, however.
+
+=== await(ConnPid, StreamRef) -> await(ConnPid, StreamRef, 5000, MonitorRef)
+
+Alias of `gun:await/4`.
+
+A monitor `MonitorRef` is automatically created for the duration of
+this call and an error will be returned if the Gun connection process
+terminates.
+
+=== await(ConnPid, StreamRef, MonitorRef) -> await(ConnPid, StreamRef, 5000, MonitorRef)
+
+Alias of `gun:await/4`.
+
+=== await(ConnPid, StreamRef, Timeout) -> await(ConnPid, StreamRef, Timeout, MonitorRef)
+
+Alias of `gun:await/4`.
+
+A monitor `MonitorRef` is automatically created for the duration of
+this call and an error will be returned if the Gun connection process
+terminates.
+
+=== await(ConnPid, StreamRef, Timeout, MonitorRef) -> tuple() -- see below
+
+ConnPid = pid():: The pid of the Gun connection process.
+StreamRef = reference():: Identifier of the stream to await messages from.
+Timeout = timeout():: How long this function will wait for messages.
+MonitorRef = reference():: Monitor reference for the Gun connection process.
+
+Wait for a response message.
+
+This function can be used when a synchronous handling of
+responses is desired. It will only return when a message
+for the given stream is received, on error or on timeout.
+
+The return values are described in the next few subsections.
+
+==== {response, IsFin, Status, Headers}
+
+IsFin = fin | nofin:: Whether this message terminates the response.
+Status = binary():: Status line for the response.
+Headers = [{binary(), binary()}]:: Headers sent with the response.
+
+Equivalent of a `gun_response` message.
+
+==== {data, IsFin, Data}
+
+IsFin = fin | nofin:: Whether this message terminates the response.
+Data = binary():: Data from the stream.
+
+Equivalent of a `gun_data` message.
+
+==== {push, NewStreamRef, URI, Headers}
+
+NewStreamRef = reference():: Identifier of the stream being pushed.
+URI = binary():: URI of the resource.
+Headers = [{binary(), binary()}]:: Headers @todo
+
+Equivalent of a `gun_push` message.
+
+@todo Same changes as gun_push
+
+==== {error, Reason}
+
+Reason = any():: Error reason. @todo any?
+
+Equivalent of a `gun_error` message.
+
+@todo I think we want to distinguish a stream error, a general error,
+@todo a DOWN and a timeout error
+
+=== await_body(ConnPid, StreamRef) -> await_body(ConnPid, StreamRef, 5000, MonitorRef)
+
+Alias of `gun:await_body/4`.
+
+A monitor `MonitorRef` is automatically created for the duration of
+this call and an error will be returned if the Gun connection process
+terminates.
+
+=== await_body(ConnPid, StreamRef, MonitorRef) -> await_body(ConnPid, StreamRef, 5000, MonitorRef)
+
+Alias of `gun:await_body/4`.
+
+=== await_body(ConnPid, StreamRef, Timeout) -> await_body(ConnPid, StreamRef, Timeout, MonitorRef)
+
+Alias of `gun:await_body/4`.
+
+A monitor `MonitorRef` is automatically created for the duration of
+this call and an error will be returned if the Gun connection process
+terminates.
+
+=== await_body(ConnPid, StreamRef, Timeout, MonitorRef) -> {ok, Body} | {error, Reason}
+
+ConnPid = pid():: The pid of the Gun connection process.
+StreamRef = reference():: Identifier of the stream to await messages from.
+Timeout = timeout():: How long this function will wait for each message.
+MonitorRef = reference():: Monitor reference for the Gun connection process.
+Body = binary():: Body for the given stream.
+Reason = any():: Error reason. @todo any?
+
+Wait for a response body.
+
+This function can be used when a synchronous handling of
+responses is desired. It will only return when it has
+finished fetching the entire response body.
+
+The timeout value is *per message*. The actual function call
+can last much longer for large bodies.
+
+@todo I think we want to distinguish a stream error, a general error,
+@todo a DOWN and a timeout error
+
+@todo guide might be a little incorrect about await/await_body
+
+=== flush(ConnPid) -> ok
+
+ConnPid = pid():: The pid of the Gun connection process.
+
+Flush all messages from the Gun connection process from the mailbox.
+
+=== flush(StreamRef) -> ok
+
+StreamRef = reference():: Stream identifier.
+
+Flush all messages related to the given stream.
+
+=== cancel(ConnPid, StreamRef) -> ok
+
+ConnPid = pid():: The pid of the Gun connection process.
+StreamRef = reference():: Identifier of the stream to cancel.
+
+Cancel the given stream.
+
+HTTP/1.1 streams can't be cancelled. Gun will simply silence
+the stream and stop relaying messages.
+
+@todo Depending on the length
+@todo of a response Gun may also attempt to reconnect rather than
+@todo receive the entire response body.
+
+SPDY streams can however be cancelled at any time.
+
+=== ws_upgrade(ConnPid, Path) -> ws_upgrade(ConnPid, Path, [], #{})
+
+Alias of `gun:ws_upgrade/4`.
+
+=== ws_upgrade(ConnPid, Path, Headers) -> ws_upgrade(ConnPid, Path, Headers, #{})
+
+Alias of `gun:ws_upgrade/4`.
+
+=== ws_upgrade(ConnPid, Path, Headers, Opts) -> ok
+
+ConnPid = pid():: The pid of the Gun connection process.
+Path = iodata():: Path to the resource.
+Headers = [{binary(), iodata()}]:: Additional request headers.
+Opts = map():: Options for the Websocket connection.
+
+Request the connection to be upgraded to the Websocket protocol.
+
+@todo Only possible for HTTP.
+
+=== ws_send(ConnPid, Frames) -> ok
+
+ConnPid = pid():: The pid of the Gun connection process.
+Frames = @todo:: @todo
+
+Send one or more Websocket frames.
+
+This function can only be used following a successful `ws_upgrade` call.
diff --git a/doc/src/manual/gun_app.asciidoc b/doc/src/manual/gun_app.asciidoc
new file mode 100644
index 0000000..e4447d6
--- /dev/null
+++ b/doc/src/manual/gun_app.asciidoc
@@ -0,0 +1,24 @@
+= gun(7)
+
+== Name
+
+gun - Erlang HTTP client with support for HTTP/1.1, SPDY and Websocket.
+
+== Dependencies
+
+The `gun` application uses the Erlang applications `ranch`
+for abstracting TCP and TLS over a common interface, and
+the `ssl` application for TLS support, required for HTTPS
+and SPDY support. In addition, Gun requires the `crypto`
+application (a dependency of `ssl`) for Websocket.
+
+These dependencies must be started for the `gun`
+application to work. In an embedded environment
+this means that they need to be started with the
+`application:start/{1,2}` function before the `gun`
+application is started.
+
+== Environment
+
+The `gun` application does not define any application
+environment configuration parameters.
diff --git a/erlang.mk b/erlang.mk
index 7da0151..e6833bc 100644
--- a/erlang.mk
+++ b/erlang.mk
@@ -1,4 +1,4 @@
-# Copyright (c) 2013-2014, Loïc Hoguin <[email protected]>
+# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -12,72 +12,106 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-# Project.
+.PHONY: all deps app rel docs tests clean distclean help erlang-mk
-PROJECT ?= $(notdir $(CURDIR))
-
-# Packages database file.
-
-PKG_FILE ?= $(CURDIR)/.erlang.mk.packages.v1
-export PKG_FILE
+ERLANG_MK_VERSION = 1
-PKG_FILE_URL ?= https://raw.github.com/extend/erlang.mk/master/packages.v1.tsv
+# Core configuration.
-define get_pkg_file
- wget --no-check-certificate -O $(PKG_FILE) $(PKG_FILE_URL) || rm $(PKG_FILE)
-endef
+PROJECT ?= $(notdir $(CURDIR))
+PROJECT := $(strip $(PROJECT))
-# Verbosity and tweaks.
+# Verbosity.
V ?= 0
-appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src;
-appsrc_verbose = $(appsrc_verbose_$(V))
+gen_verbose_0 = @echo " GEN " $@;
+gen_verbose = $(gen_verbose_$(V))
-erlc_verbose_0 = @echo " ERLC " $(filter %.erl %.core,$(?F));
-erlc_verbose = $(erlc_verbose_$(V))
+# "erl" command.
-xyrl_verbose_0 = @echo " XYRL " $(filter %.xrl %.yrl,$(?F));
-xyrl_verbose = $(xyrl_verbose_$(V))
+ERL = erl +A0 -noinput -boot start_clean
-dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F));
-dtl_verbose = $(dtl_verbose_$(V))
+# Core targets.
-gen_verbose_0 = @echo " GEN " $@;
-gen_verbose = $(gen_verbose_$(V))
+ifneq ($(words $(MAKECMDGOALS)),1)
+.NOTPARALLEL:
+endif
-.PHONY: rel clean-rel all clean-all app clean deps clean-deps \
- docs clean-docs build-tests tests build-plt dialyze
+all:: deps
+ @$(MAKE) --no-print-directory app
+ @$(MAKE) --no-print-directory rel
-# Release.
+# Noop to avoid a Make warning when there's nothing to do.
+rel::
+ @echo -n
-RELX_CONFIG ?= $(CURDIR)/relx.config
+clean:: clean-crashdump
-ifneq ($(wildcard $(RELX_CONFIG)),)
+clean-crashdump:
+ifneq ($(wildcard erl_crash.dump),)
+ $(gen_verbose) rm -f erl_crash.dump
+endif
-RELX ?= $(CURDIR)/relx
-export RELX
+distclean:: clean
+
+help::
+ @printf "%s\n" \
+ "erlang.mk (version $(ERLANG_MK_VERSION)) is distributed under the terms of the ISC License." \
+ "Copyright (c) 2013-2014 Loïc Hoguin <[email protected]>" \
+ "" \
+ "Usage: [V=1] make [-jNUM] [target]" \
+ "" \
+ "Core targets:" \
+ " all Run deps, app and rel targets in that order" \
+ " deps Fetch dependencies (if needed) and compile them" \
+ " app Compile the project" \
+ " rel Build a release for this project, if applicable" \
+ " docs Build the documentation for this project" \
+ " tests Run the tests for this project" \
+ " clean Delete temporary and output files from most targets" \
+ " distclean Delete all temporary and output files" \
+ " help Display this help and exit" \
+ "" \
+ "The target clean only removes files that are commonly removed." \
+ "Dependencies and releases are left untouched." \
+ "" \
+ "Setting V=1 when calling make enables verbose mode." \
+ "Parallel execution is supported through the -j Make flag."
+
+# Core functions.
+
+ifeq ($(shell which wget 2>/dev/null | wc -l), 1)
+define core_http_get
+ wget --no-check-certificate -O $(1) $(2)|| rm $(1)
+endef
+else
+define core_http_get
+ $(ERL) -eval 'ssl:start(), inets:start(), case httpc:request(get, {"$(2)", []}, [{autoredirect, true}], []) of {ok, {{_, 200, _}, _, Body}} -> case file:write_file("$(1)", Body) of ok -> ok; {error, R1} -> halt(R1) end; {error, R2} -> halt(R2) end, halt(0).'
+endef
+endif
-RELX_URL ?= https://github.com/erlware/relx/releases/download/v0.6.0/relx
-RELX_OPTS ?=
+# Automated update.
-define get_relx
- wget -O $(RELX) $(RELX_URL) || rm $(RELX)
- chmod +x $(RELX)
-endef
+ERLANG_MK_BUILD_CONFIG ?= build.config
+ERLANG_MK_BUILD_DIR ?= .erlang.mk.build
-rel: clean-rel all $(RELX)
- @$(RELX) -c $(RELX_CONFIG) $(RELX_OPTS)
+erlang-mk:
+ git clone https://github.com/ninenines/erlang.mk $(ERLANG_MK_BUILD_DIR)
+ if [ -f $(ERLANG_MK_BUILD_CONFIG) ]; then cp $(ERLANG_MK_BUILD_CONFIG) $(ERLANG_MK_BUILD_DIR); fi
+ cd $(ERLANG_MK_BUILD_DIR) && make
+ cp $(ERLANG_MK_BUILD_DIR)/erlang.mk ./erlang.mk
+ rm -rf $(ERLANG_MK_BUILD_DIR)
-$(RELX):
- @$(call get_relx)
+# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
-clean-rel:
- @rm -rf _rel
+.PHONY: distclean-deps distclean-pkg pkg-list pkg-search
-endif
+# Configuration.
-# Deps directory.
+AUTOPATCH ?= edown gen_leader gproc
+export AUTOPATCH
DEPS_DIR ?= $(CURDIR)/deps
export DEPS_DIR
@@ -86,9 +120,6 @@ REBAR_DEPS_DIR = $(DEPS_DIR)
export REBAR_DEPS_DIR
ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DEPS))
-ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS))
-
-# Application.
ifeq ($(filter $(DEPS_DIR),$(subst :, ,$(ERL_LIBS))),)
ifeq ($(ERL_LIBS),)
@@ -99,26 +130,206 @@ endif
endif
export ERL_LIBS
-ERLC_OPTS ?= -Werror +debug_info +warn_export_all +warn_export_vars \
- +warn_shadow_vars +warn_obsolete_guard # +bin_opt_info +warn_missing_spec
+PKG_FILE2 ?= $(CURDIR)/.erlang.mk.packages.v2
+export PKG_FILE2
+
+PKG_FILE_URL ?= https://raw.githubusercontent.com/ninenines/erlang.mk/master/packages.v2.tsv
+
+# Core targets.
+
+deps:: $(ALL_DEPS_DIRS)
+ @for dep in $(ALL_DEPS_DIRS) ; do \
+ if [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ] ; then \
+ $(MAKE) -C $$dep ; \
+ else \
+ echo "include $(CURDIR)/erlang.mk" | ERLC_OPTS=+debug_info $(MAKE) -f - -C $$dep ; \
+ fi ; \
+ done
+
+distclean:: distclean-deps distclean-pkg
+
+# Deps related targets.
+
+define dep_autopatch
+ $(ERL) -eval " \
+DepDir = \"$(DEPS_DIR)/$(1)/\", \
+fun() -> \
+ {ok, Conf} = file:consult(DepDir ++ \"rebar.config\"), \
+ File = case lists:keyfind(deps, 1, Conf) of false -> []; {_, Deps} -> \
+ [begin {Method, Repo, Commit} = case Repos of \
+ {git, R} -> {git, R, master}; \
+ {M, R, {branch, C}} -> {M, R, C}; \
+ {M, R, {tag, C}} -> {M, R, C}; \
+ {M, R, C} -> {M, R, C} \
+ end, \
+ io_lib:format(\"DEPS += ~s\ndep_~s = ~s ~s ~s~n\", [Name, Name, Method, Repo, Commit]) \
+ end || {Name, _, Repos} <- Deps] \
+ end, \
+ ok = file:write_file(\"$(DEPS_DIR)/$(1)/Makefile\", [\"ERLC_OPTS = +debug_info\n\n\", File, \"\ninclude erlang.mk\"]) \
+end(), \
+AppSrcOut = \"$(DEPS_DIR)/$(1)/src/$(1).app.src\", \
+AppSrcIn = case filelib:is_regular(AppSrcOut) of false -> \"$(DEPS_DIR)/$(1)/ebin/$(1).app\"; true -> AppSrcOut end, \
+fun() -> \
+ {ok, [{application, $(1), L}]} = file:consult(AppSrcIn), \
+ L2 = case lists:keyfind(modules, 1, L) of {_, _} -> L; false -> [{modules, []}|L] end, \
+ L3 = case lists:keyfind(vsn, 1, L2) of {vsn, git} -> lists:keyreplace(vsn, 1, L2, {vsn, \"git\"}); _ -> L2 end, \
+ ok = file:write_file(AppSrcOut, io_lib:format(\"~p.~n\", [{application, $(1), L3}])) \
+end(), \
+case AppSrcOut of AppSrcIn -> ok; _ -> ok = file:delete(AppSrcIn) end, \
+halt()."
+endef
+
+ifeq ($(V),0)
+define dep_autopatch_verbose
+ @echo " PATCH " $(1);
+endef
+endif
+
+define dep_fetch
+ if [ "$$$$VS" = "git" ]; then \
+ git clone -n -- $$$$REPO $(DEPS_DIR)/$(1); \
+ cd $(DEPS_DIR)/$(1) && git checkout -q $$$$COMMIT; \
+ elif [ "$$$$VS" = "hg" ]; then \
+ hg clone -U $$$$REPO $(DEPS_DIR)/$(1); \
+ cd $(DEPS_DIR)/$(1) && hg update -q $$$$COMMIT; \
+ elif [ "$$$$VS" = "svn" ]; then \
+ svn checkout $$$$REPO $(DEPS_DIR)/$(1); \
+ else \
+ echo "Unknown or invalid dependency: $(1). Please consult the erlang.mk README for instructions." >&2; \
+ exit 78; \
+ fi
+endef
+
+define dep_target
+$(DEPS_DIR)/$(1):
+ @mkdir -p $(DEPS_DIR)
+ifeq (,$(dep_$(1)))
+ @if [ ! -f $(PKG_FILE2) ]; then $(call core_http_get,$(PKG_FILE2),$(PKG_FILE_URL)); fi
+ @DEPPKG=$$$$(awk 'BEGIN { FS = "\t" }; $$$$1 == "$(1)" { print $$$$2 " " $$$$3 " " $$$$4 }' $(PKG_FILE2);); \
+ VS=$$$$(echo $$$$DEPPKG | cut -d " " -f1); \
+ REPO=$$$$(echo $$$$DEPPKG | cut -d " " -f2); \
+ COMMIT=$$$$(echo $$$$DEPPKG | cut -d " " -f3); \
+ $(call dep_fetch,$(1))
+else
+ @VS=$(word 1,$(dep_$(1))); \
+ REPO=$(word 2,$(dep_$(1))); \
+ COMMIT=$(word 3,$(dep_$(1))); \
+ $(call dep_fetch,$(1))
+endif
+ifneq ($(filter $(1),$(AUTOPATCH)),)
+ $(call dep_autopatch_verbose,$(1)) if [ -f $(DEPS_DIR)/$(1)/rebar.config ]; then \
+ $(call dep_autopatch,$(1)); \
+ cd $(DEPS_DIR)/$(1)/ && ln -s ../../erlang.mk; \
+ elif [ ! -f $(DEPS_DIR)/$(1)/Makefile ]; then \
+ echo "ERLC_OPTS = +debug_info\ninclude erlang.mk" > $(DEPS_DIR)/$(1)/Makefile; \
+ cd $(DEPS_DIR)/$(1)/ && ln -s ../../erlang.mk; \
+ fi
+endif
+endef
+
+$(foreach dep,$(DEPS),$(eval $(call dep_target,$(dep))))
+
+distclean-deps:
+ $(gen_verbose) rm -rf $(DEPS_DIR)
+
+# Packages related targets.
+
+$(PKG_FILE2):
+ @$(call core_http_get,$(PKG_FILE2),$(PKG_FILE_URL))
+
+pkg-list: $(PKG_FILE2)
+ @cat $(PKG_FILE2) | awk 'BEGIN { FS = "\t" }; { print \
+ "Name:\t\t" $$1 "\n" \
+ "Repository:\t" $$3 "\n" \
+ "Website:\t" $$5 "\n" \
+ "Description:\t" $$6 "\n" }'
+
+ifdef q
+pkg-search: $(PKG_FILE2)
+ @cat $(PKG_FILE2) | grep -i ${q} | awk 'BEGIN { FS = "\t" }; { print \
+ "Name:\t\t" $$1 "\n" \
+ "Repository:\t" $$3 "\n" \
+ "Website:\t" $$5 "\n" \
+ "Description:\t" $$6 "\n" }'
+else
+pkg-search:
+ $(error Usage: make pkg-search q=STRING)
+endif
+
+ifeq ($(PKG_FILE2),$(CURDIR)/.erlang.mk.packages.v2)
+distclean-pkg:
+ $(gen_verbose) rm -f $(PKG_FILE2)
+endif
+
+help::
+ @printf "%s\n" "" \
+ "Package-related targets:" \
+ " pkg-list List all known packages" \
+ " pkg-search q=STRING Search for STRING in the package index"
+
+# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: clean-app
+
+# Configuration.
+
+ERLC_OPTS ?= -Werror +debug_info +warn_export_vars +warn_shadow_vars \
+ +warn_obsolete_guard # +bin_opt_info +warn_export_all +warn_missing_spec
COMPILE_FIRST ?=
COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST)))
+ERLC_EXCLUDE ?=
+ERLC_EXCLUDE_PATHS = $(addprefix src/,$(addsuffix .erl,$(ERLC_EXCLUDE)))
+
+ERLC_MIB_OPTS ?=
+COMPILE_MIB_FIRST ?=
+COMPILE_MIB_FIRST_PATHS = $(addprefix mibs/,$(addsuffix .mib,$(COMPILE_MIB_FIRST)))
+
+# Verbosity.
+
+appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src;
+appsrc_verbose = $(appsrc_verbose_$(V))
+
+erlc_verbose_0 = @echo " ERLC " $(filter-out $(patsubst %,%.erl,$(ERLC_EXCLUDE)),\
+ $(filter %.erl %.core,$(?F)));
+erlc_verbose = $(erlc_verbose_$(V))
-all: deps app
+xyrl_verbose_0 = @echo " XYRL " $(filter %.xrl %.yrl,$(?F));
+xyrl_verbose = $(xyrl_verbose_$(V))
+
+mib_verbose_0 = @echo " MIB " $(filter %.bin %.mib,$(?F));
+mib_verbose = $(mib_verbose_$(V))
-clean-all: clean clean-deps clean-docs
- $(gen_verbose) rm -rf .$(PROJECT).plt $(DEPS_DIR) logs
+# Targets.
+
+ifeq ($(wildcard ebin/test),)
+app:: app-build
+else
+app:: clean app-build
+endif
-app: ebin/$(PROJECT).app
+app-build: erlc-include ebin/$(PROJECT).app
$(eval MODULES := $(shell find ebin -type f -name \*.beam \
- | sed 's/ebin\///;s/\.beam/,/' | sed '$$s/.$$//'))
+ | sed "s/ebin\//'/;s/\.beam/',/" | sed '$$s/.$$//'))
+ @if [ -z "$$(grep -E '^[^%]*{modules,' src/$(PROJECT).app.src)" ]; then \
+ echo "Empty modules entry not found in $(PROJECT).app.src. Please consult the erlang.mk README for instructions." >&2; \
+ exit 1; \
+ fi
+ $(eval GITDESCRIBE := $(shell git describe --dirty --abbrev=7 --tags --always --first-parent 2>/dev/null || true))
$(appsrc_verbose) cat src/$(PROJECT).app.src \
- | sed 's/{modules,[[:space:]]*\[\]}/{modules, \[$(MODULES)\]}/' \
+ | sed "s/{modules,[[:space:]]*\[\]}/{modules, \[$(MODULES)\]}/" \
+ | sed "s/{id,[[:space:]]*\"git\"}/{id, \"$(GITDESCRIBE)\"}/" \
> ebin/$(PROJECT).app
+erlc-include:
+ -@if [ -d ebin/ ]; then \
+ find include/ src/ -type f -name \*.hrl -newer ebin -exec touch $(shell find src/ -type f -name "*.erl") \; 2>/dev/null || printf ''; \
+ fi
+
define compile_erl
$(erlc_verbose) erlc -v $(ERLC_OPTS) -o ebin/ \
- -pa ebin/ -I include/ $(COMPILE_FIRST_PATHS) $(1)
+ -pa ebin/ -I include/ $(filter-out $(ERLC_EXCLUDE_PATHS),\
+ $(COMPILE_FIRST_PATHS) $(1))
endef
define compile_xyrl
@@ -127,166 +338,938 @@ define compile_xyrl
@rm ebin/*.erl
endef
-define compile_dtl
- $(dtl_verbose) erl -noshell -pa ebin/ $(DEPS_DIR)/erlydtl/ebin/ -eval ' \
- Compile = fun(F) -> \
- Module = list_to_atom( \
- string:to_lower(filename:basename(F, ".dtl")) ++ "_dtl"), \
- erlydtl:compile(F, Module, [{out_dir, "ebin/"}]) \
- end, \
- _ = [Compile(F) || F <- string:tokens("$(1)", " ")], \
- init:stop()'
+define compile_mib
+ $(mib_verbose) erlc -v $(ERLC_MIB_OPTS) -o priv/mibs/ \
+ -I priv/mibs/ $(COMPILE_MIB_FIRST_PATHS) $(1)
+ $(mib_verbose) erlc -o include/ -- priv/mibs/*.bin
endef
-ebin/$(PROJECT).app: $(shell find src -type f -name \*.erl) \
- $(shell find src -type f -name \*.core) \
- $(shell find src -type f -name \*.xrl) \
- $(shell find src -type f -name \*.yrl) \
- $(shell find templates -type f -name \*.dtl 2>/dev/null)
+ifneq ($(wildcard src/),)
+ebin/$(PROJECT).app::
@mkdir -p ebin/
- $(if $(strip $(filter %.erl %.core,$?)), \
- $(call compile_erl,$(filter %.erl %.core,$?)))
- $(if $(strip $(filter %.xrl %.yrl,$?)), \
- $(call compile_xyrl,$(filter %.xrl %.yrl,$?)))
- $(if $(strip $(filter %.dtl,$?)), \
- $(call compile_dtl,$(filter %.dtl,$?)))
-clean:
- $(gen_verbose) rm -rf ebin/ test/*.beam erl_crash.dump
+ifneq ($(wildcard mibs/),)
+ebin/$(PROJECT).app:: $(shell find mibs -type f -name \*.mib)
+ @mkdir -p priv/mibs/ include
+ $(if $(strip $?),$(call compile_mib,$?))
+endif
-# Dependencies.
+ebin/$(PROJECT).app:: $(shell find src -type f -name \*.erl) \
+ $(shell find src -type f -name \*.core)
+ $(if $(strip $?),$(call compile_erl,$?))
-define get_dep
- @mkdir -p $(DEPS_DIR)
-ifeq (,$(findstring pkg://,$(word 1,$(dep_$(1)))))
- git clone -n -- $(word 1,$(dep_$(1))) $(DEPS_DIR)/$(1)
-else
- @if [ ! -f $(PKG_FILE) ]; then $(call get_pkg_file); fi
- git clone -n -- `awk 'BEGIN { FS = "\t" }; \
- $$$$1 == "$(subst pkg://,,$(word 1,$(dep_$(1))))" { print $$$$2 }' \
- $(PKG_FILE)` $(DEPS_DIR)/$(1)
+ebin/$(PROJECT).app:: $(shell find src -type f -name \*.xrl) \
+ $(shell find src -type f -name \*.yrl)
+ $(if $(strip $?),$(call compile_xyrl,$?))
endif
- cd $(DEPS_DIR)/$(1) ; git checkout -q $(word 2,$(dep_$(1)))
-endef
-define dep_target
-$(DEPS_DIR)/$(1):
- $(call get_dep,$(1))
-endef
+clean:: clean-app
-$(foreach dep,$(DEPS),$(eval $(call dep_target,$(dep))))
+clean-app:
+ $(gen_verbose) rm -rf ebin/ priv/mibs/ \
+ $(addprefix include/,$(addsuffix .hrl,$(notdir $(basename $(wildcard mibs/*.mib)))))
-deps: $(ALL_DEPS_DIRS)
- @for dep in $(ALL_DEPS_DIRS) ; do \
- if [ -f $$dep/Makefile ] ; then \
- $(MAKE) -C $$dep ; \
- else \
- echo "include $(CURDIR)/erlang.mk" | $(MAKE) -f - -C $$dep ; \
- fi ; \
- done
+# Copyright (c) 2015, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
-clean-deps:
- @for dep in $(ALL_DEPS_DIRS) ; do \
- if [ -f $$dep/Makefile ] ; then \
- $(MAKE) -C $$dep clean ; \
- else \
- echo "include $(CURDIR)/erlang.mk" | $(MAKE) -f - -C $$dep clean ; \
- fi ; \
- done
+.PHONY: test-deps test-dir test-build clean-test-dir
-# Documentation.
+# Configuration.
-EDOC_OPTS ?=
+TEST_DIR ?= test
-docs: clean-docs
- $(gen_verbose) erl -noshell \
- -eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), init:stop().'
+ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS))
-clean-docs:
- $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info
+TEST_ERLC_OPTS ?= +debug_info +warn_export_vars +warn_shadow_vars +warn_obsolete_guard
+TEST_ERLC_OPTS += -DTEST=1
-# Tests.
+# Targets.
$(foreach dep,$(TEST_DEPS),$(eval $(call dep_target,$(dep))))
-TEST_ERLC_OPTS ?= +debug_info +warn_export_vars +warn_shadow_vars +warn_obsolete_guard
-TEST_ERLC_OPTS += -DTEST=1 -DEXTRA=1 +'{parse_transform, eunit_autoexport}'
-
-build-test-deps: $(ALL_TEST_DEPS_DIRS)
+test-deps: $(ALL_TEST_DEPS_DIRS)
@for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep; done
-build-tests: build-test-deps
- $(gen_verbose) erlc -v $(TEST_ERLC_OPTS) -o test/ \
- $(wildcard test/*.erl test/*/*.erl) -pa ebin/
+ifneq ($(strip $(TEST_DIR)),)
+test-dir:
+ $(gen_verbose) erlc -v $(TEST_ERLC_OPTS) -I include/ -o $(TEST_DIR) \
+ $(wildcard $(TEST_DIR)/*.erl $(TEST_DIR)/*/*.erl) -pa ebin/
+endif
+
+ifeq ($(wildcard ebin/test),)
+test-build: ERLC_OPTS=$(TEST_ERLC_OPTS)
+test-build: clean deps test-deps
+ @$(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)"
+ $(gen_verbose) touch ebin/test
+else
+test-build: ERLC_OPTS=$(TEST_ERLC_OPTS)
+test-build: deps test-deps
+ @$(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)"
+endif
+
+clean:: clean-test-dir
+
+clean-test-dir:
+ifneq ($(wildcard $(TEST_DIR)/*.beam),)
+ $(gen_verbose) rm -f $(TEST_DIR)/*.beam
+endif
+
+# Copyright (c) 2014-2015, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: bootstrap bootstrap-lib bootstrap-rel new list-templates
+
+# Core targets.
+
+help::
+ @printf "%s\n" "" \
+ "Bootstrap targets:" \
+ " bootstrap Generate a skeleton of an OTP application" \
+ " bootstrap-lib Generate a skeleton of an OTP library" \
+ " bootstrap-rel Generate the files needed to build a release" \
+ " new t=TPL n=NAME Generate a module NAME based on the template TPL" \
+ " list-templates List available templates"
+
+# Bootstrap templates.
+
+bs_appsrc = "{application, $(PROJECT), [" \
+ " {description, \"\"}," \
+ " {vsn, \"0.1.0\"}," \
+ " {id, \"git\"}," \
+ " {modules, []}," \
+ " {registered, []}," \
+ " {applications, [" \
+ " kernel," \
+ " stdlib" \
+ " ]}," \
+ " {mod, {$(PROJECT)_app, []}}," \
+ " {env, []}" \
+ "]}."
+bs_appsrc_lib = "{application, $(PROJECT), [" \
+ " {description, \"\"}," \
+ " {vsn, \"0.1.0\"}," \
+ " {id, \"git\"}," \
+ " {modules, []}," \
+ " {registered, []}," \
+ " {applications, [" \
+ " kernel," \
+ " stdlib" \
+ " ]}" \
+ "]}."
+bs_Makefile = "PROJECT = $(PROJECT)" \
+ "include erlang.mk"
+bs_app = "-module($(PROJECT)_app)." \
+ "-behaviour(application)." \
+ "" \
+ "-export([start/2])." \
+ "-export([stop/1])." \
+ "" \
+ "start(_Type, _Args) ->" \
+ " $(PROJECT)_sup:start_link()." \
+ "" \
+ "stop(_State) ->" \
+ " ok."
+bs_relx_config = "{release, {$(PROJECT)_release, \"1\"}, [$(PROJECT)]}." \
+ "{extended_start_script, true}." \
+ "{sys_config, \"rel/sys.config\"}." \
+ "{vm_args, \"rel/vm.args\"}."
+bs_sys_config = "[" \
+ "]."
+bs_vm_args = "-name $(PROJECT)@127.0.0.1" \
+ "-setcookie $(PROJECT)" \
+ "-heart"
+# Normal templates.
+tpl_supervisor = "-module($(n))." \
+ "-behaviour(supervisor)." \
+ "" \
+ "-export([start_link/0])." \
+ "-export([init/1])." \
+ "" \
+ "start_link() ->" \
+ " supervisor:start_link({local, ?MODULE}, ?MODULE, [])." \
+ "" \
+ "init([]) ->" \
+ " Procs = []," \
+ " {ok, {{one_for_one, 1, 5}, Procs}}."
+tpl_gen_server = "-module($(n))." \
+ "-behaviour(gen_server)." \
+ "" \
+ "%% API." \
+ "-export([start_link/0])." \
+ "" \
+ "%% gen_server." \
+ "-export([init/1])." \
+ "-export([handle_call/3])." \
+ "-export([handle_cast/2])." \
+ "-export([handle_info/2])." \
+ "-export([terminate/2])." \
+ "-export([code_change/3])." \
+ "" \
+ "-record(state, {" \
+ "})." \
+ "" \
+ "%% API." \
+ "" \
+ "-spec start_link() -> {ok, pid()}." \
+ "start_link() ->" \
+ " gen_server:start_link(?MODULE, [], [])." \
+ "" \
+ "%% gen_server." \
+ "" \
+ "init([]) ->" \
+ " {ok, \#state{}}." \
+ "" \
+ "handle_call(_Request, _From, State) ->" \
+ " {reply, ignored, State}." \
+ "" \
+ "handle_cast(_Msg, State) ->" \
+ " {noreply, State}." \
+ "" \
+ "handle_info(_Info, State) ->" \
+ " {noreply, State}." \
+ "" \
+ "terminate(_Reason, _State) ->" \
+ " ok." \
+ "" \
+ "code_change(_OldVsn, State, _Extra) ->" \
+ " {ok, State}."
+tpl_gen_fsm = "-module($(n))." \
+ "-behaviour(gen_fsm)." \
+ "" \
+ "%% API." \
+ "-export([start_link/0])." \
+ "" \
+ "%% gen_fsm." \
+ "-export([init/1])." \
+ "-export([state_name/2])." \
+ "-export([handle_event/3])." \
+ "-export([state_name/3])." \
+ "-export([handle_sync_event/4])." \
+ "-export([handle_info/3])." \
+ "-export([terminate/3])." \
+ "-export([code_change/4])." \
+ "" \
+ "-record(state, {" \
+ "})." \
+ "" \
+ "%% API." \
+ "" \
+ "-spec start_link() -> {ok, pid()}." \
+ "start_link() ->" \
+ " gen_fsm:start_link(?MODULE, [], [])." \
+ "" \
+ "%% gen_fsm." \
+ "" \
+ "init([]) ->" \
+ " {ok, state_name, \#state{}}." \
+ "" \
+ "state_name(_Event, StateData) ->" \
+ " {next_state, state_name, StateData}." \
+ "" \
+ "handle_event(_Event, StateName, StateData) ->" \
+ " {next_state, StateName, StateData}." \
+ "" \
+ "state_name(_Event, _From, StateData) ->" \
+ " {reply, ignored, state_name, StateData}." \
+ "" \
+ "handle_sync_event(_Event, _From, StateName, StateData) ->" \
+ " {reply, ignored, StateName, StateData}." \
+ "" \
+ "handle_info(_Info, StateName, StateData) ->" \
+ " {next_state, StateName, StateData}." \
+ "" \
+ "terminate(_Reason, _StateName, _StateData) ->" \
+ " ok." \
+ "" \
+ "code_change(_OldVsn, StateName, StateData, _Extra) ->" \
+ " {ok, StateName, StateData}."
+tpl_cowboy_http = "-module($(n))." \
+ "-behaviour(cowboy_http_handler)." \
+ "" \
+ "-export([init/3])." \
+ "-export([handle/2])." \
+ "-export([terminate/3])." \
+ "" \
+ "-record(state, {" \
+ "})." \
+ "" \
+ "init(_, Req, _Opts) ->" \
+ " {ok, Req, \#state{}}." \
+ "" \
+ "handle(Req, State=\#state{}) ->" \
+ " {ok, Req2} = cowboy_req:reply(200, Req)," \
+ " {ok, Req2, State}." \
+ "" \
+ "terminate(_Reason, _Req, _State) ->" \
+ " ok."
+tpl_cowboy_loop = "-module($(n))." \
+ "-behaviour(cowboy_loop_handler)." \
+ "" \
+ "-export([init/3])." \
+ "-export([info/3])." \
+ "-export([terminate/3])." \
+ "" \
+ "-record(state, {" \
+ "})." \
+ "" \
+ "init(_, Req, _Opts) ->" \
+ " {loop, Req, \#state{}, 5000, hibernate}." \
+ "" \
+ "info(_Info, Req, State) ->" \
+ " {loop, Req, State, hibernate}." \
+ "" \
+ "terminate(_Reason, _Req, _State) ->" \
+ " ok."
+tpl_cowboy_rest = "-module($(n))." \
+ "" \
+ "-export([init/3])." \
+ "-export([content_types_provided/2])." \
+ "-export([get_html/2])." \
+ "" \
+ "init(_, _Req, _Opts) ->" \
+ " {upgrade, protocol, cowboy_rest}." \
+ "" \
+ "content_types_provided(Req, State) ->" \
+ " {[{{<<\"text\">>, <<\"html\">>, '*'}, get_html}], Req, State}." \
+ "" \
+ "get_html(Req, State) ->" \
+ " {<<\"<html><body>This is REST!</body></html>\">>, Req, State}."
+tpl_cowboy_ws = "-module($(n))." \
+ "-behaviour(cowboy_websocket_handler)." \
+ "" \
+ "-export([init/3])." \
+ "-export([websocket_init/3])." \
+ "-export([websocket_handle/3])." \
+ "-export([websocket_info/3])." \
+ "-export([websocket_terminate/3])." \
+ "" \
+ "-record(state, {" \
+ "})." \
+ "" \
+ "init(_, _, _) ->" \
+ " {upgrade, protocol, cowboy_websocket}." \
+ "" \
+ "websocket_init(_, Req, _Opts) ->" \
+ " Req2 = cowboy_req:compact(Req)," \
+ " {ok, Req2, \#state{}}." \
+ "" \
+ "websocket_handle({text, Data}, Req, State) ->" \
+ " {reply, {text, Data}, Req, State};" \
+ "websocket_handle({binary, Data}, Req, State) ->" \
+ " {reply, {binary, Data}, Req, State};" \
+ "websocket_handle(_Frame, Req, State) ->" \
+ " {ok, Req, State}." \
+ "" \
+ "websocket_info(_Info, Req, State) ->" \
+ " {ok, Req, State}." \
+ "" \
+ "websocket_terminate(_Reason, _Req, _State) ->" \
+ " ok."
+tpl_ranch_protocol = "-module($(n))." \
+ "-behaviour(ranch_protocol)." \
+ "" \
+ "-export([start_link/4])." \
+ "-export([init/4])." \
+ "" \
+ "-type opts() :: []." \
+ "-export_type([opts/0])." \
+ "" \
+ "-record(state, {" \
+ " socket :: inet:socket()," \
+ " transport :: module()" \
+ "})." \
+ "" \
+ "start_link(Ref, Socket, Transport, Opts) ->" \
+ " Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts])," \
+ " {ok, Pid}." \
+ "" \
+ "-spec init(ranch:ref(), inet:socket(), module(), opts()) -> ok." \
+ "init(Ref, Socket, Transport, _Opts) ->" \
+ " ok = ranch:accept_ack(Ref)," \
+ " loop(\#state{socket=Socket, transport=Transport})." \
+ "" \
+ "loop(State) ->" \
+ " loop(State)."
+
+# Plugin-specific targets.
+
+bootstrap:
+ifneq ($(wildcard src/),)
+ $(error Error: src/ directory already exists)
+endif
+ @printf "%s\n" $(bs_Makefile) > Makefile
+ @mkdir src/
+ @printf "%s\n" $(bs_appsrc) > src/$(PROJECT).app.src
+ @printf "%s\n" $(bs_app) > src/$(PROJECT)_app.erl
+ $(eval n := $(PROJECT)_sup)
+ @printf "%s\n" $(tpl_supervisor) > src/$(PROJECT)_sup.erl
+
+bootstrap-lib:
+ifneq ($(wildcard src/),)
+ $(error Error: src/ directory already exists)
+endif
+ @printf "%s\n" $(bs_Makefile) > Makefile
+ @mkdir src/
+ @printf "%s\n" $(bs_appsrc_lib) > src/$(PROJECT).app.src
+
+bootstrap-rel:
+ifneq ($(wildcard relx.config),)
+ $(error Error: relx.config already exists)
+endif
+ifneq ($(wildcard rel/),)
+ $(error Error: rel/ directory already exists)
+endif
+ @printf "%s\n" $(bs_relx_config) > relx.config
+ @mkdir rel/
+ @printf "%s\n" $(bs_sys_config) > rel/sys.config
+ @printf "%s\n" $(bs_vm_args) > rel/vm.args
+
+new:
+ifeq ($(wildcard src/),)
+ $(error Error: src/ directory does not exist)
+endif
+ifndef t
+ $(error Usage: make new t=TEMPLATE n=NAME)
+endif
+ifndef tpl_$(t)
+ $(error Unknown template)
+endif
+ifndef n
+ $(error Usage: make new t=TEMPLATE n=NAME)
+endif
+ @printf "%s\n" $(tpl_$(t)) > src/$(n).erl
+
+list-templates:
+ @echo Available templates: $(sort $(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES))))
+
+# Copyright (c) 2014-2015, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: clean-c_src distclean-c_src-env
+# todo
+
+# Configuration.
+
+C_SRC_DIR = $(CURDIR)/c_src
+C_SRC_ENV ?= $(C_SRC_DIR)/env.mk
+C_SRC_OUTPUT ?= $(CURDIR)/priv/$(PROJECT).so
+
+# System type and C compiler/flags.
+
+UNAME_SYS := $(shell uname -s)
+ifeq ($(UNAME_SYS), Darwin)
+ CC ?= cc
+ CFLAGS ?= -O3 -std=c99 -arch x86_64 -finline-functions -Wall -Wmissing-prototypes
+ CXXFLAGS ?= -O3 -arch x86_64 -finline-functions -Wall
+ LDFLAGS ?= -arch x86_64 -flat_namespace -undefined suppress
+else ifeq ($(UNAME_SYS), FreeBSD)
+ CC ?= cc
+ CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes
+ CXXFLAGS ?= -O3 -finline-functions -Wall
+else ifeq ($(UNAME_SYS), Linux)
+ CC ?= gcc
+ CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes
+ CXXFLAGS ?= -O3 -finline-functions -Wall
+endif
+
+CFLAGS += -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR)
+CXXFLAGS += -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR)
+
+LDLIBS += -L $(ERL_INTERFACE_LIB_DIR) -lerl_interface -lei
+LDFLAGS += -shared
+
+# Verbosity.
+
+c_verbose_0 = @echo " C " $(?F);
+c_verbose = $(c_verbose_$(V))
+
+cpp_verbose_0 = @echo " CPP " $(?F);
+cpp_verbose = $(cpp_verbose_$(V))
+
+link_verbose_0 = @echo " LD " $(@F);
+link_verbose = $(link_verbose_$(V))
+
+# Targets.
+
+ifeq ($(wildcard $(C_SRC_DIR)),)
+else ifneq ($(wildcard $(C_SRC_DIR)/Makefile),)
+app::
+ $(MAKE) -C $(C_SRC_DIR)
+
+clean::
+ $(MAKE) -C $(C_SRC_DIR) clean
+
+else
+SOURCES := $(shell find $(C_SRC_DIR) -type f \( -name "*.c" -o -name "*.C" -o -name "*.cc" -o -name "*.cpp" \))
+OBJECTS = $(addsuffix .o, $(basename $(SOURCES)))
+
+COMPILE_C = $(c_verbose) $(CC) $(CFLAGS) $(CPPFLAGS) -c
+COMPILE_CPP = $(cpp_verbose) $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c
+
+app:: $(C_SRC_ENV) $(C_SRC_OUTPUT)
+
+$(C_SRC_OUTPUT): $(OBJECTS)
+ @mkdir -p priv/
+ $(link_verbose) $(CC) $(OBJECTS) $(LDFLAGS) $(LDLIBS) -o $(C_SRC_OUTPUT)
+
+%.o: %.c
+ $(COMPILE_C) $(OUTPUT_OPTION) $<
+
+%.o: %.cc
+ $(COMPILE_CPP) $(OUTPUT_OPTION) $<
+
+%.o: %.C
+ $(COMPILE_CPP) $(OUTPUT_OPTION) $<
+
+%.o: %.cpp
+ $(COMPILE_CPP) $(OUTPUT_OPTION) $<
+
+$(C_SRC_ENV):
+ @$(ERL) -eval "file:write_file(\"$(C_SRC_ENV)\", \
+ io_lib:format( \
+ \"ERTS_INCLUDE_DIR ?= ~s/erts-~s/include/~n\" \
+ \"ERL_INTERFACE_INCLUDE_DIR ?= ~s~n\" \
+ \"ERL_INTERFACE_LIB_DIR ?= ~s~n\", \
+ [code:root_dir(), erlang:system_info(version), \
+ code:lib_dir(erl_interface, include), \
+ code:lib_dir(erl_interface, lib)])), \
+ halt()."
+
+clean:: clean-c_src
+
+clean-c_src:
+ $(gen_verbose) rm -f $(C_SRC_OUTPUT) $(OBJECTS)
+
+distclean:: distclean-c_src-env
+
+distclean-c_src-env:
+ $(gen_verbose) rm -f $(C_SRC_ENV)
+
+-include $(C_SRC_ENV)
+endif
+
+# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: ct distclean-ct
+
+# Configuration.
CT_OPTS ?=
+ifneq ($(wildcard $(TEST_DIR)),)
+ CT_SUITES ?= $(sort $(subst _SUITE.erl,,$(shell find $(TEST_DIR) -type f -name \*_SUITE.erl -exec basename {} \;)))
+else
+ CT_SUITES ?=
+endif
+
+# Core targets.
+
+tests:: ct
+
+distclean:: distclean-ct
+
+help::
+ @printf "%s\n" "" \
+ "Common_test targets:" \
+ " ct Run all the common_test suites for this project" \
+ "" \
+ "All your common_test suites have their associated targets." \
+ "A suite named http_SUITE can be ran using the ct-http target."
+
+# Plugin-specific targets.
+
CT_RUN = ct_run \
-no_auto_compile \
- -noshell \
- -pa $(realpath ebin) $(DEPS_DIR)/*/ebin \
- -dir test \
- -logdir logs \
- $(CT_OPTS)
-
-CT_SUITES ?=
-
-define test_target
-test_$(1): ERLC_OPTS = $(TEST_ERLC_OPTS)
-test_$(1): clean deps app build-tests
- @if [ -d "test" ] ; \
- then \
- mkdir -p logs/ ; \
- $(CT_RUN) -suite $(addsuffix _SUITE,$(1)) ; \
- fi
- $(gen_verbose) rm -f test/*.beam
+ -noinput \
+ -pa ebin $(DEPS_DIR)/*/ebin \
+ -dir $(TEST_DIR) \
+ -logdir logs
+
+ifeq ($(CT_SUITES),)
+ct:
+else
+ct: test-build
+ @mkdir -p logs/
+ $(gen_verbose) $(CT_RUN) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS)
+endif
+
+define ct_suite_target
+ct-$(1): test-build
+ @mkdir -p logs/
+ $(gen_verbose) $(CT_RUN) -suite $(addsuffix _SUITE,$(1)) $(CT_OPTS)
endef
-$(foreach test,$(CT_SUITES),$(eval $(call test_target,$(test))))
+$(foreach test,$(CT_SUITES),$(eval $(call ct_suite_target,$(test))))
-tests: ERLC_OPTS = $(TEST_ERLC_OPTS)
-tests: clean deps app build-tests
- @if [ -d "test" ] ; \
- then \
- mkdir -p logs/ ; \
- $(CT_RUN) -suite $(addsuffix _SUITE,$(CT_SUITES)) ; \
- fi
- $(gen_verbose) rm -f test/*.beam
+distclean-ct:
+ $(gen_verbose) rm -rf logs/
+
+# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: plt distclean-plt dialyze
-# Dialyzer.
+# Configuration.
DIALYZER_PLT ?= $(CURDIR)/.$(PROJECT).plt
export DIALYZER_PLT
PLT_APPS ?=
+DIALYZER_DIRS ?= --src -r src
DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions \
-Wunmatched_returns # -Wunderspecs
-build-plt: deps app
+# Core targets.
+
+distclean:: distclean-plt
+
+help::
+ @printf "%s\n" "" \
+ "Dialyzer targets:" \
+ " plt Build a PLT file for this project" \
+ " dialyze Analyze the project using Dialyzer"
+
+# Plugin-specific targets.
+
+$(DIALYZER_PLT): deps app
@dialyzer --build_plt --apps erts kernel stdlib $(PLT_APPS) $(ALL_DEPS_DIRS)
+plt: $(DIALYZER_PLT)
+
+distclean-plt:
+ $(gen_verbose) rm -f $(DIALYZER_PLT)
+
+ifneq ($(wildcard $(DIALYZER_PLT)),)
dialyze:
- @dialyzer --src src --no_native $(DIALYZER_OPTS)
+else
+dialyze: $(DIALYZER_PLT)
+endif
+ @dialyzer --no_native $(DIALYZER_DIRS) $(DIALYZER_OPTS)
-# Packages.
+# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
+# Copyright (c) 2015, Viktor Söderqvist <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
-$(PKG_FILE):
- @$(call get_pkg_file)
+.PHONY: distclean-edoc build-doc-deps
-pkg-list: $(PKG_FILE)
- @cat $(PKG_FILE) | awk 'BEGIN { FS = "\t" }; { print \
- "Name:\t\t" $$1 "\n" \
- "Repository:\t" $$2 "\n" \
- "Website:\t" $$3 "\n" \
- "Description:\t" $$4 "\n" }'
+# Configuration.
-ifdef q
-pkg-search: $(PKG_FILE)
- @cat $(PKG_FILE) | grep -i ${q} | awk 'BEGIN { FS = "\t" }; { print \
- "Name:\t\t" $$1 "\n" \
- "Repository:\t" $$2 "\n" \
- "Website:\t" $$3 "\n" \
- "Description:\t" $$4 "\n" }'
+EDOC_OPTS ?=
+
+# Core targets.
+
+docs:: distclean-edoc build-doc-deps
+ $(gen_verbose) $(ERL) -eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), halt().'
+
+distclean:: distclean-edoc
+
+# Plugin-specific targets.
+
+DOC_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DOC_DEPS))
+
+$(foreach dep,$(DOC_DEPS),$(eval $(call dep_target,$(dep))))
+
+build-doc-deps: $(DOC_DEPS_DIRS)
+ @for dep in $(DOC_DEPS_DIRS) ; do $(MAKE) -C $$dep; done
+
+distclean-edoc:
+ $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info
+
+# Copyright (c) 2014, Juan Facorro <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: elvis distclean-elvis
+
+# Configuration.
+
+ELVIS_CONFIG ?= $(CURDIR)/elvis.config
+
+ELVIS ?= $(CURDIR)/elvis
+export ELVIS
+
+ELVIS_URL ?= https://github.com/inaka/elvis/releases/download/0.2.3/elvis
+ELVIS_CONFIG_URL ?= https://github.com/inaka/elvis/releases/download/0.2.3/elvis.config
+ELVIS_OPTS ?=
+
+# Core targets.
+
+help::
+ @printf "%s\n" "" \
+ "Elvis targets:" \
+ " elvis Run Elvis using the local elvis.config or download the default otherwise"
+
+distclean:: distclean-elvis
+
+# Plugin-specific targets.
+
+$(ELVIS):
+ @$(call core_http_get,$(ELVIS),$(ELVIS_URL))
+ @chmod +x $(ELVIS)
+
+$(ELVIS_CONFIG):
+ @$(call core_http_get,$(ELVIS_CONFIG),$(ELVIS_CONFIG_URL))
+
+elvis: $(ELVIS) $(ELVIS_CONFIG)
+ @$(ELVIS) rock -c $(ELVIS_CONFIG) $(ELVIS_OPTS)
+
+distclean-elvis:
+ $(gen_verbose) rm -rf $(ELVIS)
+
+# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+# Configuration.
+
+DTL_FULL_PATH ?= 0
+
+# Verbosity.
+
+dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F));
+dtl_verbose = $(dtl_verbose_$(V))
+
+# Core targets.
+
+define compile_erlydtl
+ $(dtl_verbose) $(ERL) -pa ebin/ $(DEPS_DIR)/erlydtl/ebin/ -eval ' \
+ Compile = fun(F) -> \
+ S = fun (1) -> re:replace(filename:rootname(string:sub_string(F, 11), ".dtl"), "/", "_", [{return, list}, global]); \
+ (0) -> filename:basename(F, ".dtl") \
+ end, \
+ Module = list_to_atom(string:to_lower(S($(DTL_FULL_PATH))) ++ "_dtl"), \
+ {ok, _} = erlydtl:compile(F, Module, [{out_dir, "ebin/"}, return_errors, {doc_root, "templates"}]) \
+ end, \
+ _ = [Compile(F) || F <- string:tokens("$(1)", " ")], \
+ halt().'
+endef
+
+ifneq ($(wildcard src/),)
+ebin/$(PROJECT).app:: $(shell find templates -type f -name \*.dtl 2>/dev/null)
+ $(if $(strip $?),$(call compile_erlydtl,$?))
+endif
+
+# Copyright (c) 2014 Dave Cottlehuber <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: distclean-escript escript
+
+# Configuration.
+
+ESCRIPT_NAME ?= $(PROJECT)
+ESCRIPT_COMMENT ?= This is an -*- erlang -*- file
+
+ESCRIPT_BEAMS ?= "ebin/*", "deps/*/ebin/*"
+ESCRIPT_SYS_CONFIG ?= "rel/sys.config"
+ESCRIPT_EMU_ARGS ?= -pa . \
+ -sasl errlog_type error \
+ -escript main $(ESCRIPT_NAME)
+ESCRIPT_SHEBANG ?= /usr/bin/env escript
+ESCRIPT_STATIC ?= "deps/*/priv/**", "priv/**"
+
+# Core targets.
+
+distclean:: distclean-escript
+
+help::
+ @printf "%s\n" "" \
+ "Escript targets:" \
+ " escript Build an executable escript archive" \
+
+# Plugin-specific targets.
+
+# Based on https://github.com/synrc/mad/blob/master/src/mad_bundle.erl
+# Copyright (c) 2013 Maxim Sokhatsky, Synrc Research Center
+# Modified MIT License, https://github.com/synrc/mad/blob/master/LICENSE :
+# Software may only be used for the great good and the true happiness of all
+# sentient beings.
+
+define ESCRIPT_RAW
+'Read = fun(F) -> {ok, B} = file:read_file(filename:absname(F)), B end,'\
+'Files = fun(L) -> A = lists:concat([filelib:wildcard(X)||X<- L ]),'\
+' [F || F <- A, not filelib:is_dir(F) ] end,'\
+'Squash = fun(L) -> [{filename:basename(F), Read(F) } || F <- L ] end,'\
+'Zip = fun(A, L) -> {ok,{_,Z}} = zip:create(A, L, [{compress,all},memory]), Z end,'\
+'Ez = fun(Escript) ->'\
+' Static = Files([$(ESCRIPT_STATIC)]),'\
+' Beams = Squash(Files([$(ESCRIPT_BEAMS), $(ESCRIPT_SYS_CONFIG)])),'\
+' Archive = Beams ++ [{ "static.gz", Zip("static.gz", Static)}],'\
+' escript:create(Escript, [ $(ESCRIPT_OPTIONS)'\
+' {archive, Archive, [memory]},'\
+' {shebang, "$(ESCRIPT_SHEBANG)"},'\
+' {comment, "$(ESCRIPT_COMMENT)"},'\
+' {emu_args, " $(ESCRIPT_EMU_ARGS)"}'\
+' ]),'\
+' file:change_mode(Escript, 8#755)'\
+'end,'\
+'Ez("$(ESCRIPT_NAME)"),'\
+'halt().'
+endef
+
+ESCRIPT_COMMAND = $(subst ' ',,$(ESCRIPT_RAW))
+
+escript:: distclean-escript deps app
+ $(gen_verbose) $(ERL) -eval $(ESCRIPT_COMMAND)
+
+distclean-escript:
+ $(gen_verbose) rm -f $(ESCRIPT_NAME)
+
+# Copyright (c) 2014, Enrique Fernandez <[email protected]>
+# Copyright (c) 2015, Loïc Hoguin <[email protected]>
+# This file is contributed to erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: eunit
+
+# Configuration
+
+ifeq ($(strip $(TEST_DIR)),)
+TAGGED_EUNIT_TESTS = {dir,"ebin"}
else
-pkg-search:
- @echo "Usage: make pkg-search q=STRING"
+ifeq ($(wildcard $(TEST_DIR)),)
+TAGGED_EUNIT_TESTS = {dir,"ebin"}
+else
+# All modules in TEST_DIR
+TEST_DIR_MODS = $(notdir $(basename $(shell find $(TEST_DIR) -type f -name *.beam)))
+# All modules in 'ebin'
+EUNIT_EBIN_MODS = $(notdir $(basename $(shell find ebin -type f -name *.beam)))
+# Only those modules in TEST_DIR with no matching module in 'ebin'.
+# This is done to avoid some tests being executed twice.
+EUNIT_MODS = $(filter-out $(patsubst %,%_tests,$(EUNIT_EBIN_MODS)),$(TEST_DIR_MODS))
+TAGGED_EUNIT_TESTS = {dir,"ebin"} $(foreach mod,$(EUNIT_MODS),$(shell echo $(mod) | sed -e 's/\(.*\)/{module,\1}/g'))
+endif
+endif
+
+EUNIT_OPTS ?=
+
+# Utility functions
+
+define str-join
+ $(shell echo '$(strip $(1))' | sed -e "s/ /,/g")
+endef
+
+# Core targets.
+
+tests:: eunit
+
+help::
+ @printf "%s\n" "" \
+ "EUnit targets:" \
+ " eunit Run all the EUnit tests for this project"
+
+# Plugin-specific targets.
+
+EUNIT_RUN = $(ERL) \
+ -pa $(TEST_DIR) $(DEPS_DIR)/*/ebin \
+ -pz ebin \
+ -eval 'case eunit:test([$(call str-join,$(TAGGED_EUNIT_TESTS))], [$(EUNIT_OPTS)]) of ok -> halt(0); error -> halt(1) end.'
+
+eunit: test-build
+ $(gen_verbose) $(EUNIT_RUN)
+
+# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: relx-rel distclean-relx-rel distclean-relx
+
+# Configuration.
+
+RELX_CONFIG ?= $(CURDIR)/relx.config
+
+RELX ?= $(CURDIR)/relx
+export RELX
+
+RELX_URL ?= https://github.com/erlware/relx/releases/download/v1.2.0/relx
+RELX_OPTS ?=
+RELX_OUTPUT_DIR ?= _rel
+
+ifeq ($(firstword $(RELX_OPTS)),-o)
+ RELX_OUTPUT_DIR = $(word 2,$(RELX_OPTS))
+else
+ RELX_OPTS += -o $(RELX_OUTPUT_DIR)
+endif
+
+# Core targets.
+
+ifneq ($(wildcard $(RELX_CONFIG)),)
+rel:: distclean-relx-rel relx-rel
+endif
+
+distclean:: distclean-relx-rel distclean-relx
+
+# Plugin-specific targets.
+
+define relx_fetch
+ $(call core_http_get,$(RELX),$(RELX_URL))
+ chmod +x $(RELX)
+endef
+
+$(RELX):
+ @$(call relx_fetch)
+
+relx-rel: $(RELX)
+ @$(RELX) -c $(RELX_CONFIG) $(RELX_OPTS)
+
+distclean-relx-rel:
+ $(gen_verbose) rm -rf $(RELX_OUTPUT_DIR)
+
+distclean-relx:
+ $(gen_verbose) rm -rf $(RELX)
+
+# Copyright (c) 2014, M Robert Martin <[email protected]>
+# This file is contributed to erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: shell
+
+# Configuration.
+
+SHELL_PATH ?= -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin
+SHELL_OPTS ?=
+
+ALL_SHELL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(SHELL_DEPS))
+
+# Core targets
+
+help::
+ @printf "%s\n" "" \
+ "Shell targets:" \
+ " shell Run an erlang shell with SHELL_OPTS or reasonable default"
+
+# Plugin-specific targets.
+
+$(foreach dep,$(SHELL_DEPS),$(eval $(call dep_target,$(dep))))
+
+build-shell-deps: $(ALL_SHELL_DEPS_DIRS)
+ @for dep in $(ALL_SHELL_DEPS_DIRS) ; do $(MAKE) -C $$dep ; done
+
+shell: build-shell-deps
+ $(gen_verbose) erl $(SHELL_PATH) $(SHELL_OPTS)
+
+# Copyright (c) 2015, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+ifneq ($(wildcard $(DEPS_DIR)/triq),)
+.PHONY: triq
+
+# Targets.
+
+tests:: triq
+
+define triq_run
+$(ERL) -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin \
+ -eval "try $(1) of true -> halt(0); _ -> halt(1) catch error:undef -> io:format(\"Undefined property or module~n\"), halt() end."
+endef
+
+ifdef t
+ifeq (,$(findstring :,$(t)))
+triq: test-build
+ @$(call triq_run,triq:check($(t)))
+else
+triq: test-build
+ @echo Testing $(t)/0
+ @$(call triq_run,triq:check($(t)()))
+endif
+else
+triq: test-build
+ $(eval MODULES := $(shell find ebin -type f -name \*.beam \
+ | sed "s/ebin\//'/;s/\.beam/',/" | sed '$$s/.$$//'))
+ $(gen_verbose) $(call triq_run,[true] =:= lists:usort([triq:check(M) || M <- [$(MODULES)]]))
+endif
endif
diff --git a/guide/connect.md b/guide/connect.md
deleted file mode 100644
index 5655d66..0000000
--- a/guide/connect.md
+++ /dev/null
@@ -1,96 +0,0 @@
-Connection
-==========
-
-This chapter describes how to open, monitor and close
-a connection using the Gun client.
-
-Opening a new connection
-------------------------
-
-Gun is designed with the SPDY and Websocket protocols in mind,
-and as such establishes a permanent connection to the remote
-server. Because of this, the connection must be initiated
-before being able to send any request.
-
-The process that creates the connection is also known as the
-owner of the connection, or the controlling process.. Only
-this process can perform operations on the connection, and
-only this process will receive messages from the connection.
-
-To open a new connection, the `gun:open/{2,3}` function can be used.
-
-``` erlang
-{ok, Pid} = gun:open("twitter.com", 443).
-```
-
-Gun will by default assume that SSL should be used.
-
-The connection is managed by a separate process and is supervised
-by the Gun supervisor directly.
-
-The connection can later be stopped either gracefully or abruptly
-by the client. If an unexpected disconnection occurs, the client
-will retry connecting every few seconds until it succeeds and
-can resume normal operations.
-
-Monitoring the connection process
----------------------------------
-
-The connection is managed by a separate process. Because
-software errors are a reality, it is important to monitor
-this process for failure. Thankfully, due to the asynchronous
-nature of Gun, we only need to create a monitor once when
-the connection is established.
-
-``` erlang
-{ok, Pid} = gun:open("twitter.com", 443).
-MRef = monitor(process, Pid).
-```
-
-There is no need to monitor again after that regardless of
-the number of requests sent or messages received.
-
-You can detect the process failure when receiving messages.
-
-``` erlang
-receive
- {'DOWN', Tag, _, _, Reason} ->
- error_logger:error_msg("Oops!"),
- exit(Reason);
- %% Receive Gun messages here...
-end.
-```
-
-You will probably want to reopen the connection when that
-happens.
-
-Closing the connection abruptly
--------------------------------
-
-The connection can be stopped abruptly at any time by calling
-the `gun:close/1` function.
-
-``` erlang
-gun:close(Pid).
-```
-
-The process is stopped immediately.
-
-Closing the connection gracefully
----------------------------------
-
-The connection can also be stopped gracefully by calling the
-`gun:shutdown/1` function.
-
-``` erlang
-gun:shutdown(Pid).
-```
-
-Gun will refuse any new requests from both the Erlang side and
-the server and will attempt to finish the currently opened
-streams. For example if you performed a GET request just before
-calling `gun:shutdown/1`, you will still receive the response
-before Gun closes the connection.
-
-If you set a monitor beforehand, it will inform you when the
-connection has finally been shutdown.
diff --git a/guide/http.md b/guide/http.md
deleted file mode 100644
index 2b16f1a..0000000
--- a/guide/http.md
+++ /dev/null
@@ -1,227 +0,0 @@
-Using HTTP
-==========
-
-This chapter describes how to use the Gun client for
-communicating with an HTTP or SPDY server.
-
-Streams
--------
-
-Every time a request is initiated, either by the client or the
-server, Gun creates a "stream". The stream controls whether
-the endpoints are still sending any data, and allows you to
-identify incoming messages.
-
-Streams are references in Gun, and are therefore always unique.
-
-Streams can be canceled at any time. This will stop any further
-messages being sent to the controlling process. Depending on
-its capabilities, the server will also be instructed to drop
-the request.
-
-Canceling a stream may result in Gun dropping the connection
-temporarily, to avoid uploading or downloading data that will
-not be used. This situation can only occur with HTTP, as SPDY
-features stream canceling as part of its protocol.
-
-To cancel a stream, the `gun:cancel/2` function can be used.
-
-``` erlang
-gun:cancel(Pid, StreamRef}.
-```
-
-Sending requests
-----------------
-
-Gun provides many convenient functions for performing common
-operations, like GET, POST or DELETE. It also provides a
-general purpose function in case you need other methods.
-
-The availability of these methods on the server can vary
-depending on the software used but also on a per-resource
-basis.
-
-To retrieve a resource, `gun:get/{2,3}` can be used. If you
-don't need the response body, `gun:head/{2,3}` is available.
-As this type of requests can't have a request body, only the
-path and optionally the headers can be specified.
-
-``` erlang
-%% Without headers.
-StreamRef = gun:get(Pid, "/organizations/extend").
-%% With headers.
-StreamRef = gun:get(Pid, "/organizations/extend", [
- {"accept", "application/json"},
- {"user-agent", "revolver/1.0"}]).
-```
-
-To create or update a resource, the functions `gun:patch/{3,4}`,
-`gun:post/{3,4}` and `gun:put/{3,4}` can be used. As this type
-of request is meant to come with a body, headers are not optional,
-because you must specify at least the content-type of the body,
-and if possible also the content-length. The body is however
-optional, because there might not be any at all, or because it
-will be subsequently streamed. If a body is set here it is assumed
-to be the full body.
-
-``` erlang
-%% Without body.
-StreamRef = gun:put(Pid, "/organizations/extend", [
- {"content-length", 23},
- {"content-type", "application/json"}]).
-%% With body.
-StreamRef = gun:put(Pid, "/organizations/extend", [
- {"content-length", 23},
- {"content-type", "application/json"}],
- "{\"msg\": \"Hello world!\"}").
-```
-
-To delete a resource, the `gun:delete/{2,3}` function can be
-used. It works similarly to the GET and HEAD functions.
-
-``` erlang
-%% Without headers.
-StreamRef = gun:delete(Pid, "/organizations/extend").
-%% With headers.
-StreamRef = gun:delete(Pid, "/organizations/extend", [
- {"accept", "application/json"},
- {"user-agent", "revolver/1.0"}]).
-```
-
-To obtain the functionality available for a given resource,
-the `gun:options/{2,3}` can be used. It also works like the
-GET and HEAD functions.
-
-``` erlang
-%% Without headers.
-StreamRef = gun:options(Pid, "/organizations/extend").
-%% With headers.
-StreamRef = gun:options(Pid, "/organizations/extend", [
- {"accept", "application/json"},
- {"user-agent", "revolver/1.0"}]).
-```
-
-You can obtain information about the server as a whole by
-using the special path `"*"`.
-
-``` erlang
-StreamRef = gun:options(Pid, "*").
-```
-
-Streaming data
---------------
-
-When a PATCH, POST or PUT operation is performed, and a
-content-type is specified but no body is given, Gun will
-expect data to be streamed to the connection using the
-`gun:data/4` function.
-
-This function can be called as many times as needed until
-all data is sent. The third argument needs to be `nofin`
-when there is remaining data to be sent, and `fin` for the
-last chunk. The last chunk may be empty if needed.
-
-For example, with an `IoDevice` opened like follow:
-
-``` erlang
-{ok, IoDevice} = file:open(Filepath, [read, binary, raw]).
-```
-
-The following function will stream all data until the end
-of the file:
-
-``` erlang
-sendfile(Pid, StreamRef, IoDevice) ->
- case file:read(IoDevice, 8000) of
- eof ->
- gun:data(Pid, StreamRef, fin, <<>>),
- file:close(IoDevice);
- {ok, Bin} ->
- gun:data(Pid, StreamRef, nofin, Bin),
- sendfile(Pid, StreamRef, IoDevice)
- end.
-```
-
-Receiving responses
--------------------
-
-All data received from the server is sent to the controlling
-process as a message. First a response message is sent, then
-zero or more data messages. If something goes wrong, error
-messages are sent instead.
-
-The response message will inform you whether there will be
-data messages following. If it contains `fin` then no data
-will follow. If it contains `nofin` then one or more data
-messages will arrive.
-
-When using SPDY this value is sent along the frame and simply
-passed on in the message. When using HTTP however Gun must
-guess whether data will follow by looking at the headers
-as documented in the HTTP RFC.
-
-``` erlang
-StreamRef = gun:get(Pid, "/"),
-receive
- {'DOWN', Tag, _, _, Reason} ->
- error_logger:error_msg("Oops!"),
- exit(Reason);
- {gun_response, Pid, StreamRef, fin, Status, Headers} ->
- no_data;
- {gun_response, Pid, StreamRef, nofin, Status, Headers} ->
- receive_data(Pid, StreamRef)
-after 1000 ->
- exit(timeout)
-end.
-```
-
-The `receive_data/2` function could look like this:
-
-``` erlang
-receive_data(Pid, Tag, StreamRef) ->
- receive
- {'DOWN', Tag, _, _, Reason} ->
- {error, incomplete};
- {gun_data, Pid, StreamRef, nofin, Data} ->
- io:format("~s~n", [Data]),
- receive_data(Pid, Tag, StreamRef);
- {gun_data, Pid, StreamRef, fin, Data} ->
- io:format("~s~n", [Data])
- after 1000 ->
- {error, timeout}
- end.
-```
-
-While it may seem verbose, using messages like this has the
-advantage of never locking your process, allowing you to
-easily debug your code. It also allows you to start more than
-one connection and concurrently perform queries on all of them
-at the same time.
-
-You may also use Gun in a synchronous manner by writing your
-own functions that perform a receive like demonstrated above.
-
-Dealing with server-pushed streams
-----------------------------------
-
-When using SPDY the server may decide to push extra resources
-after a request is performed. It will send a `gun_push` message
-which contains two references, one for the pushed stream, and
-one for the request this stream is associated with.
-
-Pushed streams typically feature a body. Replying to a pushed
-stream is forbidden and Gun will send an error message if
-attempted.
-
-Pushed streams can be received like this:
-
-``` erlang
-receive
- {gun_push, Pid, PushedStreamRef, StreamRef,
- Method, Host, Path, Headers} ->
- %% ...
-end
-```
-
-The pushed stream gets a new identifier but you still receive
-the `StreamRef` this stream is associated to.
diff --git a/guide/protocols.md b/guide/protocols.md
deleted file mode 100644
index c3aef6f..0000000
--- a/guide/protocols.md
+++ /dev/null
@@ -1,79 +0,0 @@
-Supported protocols
-===================
-
-This chapter describes the supported protocols and lists
-the calls that are valid for each of them.
-
-HTTP
-----
-
-HTTP is a text request-response protocol. The client
-initiates requests and then waits for the server responses.
-The server has no means of creating requests or pushing
-data to the client.
-
-SPDY
-----
-
-SPDY is a binary protocol based on HTTP, compatible with
-the HTTP semantics, that reduces the complexity of parsing
-requests and responses, compresses the HTTP headers and
-allows the server to push data directly to the client.
-
-Websocket
----------
-
-Websocket is a binary protocol established over HTTP that
-allows asynchronous concurrent communication between the
-client and the server. A Websocket server can push data to
-the client at any time.
-
-Websocket over SPDY is not supported by the Gun client at
-this time.
-
-Operations by protocol
-----------------------
-
-This table lists all Gun operations and whether they are
-compatible with the supported protocols.
-
-| Operation | SPDY | HTTP | Websocket |
-| ---------- | ---- | ---- | --------- |
-| delete | yes | yes | no |
-| get | yes | yes | no |
-| head | yes | yes | no |
-| options | yes | yes | no |
-| patch | yes | yes | no |
-| post | yes | yes | no |
-| put | yes | yes | no |
-| request | yes | yes | no |
-| response | yes | no | no |
-| data | yes | yes | no |
-| cancel | yes | yes | no |
-| ws_upgrade | no | yes | no |
-| ws_send | no | no | yes |
-
-While the `cancel` operation is available to HTTP, its effects
-will only be local, as there is no way to tell the server to
-stop sending data. Gun instead just doesn't forward the messages
-for this stream anymore.
-
-Messages by protocol
---------------------
-
-This table lists all messages that can be received depending
-on the current protocol.
-
-| Message | SPDY | HTTP | Websocket |
-| ------------------------------- | ---- | ---- | --------- |
-| {gun_push, ...} | yes | no | no |
-| {gun_response, ...} | yes | yes | no |
-| {gun_data, ...} | yes | yes | no |
-| {gun_error, _, StreamRef, _} | yes | yes | no |
-| {gun_error, _, _} | yes | yes | yes |
-| {gun_ws_upgrade, _, ok} | no | yes | no |
-| {gun_ws_upgrade, _, error, ...} | no | yes | no |
-| {gun_ws, ...} | no | no | yes |
-
-Do not forget that other messages may still be in the mailbox
-after you upgrade to Websocket.
diff --git a/guide/toc.md b/guide/toc.md
deleted file mode 100644
index 00bd6bd..0000000
--- a/guide/toc.md
+++ /dev/null
@@ -1,11 +0,0 @@
-Gun User Guide
-==============
-
-The Gun User Guide explains in details how the Gun client
-should be used for communicating with Web servers.
-
- * [Introduction](introduction.md)
- * [Connection](connect.md)
- * [Supported protocols](protocols.md)
- * [Using HTTP](http.md)
- * [Using Websocket](websocket.md)
diff --git a/guide/websocket.md b/guide/websocket.md
deleted file mode 100644
index 26b73c2..0000000
--- a/guide/websocket.md
+++ /dev/null
@@ -1,85 +0,0 @@
-Using Websocket
-===============
-
-This chapter describes how to use the Gun client for
-communicating with a Websocket server.
-
-HTTP upgrade
-------------
-
-Websocket is a protocol built on top of HTTP. To use Websocket,
-you must first request for the connection to be upgraded.
-
-Gun allows you to perform Websocket upgrade requests by using
-the `gun:ws_upgrade/{2,3}` function. Gun will fill out all
-necessary headers for performing the Websocket upgrade, but
-you can optionally specify additional headers, for example if
-you would like to setup a custom sub-protocol.
-
-``` erlang
-%% Without headers.
-gun:ws_upgrade(Pid, "/websocket").
-%% With headers.
-gun:ws_upgrade(Pid, "/websocket", [
- {"sec-websocket-protocol", "mychat"}
-]).
-```
-
-The success or failure of this operation will be sent as a
-message.
-
-``` erlang
-receive
- {gun_ws_upgrade, Pid, ok} ->
- upgrade_success(Pid);
- {gun_ws_upgrade, Pid, error, IsFin, Status, Headers} ->
- exit({ws_upgrade_failed, Status, Headers});
- %% More clauses here as needed.
-after 1000 ->
- exit(timeout);
-end.
-```
-
-Sending data
-------------
-
-You can then use the `gun:ws_send/2` function to send one or
-more frames to the server.
-
-``` erlang
-%% Send one text frame.
-gun:ws_send(Pid, {text, "Hello!"}).
-%% Send one text frame, one binary frame and close the connection.
-gun:ws_send(Pid, [
- {text, "Hello!"},
- {binary, SomeBin},
- close
-]).
-```
-
-Note that if you send a close frame, Gun will close the connection
-cleanly and will not attempt to reconnect afterwards, similar to
-calling `gun:shutdown/1`.
-
-Receiving data
---------------
-
-Every time Gun receives a frame from the server a message will be
-sent to the controlling process. This message will always contain
-a single frame.
-
-``` erlang
-receive
- {gun_ws, Pid, Frame} ->
- handle_frame(Pid, Frame);
- {gun_error, Pid, Reason} ->
- error_logger:error_msg("Oops! ~p~n", [Reason]),
- upgrade_again(Pid)
-end.
-```
-
-Gun will automatically send ping messages to the server to keep
-the connection alive, however if the connection dies and Gun has
-to reconnect it will not upgrade to Websocket automatically, you
-need to perform the operation when you receive the `gun_error`
-message.
diff --git a/manual/gun.md b/manual/gun.md
deleted file mode 100644
index 3baa1f7..0000000
--- a/manual/gun.md
+++ /dev/null
@@ -1,340 +0,0 @@
-gun
-===
-
-The `gun` module provides an asynchronous interface for
-connecting and communicating with Web servers over SPDY,
-HTTP or Websocket.
-
-Types
------
-
-### opts() = [{keepalive, pos_integer()}
- | {retry, non_neg_integer()}
- | {retry_timeout, pos_integer()}
- | {type, ssl | tcp | tcp_spdy}].
-
-> Configuration for the connection.
-
-Option descriptions
--------------------
-
-The default value is given next to the option name.
-
- - keepalive (5000)
- - Time between pings in milliseconds.
- - retry (5)
- - Number of times Gun will try to reconnect on failure before giving up.
- - retry_timeout (5000)
- - Time between retries in milliseconds.
- - type (ssl)
- - Whether to use SSL, plain TCP (for HTTP/Websocket) or SPDY over TCP.
-
-Messages
---------
-
-Calling functions from this module may result in the following
-messages being sent.
-
-### {gun_push, ServerPid, StreamRef, AssocToStreamRef,
- Method, Host, Path, Headers}
-
-> Types:
-> * ServerPid = pid()
-> * StreamRef = AssocToStreamRef = reference()
-> * Method = binary()
-> * Host = binary()
-> * Path = binary()
-> * Headers = [{binary(), binary()}]
->
-> A resource pushed alongside an HTTP response.
-
-### {gun_response, ServerPid, StreamRef, IsFin, Status, Headers}
-
-> Types:
-> * ServerPid = pid()
-> * StreamRef = reference()
-> * IsFin = fin | nofin
-> * Status = binary()
-> * Headers = [{binary(), binary()}]
->
-> A response to an HTTP request.
-
-### {gun_data, ServerPid, StreamRef, IsFin, Data}
-
-> Types:
-> * ServerPid = pid()
-> * StreamRef = reference()
-> * IsFin = fin | nofin
-> * Data = binary()
->
-> Data associated with a response or pushed resource.
-
-### {gun_error, ServerPid, StreamRef, Reason}
-
-> Types:
-> * ServerPid = pid()
-> * StreamRef = reference()
-> * Reason = any()
->
-> An error specific to a particular stream.
-
-### {gun_error, ServerPid, Reason}
-
-> Types:
-> * ServerPid = pid()
-> * Reason = any()
->
-> A general error.
-
-### {gun_ws_upgrade, ServerPid, ok}
-
-> Types:
-> * ServerPid = pid()
->
-> Websocket upgrade success.
-
-### {gun_ws_upgrade, ServerPid, error, IsFin, Status, Headers}
-
-> Types:
-> * ServerPid = pid()
-> * IsFin = fin | nofin
-> * Status = binary()
-> * Headers = [{binary(), binary()}]
->
-> Websocket upgrade failure, with the HTTP response received.
-
-### {gun_ws, ServerPid, Frame}
-
-> Types:
-> * ServerPid = pid()
-> * Frame = ws_frame()
->
-> A Websocket frame just received.
-
-Exports
--------
-
-### open(Host, Port) -> open(Host, Port, [])
-### open(Host, Port, Opts) -> {ok, ServerPid} | {error, any()}
-
-> Types:
-> * Host = inet:hostname()
-> * Port = inet:port_number()
-> * Opts = opts()
-> * ServerPid = pid()
->
-> Open a connection to the given host.
-
-### close(ServerPid) -> ok
-
-> Types:
-> * ServerPid = pid()
->
-> Brutally close the connection.
-
-### shutdown(ServerPid) -> ok
-
-> Types:
-> * ServerPid = pid()
->
-> Gracefully close the connection.
->
-> A monitor can be used to be notified when the connection is
-> effectively closed.
-
-### delete(ServerPid, Path) -> delete(ServerPid, Path, [])
-### delete(ServerPid, Path, Headers) -> StreamRef
-
-> Types:
-> * ServerPid = pid()
-> * Path = iodata()
-> * Headers = [{binary(), iodata()}]
-> * StreamRef = reference()
->
-> Delete a resource.
-
-### get(ServerPid, Path) -> delete(ServerPid, Path, [])
-### get(ServerPid, Path, Headers) -> StreamRef
-
-> Types:
-> * ServerPid = pid()
-> * Path = iodata()
-> * Headers = [{binary(), iodata()}]
-> * StreamRef = reference()
->
-> Fetch a resource.
-
-### head(ServerPid, Path) -> delete(ServerPid, Path, [])
-### head(ServerPid, Path, Headers) -> StreamRef
-
-> Types:
-> * ServerPid = pid()
-> * Path = iodata()
-> * Headers = [{binary(), iodata()}]
-> * StreamRef = reference()
->
-> Fetch a resource's headers.
->
-> The server will not send the resource content, only headers.
-
-### options(ServerPid, Path) -> delete(ServerPid, Path, [])
-### options(ServerPid, Path, Headers) -> StreamRef
-
-> Types:
-> * ServerPid = pid()
-> * Path = iodata()
-> * Headers = [{binary(), iodata()}]
-> * StreamRef = reference()
->
-> Obtain information about the capabilities of the server or a resource.
->
-> The special path "*" can be used to obtain information about
-> the server as a whole. Any other path will return information
-> about the resource only.
-
-### patch(ServerPid, Path, Headers) -> StreamRef
-### patch(ServerPid, Path, Headers, Body) -> StreamRef
-
-> Types:
-> * ServerPid = pid()
-> * Path = iodata()
-> * Headers = [{binary(), iodata()}]
-> * StreamRef = reference()
-> * Body = iodata()
->
-> Partially update a resource.
->
-> Always set the content-type header so that Gun and the server
-> can be made aware that a body is going to be sent. Also try
-> to set the content-length header when possible.
->
-> If a body is given, even an empty one, it is expected to be
-> the full resource.
->
-> If not, Gun will assume there is no body if content-type
-> isn't set, and otherwise will expect you to stream the body.
-
-### post(ServerPid, Path, Headers) -> StreamRef
-### post(ServerPid, Path, Headers, Body) -> StreamRef
-
-> Types:
-> * ServerPid = pid()
-> * Path = iodata()
-> * Headers = [{binary(), iodata()}]
-> * StreamRef = reference()
-> * Body = iodata()
->
-> Create or update a resource.
->
-> The resource may be created at a different URL than the one
-> given.
->
-> Always set the content-type header so that Gun and the server
-> can be made aware that a body is going to be sent. Also try
-> to set the content-length header when possible.
->
-> If a body is given, even an empty one, it is expected to be
-> the full resource.
->
-> If not, Gun will assume there is no body if content-type
-> isn't set, and otherwise will expect you to stream the body.
-
-### put(ServerPid, Path, Headers) -> StreamRef
-### put(ServerPid, Path, Headers, Body) -> StreamRef
-
-> Types:
-> * ServerPid = pid()
-> * Path = iodata()
-> * Headers = [{binary(), iodata()}]
-> * StreamRef = reference()
-> * Body = iodata()
->
-> Create or update a resource.
->
-> The resource will be created at this exact URL.
->
-> Always set the content-type header so that Gun and the server
-> can be made aware that a body is going to be sent. Also try
-> to set the content-length header when possible.
->
-> If a body is given, even an empty one, it is expected to be
-> the full resource.
->
-> If not, Gun will assume there is no body if content-type
-> isn't set, and otherwise will expect you to stream the body.
-
-### request(ServerPid, Method, Path, Headers) -> StreamRef
-### request(ServerPid, Method, Path, Headers, Body) -> StreamRef
-
-> Types:
-> * ServerPid = pid()
-> * Method = iodata()
-> * Path = iodata()
-> * Headers = [{binary(), iodata()}]
-> * StreamRef = reference()
-> * Body = iodata()
->
-> Perform the given request on a resource.
->
-> This is a general purpose function that should only be used
-> when no other function can be used.
->
-> Method names are case sensitive.
->
-> Always set the content-type header so that Gun and the server
-> can be made aware that a body is going to be sent. Also try
-> to set the content-length header when possible.
->
-> If a body is given, even an empty one, it is expected to be
-> the full resource.
->
-> If not, Gun will assume there is no body if content-type
-> isn't set, and otherwise will expect you to stream the body.
-
-### data(ServerPid, StreamRef, IsFin, Data) -> ok
-
-> Types:
-> * ServerPid = pid()
-> * StreamRef = reference()
-> * IsFin = fin | nofin
-> * Data = iodata()
->
-> Stream data.
->
-> The `StreamRef` argument is the one returned by any of the
-> request functions beforehand and uniquely identifies a request.
->
-> Use `nofin` for all chunks except the last which should be `fin`.
-> The last chunk may be empty.
-
-### cancel(ServerPid, StreamRef) -> ok
-
-> Types:
-> * ServerPid = pid()
-> * StreamRef = reference()
->
-> Cancel the given stream.
->
-> The `StreamRef` argument is the one returned by any of the
-> request functions beforehand and uniquely identifies a request.
->
-> This function will do a best effort at canceling the stream,
-> depending on the capabilities of the protocol.
-
-### ws_upgrade(ServerPid, Path) -> ws_upgrade(ServerPid, Path, [])
-### ws_upgrade(ServerPid, Path, Headers) -> ok
-
-> Types:
-> * ServerPid = pid()
-> * Path = iodata()
-> * Headers = [{binary(), iodata()}]
->
-> Upgrade the connection to Websocket.
-
-### ws_send(ServerPid, Frames) -> ok
-
-> Types:
-> * ServerPid = pid()
-> * Frames = ws_frame() | [ws_frame()]
->
-> Send a Websocket frame.
diff --git a/manual/gun_app.md b/manual/gun_app.md
deleted file mode 100644
index 448145b..0000000
--- a/manual/gun_app.md
+++ /dev/null
@@ -1,22 +0,0 @@
-The Gun Application
-===================
-
-Asynchronous SPDY, HTTP and Websocket client.
-
-Dependencies
-------------
-
-The `gun` application uses the Erlang applications `ranch`
-for abstracting TCP and SSL over a common interface, and
-the applications `asn1`, `public_key` and `ssl` for using
-the SSL transport. These dependencies must be loaded for
-the `gun` application to work. In an embedded environment
-this means that they need to be started with the
-`application:start/{1,2}` function before the `gun`
-application is started.
-
-Environment
------------
-
-The `gun` application does not define any application
-environment configuration parameters.
diff --git a/manual/toc.md b/manual/toc.md
deleted file mode 100644
index 7765afc..0000000
--- a/manual/toc.md
+++ /dev/null
@@ -1,7 +0,0 @@
-Gun Function Reference
-======================
-
-The function reference documents the public interface of Gun.
-
- * [The Gun Application](gun_app.md)
- * [gun](gun.md)
diff --git a/rebar.config b/rebar.config
index fddb57f..ef5e1f3 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,4 +1,4 @@
{deps, [
- {cowlib, ".*", {git, "git://github.com/extend/cowlib.git", "master"}},
+ {cowlib, ".*", {git, "git://github.com/extend/cowlib.git", "1.3.0"}},
{ranch, ".*", {git, "git://github.com/extend/ranch.git", "master"}}
]}.
diff --git a/src/gun.app.src b/src/gun.app.src
index cb9f2c0..30fd964 100644
--- a/src/gun.app.src
+++ b/src/gun.app.src
@@ -1,4 +1,4 @@
-%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/gun.erl b/src/gun.erl
index 71af26e..b7260bc 100644
--- a/src/gun.erl
+++ b/src/gun.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -58,6 +58,7 @@
%% Websocket.
-export([ws_upgrade/2]).
-export([ws_upgrade/3]).
+-export([ws_upgrade/4]).
-export([ws_send/2]).
%% Debug.
@@ -85,6 +86,8 @@
| {type, conn_type()}].
-export_type([opts/0]).
+-type ws_opts() :: [{compress, boolean()}].
+
-record(state, {
parent :: pid(),
owner :: pid(),
@@ -98,7 +101,7 @@
socket :: inet:socket() | ssl:sslsocket(),
transport :: module(),
protocol :: module(),
- proto_opts :: gun_http:opts(), %% @todo Make a tuple with SPDY and WS too.
+ proto_opts :: gun_http:opts(), %% @todo Make a tuple with SPDY too.
protocol_state :: any()
}).
@@ -244,9 +247,8 @@ await(ServerPid, StreamRef, Timeout, MRef) ->
{response, IsFin, Status, Headers};
{gun_data, ServerPid, StreamRef, IsFin, Data} ->
{data, IsFin, Data};
- {gun_push, ServerPid, StreamRef, AssocToStreamRef,
- Method, Host, Path, Headers} ->
- {push, AssocToStreamRef, Method, Host, Path, Headers};
+ {gun_push, ServerPid, StreamRef, NewStreamRef, Method, Host, Path, Headers} ->
+ {push, NewStreamRef, Method, Host, Path, Headers};
{gun_error, ServerPid, StreamRef, Reason} ->
{error, Reason};
{gun_error, ServerPid, Reason} ->
@@ -338,14 +340,19 @@ cancel(ServerPid, StreamRef) ->
%% Websocket.
--spec ws_upgrade(pid(), iodata()) -> ok.
+-spec ws_upgrade(pid(), iodata()) -> reference().
ws_upgrade(ServerPid, Path) ->
- ws_upgrade(ServerPid, Path, []).
+ ws_upgrade(ServerPid, Path, [], #{}).
--spec ws_upgrade(pid(), iodata(), headers()) -> ok.
+-spec ws_upgrade(pid(), iodata(), headers()) -> reference().
ws_upgrade(ServerPid, Path, Headers) ->
- _ = ServerPid ! {ws_upgrade, self(), Path, Headers},
- ok.
+ ws_upgrade(ServerPid, Path, Headers, #{}).
+
+-spec ws_upgrade(pid(), iodata(), headers(), ws_opts()) -> reference().
+ws_upgrade(ServerPid, Path, Headers, Opts) ->
+ StreamRef = make_ref(),
+ _ = ServerPid ! {ws_upgrade, self(), StreamRef, Path, Headers, Opts},
+ StreamRef.
-spec ws_send(pid(), ws_frame() | [ws_frame()]) -> ok.
ws_send(ServerPid, Frames) ->
@@ -428,8 +435,10 @@ connect(State=#state{owner=Owner, host=Host, port=Port, type=Type,
retry(State, Retries - 1)
end.
-retry(State=#state{keepalive_ref=KeepaliveRef}, Retries) when
- is_reference(KeepaliveRef) ->
+%% Exit normally if the retry functionality has been disabled.
+retry(_, 0) ->
+ ok;
+retry(State=#state{keepalive_ref=KeepaliveRef}, Retries) when is_reference(KeepaliveRef) ->
_ = erlang:cancel_timer(KeepaliveRef),
%% Flush if we have a keepalive message
receive
@@ -458,7 +467,7 @@ before_loop(State=#state{keepalive=Keepalive}) ->
KeepaliveRef = erlang:send_after(Keepalive, self(), keepalive),
loop(State#state{keepalive_ref=KeepaliveRef}).
-loop(State=#state{parent=Parent, owner=Owner, host=Host,
+loop(State=#state{parent=Parent, owner=Owner, host=Host, port=Port,
retry=Retry, socket=Socket, transport=Transport,
protocol=Protocol, protocol_state=ProtoState}) ->
{OK, Closed, Error} = Transport:messages(),
@@ -470,7 +479,9 @@ loop(State=#state{parent=Parent, owner=Owner, host=Host,
Transport:close(Socket),
retry(State#state{socket=undefined, transport=undefined,
protocol=undefined}, Retry);
- ProtoState2 ->
+ {upgrade, Protocol2, ProtoState2} ->
+ ws_loop(State#state{protocol=Protocol2, protocol_state=ProtoState2});
+ ProtoState2 ->
loop(State#state{protocol_state=ProtoState2})
end;
{Closed, Socket} ->
@@ -494,11 +505,11 @@ loop(State=#state{parent=Parent, owner=Owner, host=Host,
before_loop(State#state{protocol_state=ProtoState2});
{request, Owner, StreamRef, Method, Path, Headers} ->
ProtoState2 = Protocol:request(ProtoState,
- StreamRef, Method, Host, Path, Headers),
+ StreamRef, Method, Host, Port, Path, Headers),
loop(State#state{protocol_state=ProtoState2});
{request, Owner, StreamRef, Method, Path, Headers, Body} ->
ProtoState2 = Protocol:request(ProtoState,
- StreamRef, Method, Host, Path, Headers, Body),
+ StreamRef, Method, Host, Port, Path, Headers, Body),
loop(State#state{protocol_state=ProtoState2});
{data, Owner, StreamRef, IsFin, Data} ->
ProtoState2 = Protocol:data(ProtoState,
@@ -507,11 +518,10 @@ loop(State=#state{parent=Parent, owner=Owner, host=Host,
{cancel, Owner, StreamRef} ->
ProtoState2 = Protocol:cancel(ProtoState, StreamRef),
loop(State#state{protocol_state=ProtoState2});
- {ws_upgrade, Owner, Path, Headers} when Protocol =/= gun_spdy ->
- %% @todo
- ProtoState2 = Protocol:ws_upgrade(ProtoState,
- Path, Headers),
- ws_loop(State#state{protocol=gun_ws, protocol_state=ProtoState2});
+ {ws_upgrade, Owner, StreamRef, Path, Headers, Opts} when Protocol =/= gun_spdy ->
+ ProtoState2 = Protocol:ws_upgrade(ProtoState, StreamRef, Host, Port, Path, Headers, Opts),
+ loop(State#state{protocol_state=ProtoState2});
+ %% @todo can fail if http/1.0
{shutdown, Owner} ->
%% @todo Protocol:shutdown?
ok;
@@ -525,9 +535,9 @@ loop(State=#state{parent=Parent, owner=Owner, host=Host,
element(2, Any) ! {gun_error, self(), {notowner,
"Operations are restricted to the owner of the connection."}},
loop(State);
- {ws_upgrade, _, _, _} ->
- Owner ! {gun_error, self(), {badstate,
- "Websocket over SPDY isn't supported."}},
+ {ws_upgrade, _, StreamRef, _, _} ->
+ Owner ! {gun_error, self(), StreamRef, {badstate,
+ "Websocket is only supported over HTTP/1.1."}},
loop(State);
{ws_send, _, _} ->
Owner ! {gun_error, self(), {badstate,
@@ -545,23 +555,34 @@ ws_loop(State=#state{parent=Parent, owner=Owner, retry=Retry, socket=Socket,
ok = Transport:setopts(Socket, [{active, once}]),
receive
{OK, Socket, Data} ->
- ProtoState2 = Protocol:handle(ProtoState, Data),
- ws_loop(State#state{protocol_state=ProtoState2});
+ case Protocol:handle(Data, ProtoState) of
+ close ->
+ Transport:close(Socket),
+ retry(State#state{socket=undefined, transport=undefined, protocol=undefined}, Retry);
+ ProtoState2 ->
+ ws_loop(State#state{protocol_state=ProtoState2})
+ end;
{Closed, Socket} ->
Transport:close(Socket),
- retry(State#state{socket=undefined, transport=undefined,
- protocol=undefined}, Retry);
+ retry(State#state{socket=undefined, transport=undefined, protocol=undefined}, Retry);
{Error, Socket, _} ->
Transport:close(Socket),
- retry(State#state{socket=undefined, transport=undefined,
- protocol=undefined}, Retry);
- %% @todo keepalive
- {ws_send, Owner, Frames} when is_list(Frames) ->
- todo; %% @todo
+ retry(State#state{socket=undefined, transport=undefined, protocol=undefined}, Retry);
+ %% Ignore any previous HTTP keep-alive.
+ keepalive ->
+ ws_loop(State);
+% {ws_send, Owner, Frames} when is_list(Frames) ->
+% todo; %% @todo
{ws_send, Owner, Frame} ->
- {todo, Frame}; %% @todo
+ case Protocol:send(Frame, ProtoState) of
+ close ->
+ Transport:close(Socket),
+ retry(State#state{socket=undefined, transport=undefined, protocol=undefined}, Retry);
+ ProtoState2 ->
+ ws_loop(State#state{protocol_state=ProtoState2})
+ end;
{shutdown, Owner} ->
- %% @todo Protocol:shutdown?
+ %% @todo Protocol:shutdown? %% @todo close frame
ok;
{system, From, Request} ->
sys:handle_system_msg(Request, From, Parent, ?MODULE, [],
diff --git a/src/gun_app.erl b/src/gun_app.erl
index 95def87..61a3ecf 100644
--- a/src/gun_app.erl
+++ b/src/gun_app.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/gun_http.erl b/src/gun_http.erl
index 86fc436..745c2a9 100644
--- a/src/gun_http.erl
+++ b/src/gun_http.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2014, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2014-2015, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -18,16 +18,19 @@
-export([handle/2]).
-export([close/1]).
-export([keepalive/1]).
--export([request/6]).
-export([request/7]).
+-export([request/8]).
-export([data/4]).
-export([cancel/2]).
+-export([ws_upgrade/7]).
-type opts() :: [{version, cow_http:version()}].
-export_type([opts/0]).
-type io() :: head | {body, non_neg_integer()} | body_close | body_chunked.
+-type websocket_info() :: {websocket, reference(), binary(), [], []}. %% key, extensions, protocols
+
-record(http_state, {
owner :: pid(),
socket :: inet:socket() | ssl:sslsocket(),
@@ -35,7 +38,7 @@
version = 'HTTP/1.1' :: cow_http:version(),
connection = keepalive :: keepalive | close,
buffer = <<>> :: binary(),
- streams = [] :: [{reference(), boolean()}], %% ref + whether stream is alive
+ streams = [] :: [{reference() | websocket_info(), boolean()}], %% ref + whether stream is alive
in = head :: io(),
in_state :: {non_neg_integer(), non_neg_integer()},
out = head :: io()
@@ -47,6 +50,9 @@ init(Owner, Socket, Transport, [{version, Version}]) ->
#http_state{owner=Owner, socket=Socket, transport=Transport,
version=Version}.
+%% Stop looping when we got no more data.
+handle(<<>>, State) ->
+ State;
%% Close when server responds and we don't have any open streams.
handle(_, #http_state{streams=[]}) ->
close;
@@ -70,11 +76,11 @@ handle(Data, State=#http_state{in=body_chunked, in_state=InState,
{more, Data2, InState2} ->
send_data_if_alive(Data2, State, nofin),
State#http_state{buffer= <<>>, in_state=InState2};
- {more, Data2, _Length, InState2} ->
+ {more, Data2, Length, InState2} when is_integer(Length) ->
%% @todo See if we can recv faster than one message at a time.
send_data_if_alive(Data2, State, nofin),
State#http_state{buffer= <<>>, in_state=InState2};
- {more, Data2, _Length, Rest, InState2} ->
+ {more, Data2, Rest, InState2} ->
%% @todo See if we can recv faster than one message at a time.
send_data_if_alive(Data2, State, nofin),
State#http_state{buffer=Rest, in_state=InState2};
@@ -125,31 +131,40 @@ handle_head(Data, State=#http_state{owner=Owner, version=ClientVersion,
connection=Conn, streams=[{StreamRef, IsAlive}|_]}) ->
{Version, Status, _, Rest} = cow_http:parse_status_line(Data),
{Headers, Rest2} = cow_http:parse_headers(Rest),
- In = io_from_headers(Version, Headers),
- IsFin = case In of head -> fin; _ -> nofin end,
- case IsAlive of
- false ->
- ok;
- true ->
- Owner ! {gun_response, self(), StreamRef,
- IsFin, Status, Headers},
- ok
- end,
- Conn2 = if
- Conn =:= close -> close;
- Version =:= 'HTTP/1.0' -> close;
- ClientVersion =:= 'HTTP/1.0' -> close;
- true -> conn_from_headers(Headers)
- end,
- %% We always reset in_state even if not chunked.
- if
- IsFin =:= fin, Conn2 =:= close ->
- close;
- IsFin =:= fin ->
- handle(Rest2, end_stream(State#http_state{in=In,
- in_state={0, 0}, connection=Conn2}));
- true ->
- handle(Rest2, State#http_state{in=In, in_state={0, 0}, connection=Conn2})
+ case {Status, StreamRef} of
+ {101, {websocket, _, WsKey, WsExtensions, WsProtocols, WsOpts}} ->
+ ws_handshake(Rest2, State, Headers, WsKey, WsExtensions, WsProtocols, WsOpts);
+ _ ->
+ In = response_io_from_headers(Version, Headers),
+ IsFin = case In of head -> fin; _ -> nofin end,
+ case IsAlive of
+ false ->
+ ok;
+ true ->
+ StreamRef2 = case StreamRef of
+ {websocket, SR, _, _, _} -> SR;
+ _ -> StreamRef
+ end,
+ Owner ! {gun_response, self(), StreamRef2,
+ IsFin, Status, Headers},
+ ok
+ end,
+ Conn2 = if
+ Conn =:= close -> close;
+ Version =:= 'HTTP/1.0' -> close;
+ ClientVersion =:= 'HTTP/1.0' -> close;
+ true -> conn_from_headers(Version, Headers)
+ end,
+ %% We always reset in_state even if not chunked.
+ if
+ IsFin =:= fin, Conn2 =:= close ->
+ close;
+ IsFin =:= fin ->
+ handle(Rest2, end_stream(State#http_state{in=In,
+ in_state={0, 0}, connection=Conn2}));
+ true ->
+ handle(Rest2, State#http_state{in=In, in_state={0, 0}, connection=Conn2})
+ end
end.
send_data_if_alive(<<>>, _, nofin) ->
@@ -177,39 +192,38 @@ close_streams(Owner, [{StreamRef, _}|Tail]) ->
close_streams(Owner, Tail).
%% We can only keep-alive by sending an empty line in-between streams.
-keepalive(State=#http_state{socket=Socket, transport=Transport}) ->
+keepalive(State=#http_state{socket=Socket, transport=Transport, out=head}) ->
Transport:send(Socket, <<"\r\n">>),
+ State;
+keepalive(State) ->
State.
request(State=#http_state{socket=Socket, transport=Transport, version=Version,
- out=head}, StreamRef, Method, Host, Path, Headers) ->
- Headers2 = case Version of
- 'HTTP/1.0' -> lists:keydelete(<<"transfer-encoding">>, 1, Headers);
- 'HTTP/1.1' -> Headers
- end,
+ out=head}, StreamRef, Method, Host, Port, Path, Headers) ->
+ Headers2 = lists:keydelete(<<"transfer-encoding">>, 1, Headers),
Headers3 = case lists:keymember(<<"host">>, 1, Headers) of
- false -> [{<<"host">>, Host}|Headers2];
+ false -> [{<<"host">>, [Host, $:, integer_to_binary(Port)]}|Headers2];
true -> Headers2
end,
%% We use Headers2 because this is the smallest list.
- Conn = conn_from_headers(Headers2),
- Out = io_from_headers(Version, Headers2),
+ Conn = conn_from_headers(Version, Headers2),
+ Out = request_io_from_headers(Headers2),
Transport:send(Socket, cow_http:request(Method, Path, Version, Headers3)),
new_stream(State#http_state{connection=Conn, out=Out}, StreamRef).
request(State=#http_state{socket=Socket, transport=Transport, version=Version,
- out=head}, StreamRef, Method, Host, Path, Headers, Body) ->
+ out=head}, StreamRef, Method, Host, Port, Path, Headers, Body) ->
Headers2 = lists:keydelete(<<"content-length">>, 1,
lists:keydelete(<<"transfer-encoding">>, 1, Headers)),
Headers3 = case lists:keymember(<<"host">>, 1, Headers) of
- false -> [{<<"host">>, Host}|Headers2];
+ false -> [{<<"host">>, [Host, $:, integer_to_binary(Port)]}|Headers2];
true -> Headers2
end,
%% We use Headers2 because this is the smallest list.
- Conn = conn_from_headers(Headers2),
+ Conn = conn_from_headers(Version, Headers2),
Transport:send(Socket, [
cow_http:request(Method, Path, Version, [
- {<<"content-length">>, integer_to_list(iolist_size(Body))}
+ {<<"content-length">>, integer_to_binary(iolist_size(Body))}
|Headers3]),
Body]),
new_stream(State#http_state{connection=Conn}, StreamRef).
@@ -278,8 +292,10 @@ error_stream_not_found(State=#http_state{owner=Owner}) ->
%% Headers information retrieval.
-conn_from_headers(Headers) ->
+conn_from_headers(Version, Headers) ->
case lists:keyfind(<<"connection">>, 1, Headers) of
+ false when Version =:= 'HTTP/1.0' ->
+ close;
false ->
keepalive;
{_, ConnHd} ->
@@ -290,7 +306,20 @@ conn_from_headers(Headers) ->
end
end.
-io_from_headers(Version, Headers) ->
+request_io_from_headers(Headers) ->
+ case lists:keyfind(<<"content-length">>, 1, Headers) of
+ {_, <<"0">>} ->
+ head;
+ {_, Length} ->
+ {body, cow_http_hd:parse_content_length(Length)};
+ _ ->
+ case lists:keymember(<<"content-type">>, 1, Headers) of
+ true -> body_chunked;
+ false -> head
+ end
+ end.
+
+response_io_from_headers(Version, Headers) ->
case lists:keyfind(<<"content-length">>, 1, Headers) of
{_, <<"0">>} ->
head;
@@ -329,3 +358,91 @@ cancel_stream(State=#http_state{streams=Streams}, StreamRef) ->
end_stream(State=#http_state{streams=[_|Tail]}) ->
State#http_state{in=head, streams=Tail}.
+
+%% Websocket upgrade.
+
+%% Ensure version is 1.1.
+ws_upgrade(#http_state{version='HTTP/1.0'}, _, _, _, _, _, _) ->
+ error; %% @todo
+ws_upgrade(State=#http_state{socket=Socket, transport=Transport, out=head},
+ StreamRef, Host, Port, Path, Headers, WsOpts) ->
+ %% @todo Add option for setting protocol.
+ {ExtHeaders, GunExtensions} = case maps:get(compress, WsOpts, false) of
+ true -> {[{<<"sec-websocket-extensions">>, <<"permessage-deflate; client_max_window_bits; server_max_window_bits=15">>}],
+ [<<"permessage-deflate">>]};
+ false -> {[], []}
+ end,
+ Key = cow_ws:key(),
+ Headers2 = [
+ {<<"connection">>, <<"upgrade">>},
+ {<<"upgrade">>, <<"websocket">>},
+ {<<"sec-websocket-version">>, <<"13">>},
+ {<<"sec-websocket-key">>, Key}
+ |ExtHeaders
+ ],
+ IsSecure = Transport:secure(),
+ Headers3 = case lists:keymember(<<"host">>, 1, Headers) of
+ true -> Headers2;
+ false when Port =:= 80, not IsSecure -> [{<<"host">>, Host}|Headers2];
+ false when Port =:= 443, IsSecure -> [{<<"host">>, Host}|Headers2];
+ false -> [{<<"host">>, [Host, $:, integer_to_binary(Port)]}|Headers2]
+ end,
+ Transport:send(Socket, cow_http:request(<<"GET">>, Path, 'HTTP/1.1', Headers3)),
+ new_stream(State#http_state{connection=keepalive, out=head},
+ {websocket, StreamRef, Key, GunExtensions, [], WsOpts}).
+
+ws_handshake(Buffer, State, Headers, Key, GunExtensions, GunProtocols, Opts) ->
+ %% @todo check upgrade, connection
+ case lists:keyfind(<<"sec-websocket-accept">>, 1, Headers) of
+ false ->
+ close;
+ {_, Accept} ->
+ case cow_ws:encode_key(Key) of
+ Accept -> ws_handshake_extensions(Buffer, State, Headers, GunExtensions, GunProtocols, Opts);
+ _ -> close
+ end
+ end.
+
+ws_handshake_extensions(Buffer, State, Headers, GunExtensions, GunProtocols, Opts) ->
+ case lists:keyfind(<<"sec-websocket-extensions">>, 1, Headers) of
+ false ->
+ ws_handshake_protocols(Buffer, State, Headers, #{}, GunProtocols);
+ {_, ExtHd} ->
+ case ws_validate_extensions(cow_http_hd:parse_sec_websocket_extensions(ExtHd), GunExtensions, #{}, Opts) of
+ close -> close;
+ Extensions -> ws_handshake_protocols(Buffer, State, Headers, Extensions, GunProtocols)
+ end
+ end.
+
+ws_validate_extensions([], _, Acc, _) ->
+ Acc;
+ws_validate_extensions([{Name = <<"permessage-deflate">>, Params}|Tail], GunExts, Acc, Opts) ->
+ case lists:member(Name, GunExts) of
+ true ->
+ case cow_ws:validate_permessage_deflate(Params, Acc, Opts) of
+ {ok, Acc2} -> ws_validate_extensions(Tail, GunExts, Acc2, Opts);
+ error -> close
+ end;
+ %% Fail the connection if extension was not requested.
+ false ->
+ close
+ end;
+%% Fail the connection on unknown extension.
+ws_validate_extensions(_, _, _, _) ->
+ close.
+
+%% @todo Validate protocols.
+ws_handshake_protocols(Buffer, State, _Headers, Extensions, _GunProtocols = []) ->
+ Protocols = [],
+ ws_handshake_end(Buffer, State, Extensions, Protocols).
+
+ws_handshake_end(Buffer, #http_state{owner=Owner, socket=Socket, transport=Transport}, Extensions, Protocols) ->
+ %% Send ourselves the remaining buffer, if any.
+ _ = case Buffer of
+ <<>> ->
+ ok;
+ _ ->
+ {OK, _, _} = Transport:messages(),
+ self() ! {OK, Socket, Buffer}
+ end,
+ gun_ws:init(Owner, Socket, Transport, Extensions, Protocols).
diff --git a/src/gun_spdy.erl b/src/gun_spdy.erl
index 5ada67a..7651584 100644
--- a/src/gun_spdy.erl
+++ b/src/gun_spdy.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -18,8 +18,8 @@
-export([handle/2]).
-export([close/1]).
-export([keepalive/1]).
--export([request/6]).
-export([request/7]).
+-export([request/8]).
-export([data/4]).
-export([cancel/2]).
@@ -89,8 +89,8 @@ handle_frame(Rest, State=#spdy_state{owner=Owner,
case get_stream_by_id(AssocToStreamID, State) of
#stream{ref=AssocToStreamRef} ->
StreamRef = make_ref(),
- Owner ! {gun_push, self(), StreamRef,
- AssocToStreamRef, Method, Host, Path, Headers},
+ Owner ! {gun_push, self(), AssocToStreamRef,
+ StreamRef, Method, Host, Path, Headers},
handle_loop(Rest, new_stream(StreamID, StreamRef,
not IsFin, false, Version, State));
false ->
@@ -189,30 +189,44 @@ keepalive(State=#spdy_state{socket=Socket, transport=Transport,
Transport:send(Socket, cow_spdy:ping(PingID)),
State#spdy_state{ping_id=PingID + 2}.
-%% @todo Allow overriding the host when doing requests.
+%% @todo Always https scheme?
request(State=#spdy_state{socket=Socket, transport=Transport, zdef=Zdef,
- stream_id=StreamID}, StreamRef, Method, Host, Path, Headers) ->
- Out = false =/= lists:keyfind(<<"content-type">>, 1, Headers),
+ stream_id=StreamID}, StreamRef, Method, Host, Port, Path, Headers) ->
+ {Host2, Headers2} = prepare_request(Headers, Host, Port),
+ Out = (false =/= lists:keyfind(<<"content-type">>, 1, Headers2))
+ orelse (false =/= lists:keyfind(<<"content-length">>, 1, Headers2)),
Transport:send(Socket, cow_spdy:syn_stream(Zdef,
StreamID, 0, not Out, false, 0,
- Method, <<"https">>, Host, Path, <<"HTTP/1.1">>, Headers)),
+ Method, <<"https">>, Host2, Path, <<"HTTP/1.1">>, Headers2)),
new_stream(StreamID, StreamRef, true, Out, <<"HTTP/1.1">>,
State#spdy_state{stream_id=StreamID + 2}).
%% @todo Handle Body > 16MB. (split it out into many frames)
+%% @todo Always https scheme?
request(State=#spdy_state{socket=Socket, transport=Transport, zdef=Zdef,
- stream_id=StreamID}, StreamRef, Method, Host, Path, Headers, Body) ->
- Headers2 = lists:keystore(<<"content-length">>, 1, Headers,
- {<<"content-length">>, integer_to_list(iolist_size(Body))}),
+ stream_id=StreamID}, StreamRef, Method, Host, Port, Path, Headers, Body) ->
+ {Host2, Headers2} = prepare_request(Headers, Host, Port),
+ Headers3 = lists:keystore(<<"content-length">>, 1, Headers2,
+ {<<"content-length">>, integer_to_binary(iolist_size(Body))}),
Transport:send(Socket, [
cow_spdy:syn_stream(Zdef,
StreamID, 0, false, false, 0,
- Method, <<"https">>, Host, Path, <<"HTTP/1.1">>, Headers2),
+ Method, <<"https">>, Host2, Path, <<"HTTP/1.1">>, Headers3),
cow_spdy:data(StreamID, true, Body)
]),
new_stream(StreamID, StreamRef, true, false, <<"HTTP/1.1">>,
State#spdy_state{stream_id=StreamID + 2}).
+prepare_request(Headers, Host, Port) ->
+ Headers2 = lists:keydelete(<<"keep-alive">>, 1,
+ lists:keydelete(<<"proxy-connection">>, 1,
+ lists:keydelete(<<"transfer-encoding">>, 1,
+ lists:keydelete(<<"connection">>, 1, Headers)))),
+ case lists:keytake(<<"host">>, 1, Headers2) of
+ false -> {[Host, $:, integer_to_binary(Port)], Headers2};
+ {value, {_, Host1}, Headers3} -> {Host1, Headers3}
+ end.
+
data(State=#spdy_state{socket=Socket, transport=Transport},
StreamRef, IsFin, Data) ->
case get_stream_by_ref(StreamRef, State) of
diff --git a/src/gun_sup.erl b/src/gun_sup.erl
index 8c0455f..c1b34fa 100644
--- a/src/gun_sup.erl
+++ b/src/gun_sup.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/gun_ws.erl b/src/gun_ws.erl
new file mode 100644
index 0000000..5379362
--- /dev/null
+++ b/src/gun_ws.erl
@@ -0,0 +1,125 @@
+%% Copyright (c) 2015, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(gun_ws).
+
+-export([init/5]).
+-export([handle/2]).
+-export([send/2]).
+
+-record(payload, {
+ type = undefined :: cow_ws:frame_type(),
+ rsv = undefined :: cow_ws:rsv(),
+ len = undefined :: non_neg_integer(),
+ mask_key = undefined :: cow_ws:mask_key(),
+ close_code = undefined :: undefined | cow_ws:close_code(),
+ unmasked = <<>> :: binary(),
+ unmasked_len = 0 :: non_neg_integer()
+}).
+
+-record(ws_state, {
+ owner :: pid(),
+ socket :: inet:socket() | ssl:sslsocket(),
+ transport :: module(),
+ buffer = <<>> :: binary(),
+ in = head :: head | #payload{} | close,
+ frag_state = undefined :: cow_ws:frag_state(),
+ frag_buffer = <<>> :: binary(),
+ utf8_state = 0 :: cow_ws:utf8_state(),
+ extensions = #{} :: cow_ws:extensions()
+}).
+
+%% @todo Protocols
+init(Owner, Socket, Transport, Extensions, _Protocols) ->
+ Owner ! {gun_ws_upgrade, self(), ok},
+ {upgrade, ?MODULE, #ws_state{owner=Owner, socket=Socket, transport=Transport, extensions=Extensions}}.
+
+%% Do not handle anything if we received a close frame.
+handle(_, State=#ws_state{in=close}) ->
+ State;
+%% Shortcut for common case when Data is empty after processing a frame.
+handle(<<>>, State=#ws_state{in=head}) ->
+ State;
+handle(Data, State=#ws_state{buffer=Buffer, in=head, frag_state=FragState, extensions=Extensions}) ->
+ Data2 = << Buffer/binary, Data/binary >>,
+ case cow_ws:parse_header(Data2, Extensions, FragState) of
+ {Type, FragState2, Rsv, Len, MaskKey, Rest} ->
+ handle(Rest, State#ws_state{buffer= <<>>,
+ in=#payload{type=Type, rsv=Rsv, len=Len, mask_key=MaskKey}, frag_state=FragState2});
+ more ->
+ State#ws_state{buffer=Data2};
+ error ->
+ close({error, badframe}, State)
+ end;
+handle(Data, State=#ws_state{in=In=#payload{type=Type, rsv=Rsv, len=Len, mask_key=MaskKey, close_code=CloseCode,
+ unmasked=Unmasked, unmasked_len=UnmaskedLen}, frag_state=FragState, utf8_state=Utf8State, extensions=Extensions}) ->
+ case cow_ws:parse_payload(Data, MaskKey, Utf8State, UnmaskedLen, Type, Len, FragState, Extensions, Rsv) of
+ {ok, CloseCode2, Payload, Utf8State2, Rest} ->
+ dispatch(Rest, State#ws_state{in=head, utf8_state=Utf8State2}, Type, << Unmasked/binary, Payload/binary >>, CloseCode2);
+ {ok, Payload, Utf8State2, Rest} ->
+ dispatch(Rest, State#ws_state{in=head, utf8_state=Utf8State2}, Type, << Unmasked/binary, Payload/binary >>, CloseCode);
+ {more, CloseCode2, Payload, Utf8State2} ->
+ State#ws_state{in=In#payload{close_code=CloseCode2, unmasked= << Unmasked/binary, Payload/binary >>,
+ len=Len - byte_size(Data), unmasked_len=2 + byte_size(Data)}, utf8_state=Utf8State2};
+ {more, Payload, Utf8State2} ->
+ State#ws_state{in=In#payload{unmasked= << Unmasked/binary, Payload/binary >>,
+ len=Len - byte_size(Data), unmasked_len=UnmaskedLen + byte_size(Data)}, utf8_state=Utf8State2};
+ Error = {error, _Reason} ->
+ close(Error, State)
+ end.
+
+dispatch(Rest, State=#ws_state{owner=Owner, frag_state=FragState, frag_buffer=SoFar},
+ Type0, Payload0, CloseCode0) ->
+ case cow_ws:make_frame(Type0, Payload0, CloseCode0, FragState) of
+ {fragment, nofin, _, Payload} ->
+ handle(Rest, State#ws_state{frag_buffer= << SoFar/binary, Payload/binary >>});
+ {fragment, fin, Type, Payload} ->
+ Owner ! {gun_ws, self(), {Type, << SoFar/binary, Payload/binary >>}},
+ handle(Rest, State#ws_state{frag_state=undefined, frag_buffer= <<>>});
+ ping ->
+ State2 = send(pong, State),
+ handle(Rest, State2);
+ {ping, Payload} ->
+ State2 = send({pong, Payload}, State),
+ handle(Rest, State2);
+ pong ->
+ handle(Rest, State);
+ {pong, _} ->
+ handle(Rest, State);
+ Frame ->
+ Owner ! {gun_ws, self(), Frame},
+ case Frame of
+ close -> handle(Rest, State#ws_state{in=close});
+ {close, _, _} -> handle(Rest, State#ws_state{in=close});
+ _ -> handle(Rest, State)
+ end
+ end.
+
+close(Reason, State) ->
+ case Reason of
+ Normal when Normal =:= stop; Normal =:= timeout ->
+ send({close, 1000, <<>>}, State);
+ {error, badframe} ->
+ send({close, 1002, <<>>}, State);
+ {error, badencoding} ->
+ send({close, 1007, <<>>}, State)
+ end.
+
+send(Frame, State=#ws_state{socket=Socket, transport=Transport, extensions=Extensions}) ->
+ Transport:send(Socket, cow_ws:masked_frame(Frame, Extensions)),
+ case Frame of
+ close -> close;
+ {close, _, _} -> close;
+ _ -> State
+ end.
diff --git a/test/gun_ct_hook.erl b/test/gun_ct_hook.erl
new file mode 100644
index 0000000..72db623
--- /dev/null
+++ b/test/gun_ct_hook.erl
@@ -0,0 +1,21 @@
+%% Copyright (c) 2015, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(gun_ct_hook).
+
+-export([init/2]).
+
+init(_, _) ->
+ ct_helper:start([gun]),
+ {ok, undefined}.
diff --git a/test/twitter_SUITE.erl b/test/twitter_SUITE.erl
index 17086e6..8873f02 100644
--- a/test/twitter_SUITE.erl
+++ b/test/twitter_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -13,42 +13,11 @@
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(twitter_SUITE).
-
--include_lib("common_test/include/ct.hrl").
-
-%% ct.
--export([all/0]).
--export([init_per_suite/1]).
--export([end_per_suite/1]).
-
-%% Tests.
--export([spdy/1]).
-
-%% ct.
+-compile(export_all).
all() ->
[spdy].
-init_per_suite(Config) ->
- ok = application:start(ranch),
- ok = application:start(crypto),
- ok = application:start(cowlib),
- ok = application:start(asn1),
- ok = application:start(public_key),
- ok = application:start(ssl),
- ok = application:start(gun),
- Config.
-
-end_per_suite(_) ->
- ok = application:stop(gun),
- ok = application:stop(ssl),
- ok = application:stop(public_key),
- ok = application:stop(asn1),
- ok = application:stop(cowlib),
- ok = application:stop(crypto),
- ok = application:stop(ranch),
- ok.
-
spdy(_) ->
{ok, Pid} = gun:open("twitter.com", 443),
Ref = gun:get(Pid, "/"),
@@ -66,6 +35,7 @@ data_loop(Pid, Ref) ->
ct:print("data ~p", [Data]),
data_loop(Pid, Ref);
{gun_data, Pid, Ref, fin, Data} ->
+ gun:close(Pid),
ct:print("data ~p~nend", [Data])
after 5000 ->
error(timeout)
diff --git a/test/ws_SUITE.erl b/test/ws_SUITE.erl
new file mode 100644
index 0000000..82f3632
--- /dev/null
+++ b/test/ws_SUITE.erl
@@ -0,0 +1,176 @@
+%% Copyright (c) 2015, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(ws_SUITE).
+-compile(export_all).
+
+-import(ct_helper, [config/2]).
+
+%% ct.
+
+all() ->
+ [{group, autobahn}].
+
+groups() ->
+ [{autobahn, [], [autobahn_fuzzingserver]}].
+
+init_per_group(autobahn, Config) ->
+ %% Some systems have it named pip2.
+ Out = os:cmd("pip show autobahntestsuite ; pip2 show autobahntestsuite"),
+ case string:str(Out, "autobahntestsuite") of
+ 0 ->
+ ct:print("Skipping the autobahn group because the "
+ "Autobahn Test Suite is not installed.~nTo install it, "
+ "please follow the instructions on this page:~n~n "
+ "http://autobahn.ws/testsuite/installation.html"),
+ {skip, "Autobahn Test Suite not installed."};
+ _ ->
+ Config
+ end.
+
+end_per_group(_, _) ->
+ oK.
+
+%% Tests.
+
+autobahn_fuzzingserver(Config) ->
+ Self = self(),
+ spawn_link(fun() -> start_port(Config, Self) end),
+ receive autobahn_ready -> ok end,
+ N = get_case_count(),
+ run_cases(0, N),
+ Report = config(priv_dir, Config) ++ "reports/clients/index.html",
+ ct:log("<h2><a href=\"~s\">Full report</a></h2>~n", [Report]),
+ ct:print("Autobahn Test Suite report: file://~s~n", [Report]),
+ log_output(),
+ terminate(),
+ {ok, HTML} = file:read_file(Report),
+ case length(binary:matches(HTML, <<"case_failed">>)) > 2 of
+ true -> error(failed);
+ false -> ok
+ end.
+
+start_port(Config, Pid) ->
+ Port = open_port({spawn, "wstest -m fuzzingserver -s " ++ config(data_dir, Config) ++ "server.json"},
+ [{line, 10000}, {cd, config(priv_dir, Config)}, binary]),
+ receive_preamble(Port, Pid),
+ receive_infinity(Port).
+
+receive_preamble(Port, Pid) ->
+ receive
+ {Port, {data, {eol, Line = <<"Ok, will run", _/bits>>}}} ->
+ Pid ! autobahn_ready,
+ io:format(user, "~s~n", [Line]);
+ {Port, {data, {eol, Line}}} ->
+ io:format(user, "~s~n", [Line]),
+ receive_preamble(Port, Pid)
+ after 5000 ->
+ terminate(),
+ error(timeout)
+ end.
+
+receive_infinity(Port) ->
+ receive
+ {Port, {data, {eol, <<"Updating reports", _/bits>>}}} ->
+ receive_infinity(Port);
+ {Port, {data, {eol, Line}}} ->
+ io:format(user, "~s~n", [Line]),
+ receive_infinity(Port)
+ end.
+
+get_case_count() ->
+ {Pid, Ref} = connect("/getCaseCount"),
+ receive
+ {gun_ws, Pid, {text, N}} ->
+ close(Pid, Ref),
+ binary_to_integer(N);
+ Msg ->
+ ct:print("Unexpected message ~p", [Msg]),
+ terminate(),
+ error(failed)
+ end.
+
+run_cases(Total, Total) ->
+ ok;
+run_cases(N, Total) ->
+ {Pid, Ref} = connect(["/runCase?case=", integer_to_binary(N + 1), "&agent=Gun"]),
+ loop(Pid, Ref),
+ update_reports(),
+ run_cases(N + 1, Total).
+
+loop(Pid, Ref) ->
+ receive
+ {gun_ws, Pid, close} ->
+ gun:ws_send(Pid, close),
+ loop(Pid, Ref);
+ {gun_ws, Pid, {close, Code, _}} ->
+ gun:ws_send(Pid, {close, Code, <<>>}),
+ loop(Pid, Ref);
+ {gun_ws, Pid, Frame} ->
+ gun:ws_send(Pid, Frame),
+ loop(Pid, Ref);
+ {'DOWN', Ref, process, Pid, normal} ->
+ close(Pid, Ref);
+ Msg ->
+ ct:print("Unexpected message ~p", [Msg]),
+ close(Pid, Ref)
+ end.
+
+update_reports() ->
+ {Pid, Ref} = connect("/updateReports?agent=Gun"),
+ receive
+ {gun_ws, Pid, close} ->
+ close(Pid, Ref)
+ after 5000 ->
+ error(failed)
+ end.
+
+log_output() ->
+ ok.
+
+connect(Path) ->
+ {ok, Pid} = gun:open("127.0.0.1", 33080, [{type, tcp}, {retry, 0}]),
+ Ref = monitor(process, Pid),
+ gun:ws_upgrade(Pid, Path, [], #{compress => true}),
+ receive
+ {gun_ws_upgrade, Pid, ok} ->
+ ok;
+ Msg ->
+ ct:print("Unexpected message ~p", [Msg]),
+ terminate(),
+ error(failed)
+ end,
+ {Pid, Ref}.
+
+close(Pid, Ref) ->
+ demonitor(Ref),
+ gun:close(Pid),
+ flush(Pid).
+
+flush(Pid) ->
+ receive
+ {gun_ws, Pid, _} ->
+ flush(Pid);
+ {gun_ws_upgrade, Pid, _} ->
+ flush(Pid);
+ {'DOWN', _, process, Pid, _} ->
+ flush(Pid)
+ after 0 ->
+ ok
+ end.
+
+terminate() ->
+ Res = os:cmd("killall wstest"),
+ io:format(user, "~s", [Res]),
+ ok.
diff --git a/test/ws_SUITE_data/server.json b/test/ws_SUITE_data/server.json
new file mode 100644
index 0000000..902b9b3
--- /dev/null
+++ b/test/ws_SUITE_data/server.json
@@ -0,0 +1,7 @@
+{
+ "url": "ws://localhost:33080",
+
+ "cases": ["*"],
+ "exclude-cases": [],
+ "exclude-agent-cases": {}
+}