Aino
An experimental HTTP framework built on top of elli. Aino is pronounced as "eye no".
Why Aino?
Aino is an experiment to try out a new way of writing HTTP applications on Elixir. It uses elli instead of Cowboy like Phoenix and Plug. Instead of writing an Endpoint like Phoenix, you write a Handler. The handler's job is to reduce across a series of middleware that are simple functions to generate a response.
The handler also works on a token instead of a conn. The token is a simple map that you can add whatever keys you wish to it. Aino has a few standard keys but you can easily ignore them if you want to write your own processing.
How to use Aino
In order to use Aino, you must add it to your supervision tree and provide a callback handler that Aino will call handle/1
on.
defmodule Aino.Application do
use Application
def start(_type, _args) do
# get your config somehow
aino_config = %Aino.Config{
callback: Example.Web.Handler,
otp_app: :example,
host: config.host,
port: config.port,
environment: config.environment,
config: %{}
}
children = [
{Aino.Supervisor, aino_config}
]
opts = [strategy: :one_for_one, name: Aino.Supervisor]
Supervisor.start_link(children, opts)
end
end
In the handler, you process the incoming request (in the token
) through a series of "middleware." The middleware all accept a single parameter, the token
. A token
is simply a map that you can store whatever you want on it.
The only thing that is initially pased in is the :request
, and at the very end of the handle/1
the token should include three keys, :response_status
, :response_headers
, and :response_body
.
Aino ships with a common set of middleware that you can include at the top of processing, if you don't want them, simply don't include them! The list of middleware can be a list of lists as well.
Another built in middleware is a simple routing layer. Import the HTTP methods from Aino.Middleware.Routes
that you're going to use in your routes. Then each HTTP method function takes the route and a middleware that should be run on the route.
defmodule MyApp.Handler do
import Aino.Middleware.Routes, only: [get: 2, get: 3, post: 2]
@behaviour Aino.Handler
def routes() do
[
get("/", &Index.index/1, as: :root),
get("/about", &Index.about/1, as: :about),
order_routes()
]
end
defp order_routes() do
[
get("/orders", &Orders.index/1, as: :orders),
get("/orders/:id", &Orders.show/1, as: :order),
post("/orders", &Orders.create/1)
]
end
@impl true
def handle(token) do
middleware = [
Aino.Middleware.common(),
&Aino.Middleware.Routes.routes(&1, routes()),
&Aino.Middleware.Routes.match_route/1,
&Aino.Middleware.params/1,
&Aino.Middleware.Routes.handle_route/1,
]
Aino.Token.reduce(token, middleware)
end
end
The route middleware take a token and generally should return the three keys required to render a response. You can also render EEx templates as shown below.
defmodule Index do
alias Aino.Token
def index(token) do
token
|> Token.response_status(200)
|> Token.response_header("Content-Type", "text/html")
|> Token.response_body(Index.View.render("index.html"))
end
end
defmodule Index.View do
require Aino.View
Aino.View.compile [
"lib/index/index.html.eex"
]
end
Concepts
Aino.Handler
A handler processes an incoming request from Aino.
The handle/1
function is passed an Aino.Token
.
The handler must return a token that contains three keys to return a response:
:response_status
:response_headers
:response_body
If the token does not contain these three keys, a 500 error is returned.
Inside your handler, you may wish to use several Aino.Middleware
including
Aino.Middleware.common/0
.
Aino.Token
The token is what flows through the entire web request. Tokens are simple maps
that contain no defined keys beyond :request
. Several Aino middleware add
keys and they are documented in the functions.
Aino.Middleware
Middleware are simple functions that take the token and return the token. They process the request and add or modify existing keys on the token.
An example middleware is Aino.Middleware.headers/1
:
def headers(%{request: request} = token) do
headers =
Enum.map(request.headers, fn {header, value} ->
{String.downcase(header), value}
end)
Map.put(token, :headers, headers)
end