defmodule Aino do
@moduledoc """
Aino, an experimental HTTP framework
To load Aino, add to your supervision tree.
`callback`, `otp_app`, `host`, and `port` are required options.
`environment` and `config` are optional and passed into your token
when it's created on each request.
```elixir
aino_config = %Aino.Config{
callback: Example.Web.Handler,
otp_app: :example,
scheme: config.scheme,
host: config.host,
port: config.port,
url_port: config.url_port,
url_scheme: config.url_scheme,
environment: config.environment,
config: %{}
}
children = [
{Aino.Supervisor, aino_config}
]
```
The `callback` should be an `Aino.Handler`, which has a single `handle/1` function that
processes the request.
`otp_app` should be the atom of your OTP application, e.g. `:example`.
`host` and `port` are used for binding and booting the webserver, and
as default assigns for the token when rendering views.
`environment` is the environment the application is running under, similar to `Mix.env()`.
`config` is a simple map that is passed into the token when created, example
values to pass through this config map is your session salt.
"""
@behaviour :elli_handler
require Logger
@doc false
def child_spec(options) do
opts = [
callback: Aino,
callback_args: options,
port: options.port
]
%{
id: __MODULE__,
start: {:elli, :start_link, [opts]},
type: :worker,
restart: :permanent,
shutdown: 500
}
end
@impl true
def init(_request, _options), do: :ignore
@impl true
def handle(request, options) do
try do
request
|> handle_request(options)
|> handle_response()
rescue
exception in Aino.View.MissingTemplateException ->
assigns = %{
exception: exception,
stacktrace: Exception.format_stacktrace(__STACKTRACE__)
}
{500, [{"Content-Type", "text/html"}], Aino.Exception.render_view_missing(assigns)}
exception ->
message = Exception.format(:error, exception, __STACKTRACE__)
Logger.error(message)
assigns = %{exception: Aino.View.Engine.html_escape(message)}
{500, [{"Content-Type", "text/html"}], Aino.Exception.render_generic(assigns)}
end
end
defp handle_request(request, options) do
callback = options.callback
token =
request
|> Aino.Elli.Request.from_record()
|> create_token(options)
callback.handle(token)
end
# Create a token from an `Aino.Request`
defp create_token(request, options) do
request
|> Aino.Token.from_request()
|> Map.put(:otp_app, options.otp_app)
|> Map.put(:scheme, options.url_scheme)
|> Map.put(:host, options.host)
|> Map.put(:port, options.url_port)
|> Map.put(:environment, options.environment)
|> Map.put(:config, options.config)
|> Map.put(:assigns, %{})
end
defp handle_response(%{handover: true}) do
{:close, <<>>}
end
defp handle_response(%{chunk: true} = token) do
Aino.ChunkedHandler.Server.start_link(token)
{:chunk, token.response_headers}
end
defp handle_response(token) do
required_keys = [:response_status, :response_headers, :response_body]
case Enum.all?(required_keys, fn key -> Map.has_key?(token, key) end) do
true ->
{token.response_status, token.response_headers, token.response_body}
false ->
missing_keys = required_keys -- Map.keys(token)
raise "Token is missing required keys - #{inspect(missing_keys)}"
end
end
@impl true
def handle_event(:request_complete, data, _options) do
{timings, _} = Enum.at(data, 4)
diff = timings[:request_end] - timings[:request_start]
microseconds = System.convert_time_unit(diff, :native, :microsecond)
if microseconds > 1_000 do
milliseconds = System.convert_time_unit(diff, :native, :millisecond)
Logger.info("Request complete in #{milliseconds}ms")
else
Logger.info("Request complete in #{microseconds}μs")
end
:ok
end
def handle_event(:request_error, data, _options) do
Logger.error("Internal server error, #{inspect(data)}")
:ok
end
def handle_event(:elli_startup, _data, options) do
Logger.info("Aino started on #{options.scheme}://#{options.host}:#{options.port}")
:ok
end
def handle_event(_event, _data, _options) do
:ok
end
end
defmodule Aino.Config do
@moduledoc """
Config for `Aino` when launching in a supervision tree
```elixir
aino_config = %Aino.Config{
callback: Example.Web.Handler,
otp_app: :example,
scheme: config.scheme,
host: config.host,
port: config.port,
url_port: config.url_port,
url_scheme: config.url_scheme,
environment: config.environment,
config: %{}
}
children = [
{Aino.Supervisor, aino_config}
]
```
"""
@enforce_keys [:callback, :otp_app, :host, :port, :url_port]
defstruct [
:callback,
:otp_app,
:host,
:port,
:config,
:url_port,
environment: "development",
scheme: :http,
url_scheme: :http
]
end
defmodule Aino.Exception do
@moduledoc false
# Compiles the error page into a function for calling in `Aino`
require EEx
EEx.function_from_file(:def, :render_generic, "lib/aino/exceptions/generic.html.eex", [:assigns])
EEx.function_from_file(
:def,
:render_view_missing,
"lib/aino/exceptions/view-missing.html.eex",
[:assigns]
)
end