Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce separate behaviour for streaming and simple cases #143

Merged
merged 7 commits into from Oct 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 14 additions & 1 deletion CHANGELOG.md
Expand Up @@ -4,14 +4,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Next
## [0.17.0](https://github.com/CrowdHailer/raxx/tree/0.17.0) - 2018-10-28

### Added

- `Raxx.SimpleServer` behaviour for servers that only need a `handle_request/2` callback.
using `Raxx.SimpleServer` automatically implements `Raxx.Server` so the module can be used in a service.

### Changed

- `use Raxx.Server` issues a warning if the module implements `handle_request/2`,
it is expected that such servers will make use of the new `Raxx.SimpleServer`.
- `Raxx.set_header/2` raises an `ArgumentError` when setting host headers.
- `ArgumentError` is raised instead of `RuntimeError` in cases of bad headers and body content.
- `Raxx.set_body/2` raises an `ArgumentError` for GET and HEAD requests.

### Removed

- `Raxx.is_application?`, use `Raxx.Server.verify_server` instead.
- `Raxx.verify_application`, use `Raxx.Server.verify_server` instead.
- `Raxx.Server.is_implemented?`, use `Raxx.Server.verify_server` instead.

## [0.16.1](https://github.com/CrowdHailer/raxx/tree/0.16.1) - 2018-09-19

