Skip to content

Commit

Permalink
Add basic send and more req parsed data
Browse files Browse the repository at this point in the history
  • Loading branch information
José Valim committed Nov 15, 2013
1 parent 1ddab8a commit cb835fb
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 19 deletions.
13 changes: 13 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright (c) 2013 Plataformatec.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
# Plug

** TODO: Add description **
Plug is:

1. An specification for composable modules in between web applications
2. A connection specification and adapters for different web servers in the Erlang VM

## Connection

A connection is represented by the `Plug.Conn` record:

```elixir
Plug.Conn[
host: "www.example.com",
path_info: ["bar", "baz"],
script_name: ["foo"],
assigns: [],
...
]
```

`Plug.Conn` is a record so it can be extended with protocols. Most of the data can be read directly from the record, which is useful for pattern matching. Whenever you want to manipulate the connection, you must use the functions defined in `Plug.Connection`.

As everything else in Elixir, **`Plug.Conn` is immutable**, so every manipulation returns a new copy of the connection:

```elixir
conn = assign(conn, :key, value)
conn = send(conn, 200, "OK!")
conn
```

Note the connection is a **direct interface to the underlying web server**. When you call `send/3` above, it will immediately send the given status and body back to the client. Furthermore, **parsing the request information is lazy**. For example, if you want to access the request headers, they need to be explicitly fetched before hand:

```elixir
conn = fetch(conn, :req_headers)
conn.req_headers["content-type"]
```

# License

Plug source code is released under Apache 2 License.
Check LICENSE file for more information.
19 changes: 17 additions & 2 deletions lib/plug/adapters/cowboy/connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,30 @@ defmodule Plug.Adapters.Cowboy.Connection do
@behaviour Plug.Connection.Spec
@moduledoc false

def build(req, _transport) do
def build(req, transport) do
{ path, req } = :cowboy_req.path req
{ host, req } = :cowboy_req.host req
{ port, req } = :cowboy_req.port req
{ meth, req } = :cowboy_req.method req

Plug.Conn[
adapter: { __MODULE__, req },
path_info: split_path(path)
host: host,
port: port,
method: meth,
scheme: scheme(transport),
path_info: split_path(path)
]
end

def send(req, status, headers, body) do
{ :ok, req } = :cowboy_req.reply(status, headers, body, req)
req
end

defp scheme(:tcp), do: :http
defp scheme(:ssl), do: :https

defp split_path(path) do
segments = :binary.split(path, "/", [:global])
lc segment inlist segments, segment != "", do: segment
Expand Down
37 changes: 31 additions & 6 deletions lib/plug/connection.ex
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
defrecord Plug.Conn, assigns: [], path_info: [], script_name: [], adapter: nil do
defrecord Plug.Conn,
assigns: [], path_info: [], script_name: [], adapter: nil,
host: nil, scheme: nil, port: nil, method: nil do

@type assigns :: Keyword.t
@type segments :: [binary]
@type adapter :: { module, term }
@type host :: binary
@type scheme :: :http | :https
@type port :: 0..65535
@type status :: non_neg_integer
@type headers :: [{ binary, binary }]
@type body :: binary
@type method :: binary

record_type assigns: assigns,
path_info: segments,
script_name: segments,
adapter: adapter
record_type assigns: assigns, path_info: segments, script_name: segments,
adapter: adapter, host: host, scheme: scheme, port: port,
method: method

@moduledoc """
The connection record.
Expand All @@ -22,10 +31,14 @@ defrecord Plug.Conn, assigns: [], path_info: [], script_name: [], adapter: nil d
* `assigns` - store user data that is shared in the application code
* `path_info` - path info information split into segments
* `script_name` - script name information split into segments
* `host` - the requested host
* `port` - the requested port
* `scheme` - the request scheme
* `method` - the request method
## Private fields
Those fields are reserved for lbiraries/framework usage.
Those fields are reserved for libraries/framework usage.
* `adapter` - holds the adapter information in a tuple
"""
Expand All @@ -46,10 +59,22 @@ defmodule Plug.Connection do
iex> conn.assigns[:hello]
nil
iex> conn = assign(conn, :hello, :world)
iex> conn.assigns[:hello]
:world
"""
@spec assign(Conn.t, atom, term) :: Conn.t
def assign(Conn[assigns: assigns] = conn, key, value) when is_atom(key) do
conn.assigns(Keyword.put(assigns, key, value))
end

@doc """
Sends to the client the given status and body.
"""
@spec send(Conn.t, Conn.status, Conn.body) :: Conn.t
def send(Conn[adapter: { adapter, payload }] = conn, status, body) when
is_integer(status) and is_binary(body) do
payload = adapter.send(payload, status, [], body)
conn.adapter({ adapter, payload })
end
end
9 changes: 8 additions & 1 deletion lib/plug/connection/spec.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
defmodule Plug.Connection.Spec do
use Behaviour

defcallback build(term, term) :: term
alias Plug.Conn
@typep payload :: term

@doc """
Sends the given status, headers and body as a response
back to the client.
"""
defcallback send(payload, Conn.status, Conn.headers, Conn.body) :: payload
end
33 changes: 27 additions & 6 deletions test/plug/adapters/cowboy/connection_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
defmodule Plug.Adapters.Cowboy.ConnectionTest do
use ExUnit.Case, async: true

alias Plug.Conn
import Plug.Connection

## Cowboy setup for testing

setup_all do
Expand All @@ -18,32 +21,50 @@ defmodule Plug.Adapters.Cowboy.ConnectionTest do
def plug(conn, []) do
function = binary_to_atom Enum.first(conn.path_info) || "root"
apply __MODULE__, function, [conn]
# rescue
# exception ->
# conn.send(500, exception.message <> "\n" <> Exception.format_stacktrace)
rescue
exception ->
send(conn, 500, exception.message <> "\n" <> Exception.format_stacktrace)
end

## Tests

def root(Plug.Conn[] = conn) do
def root(Conn[] = conn) do
assert conn.method == "HEAD"
assert conn.path_info == []
assert conn.script_name == []
conn
end

def build(Plug.Conn[] = conn) do
def build(Conn[] = conn) do
assert { Plug.Adapters.Cowboy.Connection, _ } = conn.adapter
assert conn.path_info == ["build", "foo", "bar"]
assert conn.script_name == []
assert conn.scheme == :http
assert conn.host == "127.0.0.1"
assert conn.port == 8001
assert conn.method == "GET"
conn
end

test "builds a connection" do
assert_ok request :get, "/"
assert_ok request :head, "/"
assert_ok request :get, "/build/foo/bar"
assert_ok request :get, "//build//foo//bar"
end

def send_200(conn) do
send(conn, 200, "OK")
end

def send_500(conn) do
send(conn, 500, "ERROR")
end

test "sends a response" do
assert { 200, _, "OK" } = request :get, "/send_200"
assert { 500, _, "ERROR" } = request :get, "/send_500"
end

## Helpers

defp assert_ok({ 204, _, _ }), do: :ok
Expand Down
3 changes: 0 additions & 3 deletions test/support/spec_test.ex

This file was deleted.

0 comments on commit cb835fb

Please sign in to comment.