I took lessons from the last discussion and cobbled together an example as a proof-of-concept.
Mar demonstrates a Flask-like web dev interface powered by Plug on Bandit.
use Mar
immediately makes a module a route. The user modules don’t need to report to an entry-point plug in the app. Library handles routing.
Normal def
s defines the actions to the requests. So you can compose them Elixir way. Router matches them by their names and the HTTP methods.
defmodule MyApp do
use Mar
def get(), do: "Hello, world!"
end
path
can be set. Default is "/"
otherwise.
params
declares allowed parameters alongside path parameters with :
. Later it takes matching keys from conn.params
and puts it in the actions.
Actions can return a string, a map for JSON, or a tuple being {status, headers, body}
.
defmodule MyApp do
use Mar, path: "/post/:id", params: [:comment]
def get(%{id: id}) do
"You are reading #{id}"
end
def post(%{id: id, comment: comment}) do
%{
id: id,
comment: comment
}
end
def delete(%{id: _id}) do
{301, [location: "/"], nil}
end
end
Routes can interact with the library through Mar.Route
protocol.
defmodule MyApp do
use Mar
def get(), do: "Hello, world!"
defimpl Mar.Route do
# Mar.Route.MyApp
def before_action(route) do
IO.inspect(route.conn.resp_body)
# => nil
route
end
def after_action(route) do
IO.inspect(route.conn.resp_body)
# => "Hello, world!"
route
end
end
end
Intention
While there are many possible approaches to helping adoption and facilitating learning, the challenge I tackle here is to nicely encapsulate Plug and reduce cognitive load for the users. @taro lacks the technical capability for something production-ready, this project waxes on top of Bandit with an escape hatch in an attempt to help you envision a light and intuitive web framework for Elixir.
The insight and guidance from the community is much appreciated:
Design
This library relies on protocol consolidation to handle routes. use Mar
injects a default defimpl
of Mar.Route
protocol. It lists up the user modules. At the same time, defstruct
saves the path as a default struct value. Then the list of implementation maps to structs, which has information for path-matching.
case Mar.Route.__protocol__(:impls) do
{:consolidated, modules} -> Enum.map(modules, &struct(&1))
:not_consolidated -> []
end
# [
# %MyApp{ path: "/", ...},
# %MyApp.Post{ path: "/post/:id/", ...}, ,
# ...
# ]
Mar.Router
leaves escape hatches open with the Mar.Route
protocol. The user modules redefine the functions with defimpl
to access them.
# Mar.Router
def call(conn, _options) do
# Match routes, load params
route = Mar.Route.before_action(route)
# Apply action
route = Mar.Route.after_action(route)
# Send response
end
Atom keys are preferred over string keys for the sake of nicer syntax. That’s also why params
need to be declared so the library can prevent dynamic atom creation.
What do you think? I’m hoping to hear from you!