### Fixed
Expand Down
110 changes: 51 additions & 59 deletions README.md
Expand Up @@ -11,12 +11,10 @@
- [Documentation available on hexdoc](https://hexdocs.pm/raxx)
- [Discuss on slack](https://elixir-lang.slack.com/messages/C56H3TBH8/)

See [Raxx.Kit](https://github.com/CrowdHailer/raxx_kit) for a project generator that helps you set up
See [Raxx.Kit](https://github.com/CrowdHailer/raxx_kit) for a project generator that helps you set up
a web project based on [Raxx](https://github.com/CrowdHailer/raxx)/[Ace](https://github.com/CrowdHailer/Ace).

## Simple Client

A very simple HTTP/1.1 client.
## Simple client

```elixir
request = Raxx.request(:GET, "http://example.com")
Expand All @@ -27,68 +25,72 @@ request = Raxx.request(:GET, "http://example.com")

See `Raxx.SimpleClient` for full documentation.

## Server Extensions

This project includes:

- `Raxx.Router`
- `Raxx.Logger`
- `Raxx.BasicAuth`
- `Raxx.Session.SignedCookie`
- `Raxx.RequestID`

Additional utilities that can be used in Raxx applications.

- [Raxx.MethodOverride](https://github.com/CrowdHailer/raxx_method_override)
- [Raxx.Static](https://github.com/CrowdHailer/raxx_static)
- [Raxx.ApiBlueprint](https://github.com/CrowdHailer/raxx_api_blueprint)

## Getting started
## Simple server

HTTP is an exchange where a client sends a request to a server and expects a response.
At its simplest this can be viewed as follows

```txt
Simple client server exchange.

request -->
Client ============================================ Server
<-- response
```

#### Simple server

The `HomePage` server implements the simplest HTTP message exchange.
The complete response is constructed from the request.
#### 1. Defining a server

```elixir
defmodule HomePage do
use Raxx.Server
defmodule MyServer do
use Raxx.SimpleServer

@impl Raxx.Server
@impl Raxx.SimpleServer
def handle_request(%{method: :GET, path: []}, _state) do
response(:ok)
|> set_header("content-type", "text/plain")
|> set_body("Hello, World!")
end

def handle_request(%{method: :GET, path: _}, _state) do
response(:not_found)
|> set_header("content-type", "text/plain")
|> set_body("Oops! Nothing here.")
end
end
```
- *A request's path is split into segments, so the root "/" becomes `[]`.*

#### Running a server
- *A request's path is split into segments.
A request to `GET /` has path `[]`.*

#### 2. Running a server

To start a web service a Raxx compatable server is needed.
For example [Ace](https://github.com/crowdhailer/ace).
To start a Raxx server a compatible HTTP server is needed.
This example uses [Ace](https://github.com/crowdhailer/ace) that can serve both HTTP/1 and HTTP/2.

```elixir
server = HomePage
initial_state = %{}
options = [port: 8080, cleartext: true]
raxx_server = {MyServer, nil}
http_options = [port: 8080, cleartext: true]

{:ok, pid} = Ace.HTTP.Service.start_link({server, initial_state}, options)
{:ok, pid} = Ace.HTTP.Service.start_link(raxx_server, http_options)
```

Visit [http://localhost:8080](http://localhost:8080).
- *The second element in the Raxx server tuple is passed as the second argument to the `handle_request/2` callback.
In this example it is unused and so set to nil.*

Start your project and visit [http://localhost:8080](http://localhost:8080).

## HTTP streaming

An HTTP exchange involves a client sending data to a server receiving a response.
A simple view is to model this as a single message sent in each direction.
*Working with this model corresponds to `Raxx.SimpleServer` callbacks.*

```txt
request -->
Client ============================================ Server
<-- response
```

When the simple model is insufficient Raxx exposes a lower model.
This consists of a series of messages in each direction.
*Working with this model corresponds to `Raxx.Server` callbacks.*

```txt
tail | data(1+) | head(request) -->
Client ============================================ Server
<-- head(response) | data(1+) | tail
```

- *The body of a request or a response, is the combination of all data parts sent.*

#### Stateful server

Expand All @@ -100,7 +102,7 @@ defmodule LongPoll do
use Raxx.Server

@impl Raxx.Server
def handle_request(%{method: :GET, path: ["slow"]}, state) do
def handle_head(%{method: :GET, path: ["slow"]}, state) do
Process.send_after(self(), :reply, 30_000)

{[], state}
Expand All @@ -118,19 +120,9 @@ end
and the new state of the server, in this case no change `state`.*
- *The `initial_state` is configured when the server is started.*

#### Streaming

Any client server exchange is actually a stream of information in either direction.
`Raxx.Server` provides callbacks to proccess parts of a stream as they are received.

```txt
Detailed view of client server exchange.

tail | data(1+) | head(request) -->
Client ============================================ Server
<-- head(response) | data(1+) | tail
```
- *The body of a request or a response, is the combination of all data parts sent.*

#### Server streaming

Expand Down
56 changes: 0 additions & 56 deletions lib/raxx.ex
Expand Up @@ -887,62 +887,6 @@ defmodule Raxx do
"/" <> Enum.join(request.path, "/") <> query_string
end

@doc """
Can application be run by compatable server?

## Examples

iex> is_application?({Raxx.ServerTest.DefaultServer, %{}})
true

iex> is_application?({GenServer, %{}})
false

iex> is_application?({NotAModule, %{}})
false
"""
@spec is_application?({module(), any()}) :: boolean()
def is_application?({module, _initial_state}) do
Raxx.Server.is_implemented?(module)
end

@doc """
Verify application can be run by compatable server?

## Examples

iex> verify_application({Raxx.ServerTest.DefaultServer, %{}})
{:ok, {Raxx.ServerTest.DefaultServer, %{}}}

iex> verify_application({GenServer, %{}})
{:error, "module `GenServer` does not implement `Raxx.Server` behaviour."}

iex> verify_application({NotAModule, %{}})
{:error, "module `NotAModule` is not available."}
"""
@spec verify_application({module(), any()}) :: {:ok, {module(), any()}} | {:error, String.t()}
def verify_application({module, initial_state}) do
case Code.ensure_compiled?(module) do
true ->
module.module_info[:attributes]
|> Keyword.get(:behaviour, [])
|> Enum.member?(Raxx.Server)
|> case do
true ->
{:ok, {module, initial_state}}

false ->
{
:error,
"module `#{Macro.to_string(module)}` does not implement `Raxx.Server` behaviour."
}
end

false ->
{:error, "module `#{Macro.to_string(module)}` is not available."}
end
end

@doc false
# Use to turn errors into a standard reponse format.
# In the future could be replaced with a protocol,
Expand Down
2 changes: 1 addition & 1 deletion lib/raxx/layout.ex
Expand Up @@ -32,7 +32,7 @@ defmodule Raxx.Layout do

# www/show_user.ex
defmodule WWW.ShowUser do
use Raxx.Server
use Raxx.SimpleServer
use WWW.Layout,
template: "show_user.html.eex",
arguments: [:user]
Expand Down
71 changes: 0 additions & 71 deletions lib/raxx/not_found.ex

This file was deleted.

24 changes: 2 additions & 22 deletions lib/raxx/router.ex
Expand Up @@ -33,23 +33,7 @@ defmodule Raxx.Router do
for {match, controller} <- actions do
{resolved_module, []} = Module.eval_quoted(__CALLER__, controller)

case Code.ensure_compiled(resolved_module) do
{:module, ^resolved_module} ->
behaviours =
resolved_module.module_info[:attributes]
|> Keyword.get(:behaviour, [])

case Enum.member?(behaviours, Raxx.Server) do
true ->
:no_op

false ->
raise "module #{Macro.to_string(resolved_module)} should implement behaviour Raxx.Server"
end

{:error, :nofile} ->
raise "module #{Macro.to_string(resolved_module)} is not loaded"
end
Raxx.Server.verify_implementation!(resolved_module)

# NOTE use resolved module to include any aliasing
controller_string = inspect(resolved_module)
Expand All @@ -67,16 +51,12 @@ defmodule Raxx.Router do
end

quote location: :keep do
@impl Raxx.Server
def handle_request(_request, _state) do
raise "This callback should never be called in a on #{__MODULE__}."
end

@impl Raxx.Server
unquote(routes)

@impl Raxx.Server
def handle_data(data, {controller, state}) do
# TODO add handle_data etc functions from my middleware branch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow, but you probably do :)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I started on general clean up but realised that would confuse this PR

{outbound, new_state} = Raxx.Server.handle({controller, state}, Raxx.data(data))
{outbound, {controller, new_state}}
end
Expand Down