Skip to content

oestrich/aino

Repository files navigation

Aino

Discord

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