HyperBuffs

HyperBuffs is an Elixir library which strongly connects Phoenix to Protobuf definitions. Based on content negotiation from incoming requests, your controllers will seamlessly accept and respond in either JSON or Protobuf (you can even accept one and return another). The goal is that your controller definitions are strongly typed and you give clients the option of how the data is encoded.

To use HyperBuffs, you will define your routes with a desired schema, e.g.

post "/users", HomeController, :create, private: %{req: Defs.Ping, resp: Defs.Pong}

and your controllers will speak Protobufs:

defmodule HomeController do
def create(_conn, ping=%Defs.Ping{}) do
Defs.Pong.new(payload: ping.payload)
end
end

Installation

If available in Hex, the package can be installed as:

  1. Add hyperbuffs to your list of dependencies in mix.exs:
```elixir
def deps do
[{:hyperbuffs, "~> 0.1.0"}]
end
```
  1. Add the following to your controllers and views:
`web/controllers/page_controller.ex`
```elixir
def MyApp.PageController do
use MyApp.Web, :controller
use HyperBuffs.Controller
end
```
`web/views/page_view.ex`
```elixir
def MyApp.PageView do
use MyApp.Web, :view
use HyperBuffs.View, defs: []
end
```
*or*, to add HyperBuffs to all of your controllers:
`lib/web.ex`
```elixir
defmodule MyApp.Web do
# ...
def controller do
quote do
# ...
use HyperBuffs.Controller # <- add this
end
end
def view do
quote do
# ...
use HyperBuffs.View, defs: [] # <- add this and your defs
end
end
end
```
  1. Add protobufs mime type to your config:
```elixir
config :mime, :types, %{
"application/x-protobuf" => ["proto"]
}
```
  1. After adding that, you'll need to recompile mime:
```bash
mix deps.clean mime --build
mix deps.get
```

Getting Started

To use HyperBuffs, you'll need to define some protobufs, add the proto definitions to your routes, and then rebuild your requests to take and return protobufs. The following walks through an example of this.

  1. Add your protobuf definitions, e.g.:
`lib/defs.ex`
```elixir
defmodule Defs do
use Protobuf, from: Path.wildcard(Path.expand("../definitions/**/*.proto", __DIR__))
end
```
`definitions/example.proto`
```protobuf
syntax = "proto3";
message NameTag {
string name = 1;
}
message Loudspeaker {
string greeting = 1;
}
```
  1. Add proto config to your desired routes:
```elixir
defmodule MyApp.Router do
plug Plug.Parsers, parsers: [Plug.Parsers.Protobuf] # allows Protobuf input
plug :accepts, ["json", "proto"] # allows for Protobuf response
get "/hello_world", private: %{req: :none, resp: Defs.Loudspeaker}
post "/hello", private: %{req: Defs.NameTag, resp: Defs.Loudspeaker}
end
```
  1. Build your actions in your controller:
```elixir
defmodule MyApp.HomeController do
use MyApp.Web, :controller
use HyperBuffs.Controller
def hello_world(_conn) do
Defs.Loudspeaker.new(greeting: "Hello world!")
end
def hello(_conn, name_tag) do
Defs.Loudspeaker.new(greeting: "Hello #{name_tag.name}!")
end
end
```
  1. Add desired protobuf definitions to your view:
```elixir
defmodule MyApp.HomeView do
use MyApp.Web, :view
use HyperBuffs.View, defs: [Defs.Loudspeaker]
end
```

Request and Response

Routes

HyperBuffs is based around building strong types into your route definitions. The ultimate goal of this project is to be able to generate our route definitions from a proto file itself, so we want those routes to be as declarative as possible.

# Get with just output defined
get "/abc", MyController, :abc, private: %{req: :none, resp: Defs.ProtoOut}
# Post with input and output
post "/abc", MyController, :abc, private: %{req: Defs.ProtoIn, resp: Defs.ProtoOut}
# Post with just output defined
post "/abc", MyController, :abc, private: %{req: :none, resp: Defs.ProtoOut}

Note: to be less intrusive, when defining routes, we differentiate between :none and not passing req or resp. If you do not define req or resp then HyperBuffs will ignore this route and it will follow Phoenix's standard processing (e.g. passing a params hash).

Actions

Actions in HyperBuffs try to follow an RPC model where you have a declared input and you return a declared output. HyperBuffs will ensure that the input and output can be either JSON or Protobufs based on the Content-Type and Accept headers respectively.

That said, you still have access to conn and can render traditionally as well. Here's a few examples:

@spec my_action(Plug.Conn.t, %{}) :: Plug.Conn.t | %{} | {Plug.Conn.t, %{}}
def my_action(conn, req=%Defs.SomeReq{}) do
# Return just a protobuf
Defs.SomeResp.new(msg: "Hi #{req.name}")
end
def my_action(conn, req=%Defs.SomeReq{}) do
# Return a conn and a protobuf to be rendered
{
conn |> put_resp_header("X-Req-Id", 5)
Defs.SomeResp.new(msg: "Hi #{req.name}")
}
end
def my_action(conn, req=%Defs.SomeReq{}) do
# Return just a conn
conn
|> HyperBuffs.View.render_proto Defs.SomeResp.new(msg: "Hi #{req.name}")
end

Contributing