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 services with a desired RPC schema and connect those to your routes.
service ExampleService {
rpc ping (Ping) returns (Pong) {
option (google.api.http) = { post: "/ping" };
}
rpc status (StatusRequest) returns (StatusResponse) {
option (google.api.http) = { get: "/status" };
}
}
service ExampleService, ExampleController
and your controllers will speak Protobuf:
defmodule ExampleController do
# Our actions now take a protobuf and return a protobuf
@spec create(%Plug.Conn{}, %Defs.Ping{}) :: %Defs.Pong
def create(_conn, %Defs.Ping{payload: payload}) do
Defs.Pong.new(payload: payload)
end
end
If available in Hex, the package can be installed as:
- Add
hyperbuffs
to your list of dependencies inmix.exs
:
```elixir
def deps do
[{:hyperbuffs, "~> 0.2.3"}]
end
```
- Install
protoc-gen-elixir
fromprotobuf-ex
:
```bash
cd ~
git clone https://github.com/hayesgm/protobuf-ex.git
cd protobuf-ex
mix escript.install
```
- Add the following to your controllers, views and router:
`lib/my_app.ex`
```elixir
defmodule MyApp do
# ...
def controller do
quote do
use Phoenix.Controller, namespace: MyApp
use Hyperbuffs.Controller # <- add this
# ...
end
end
def view do
quote do
use Phoenix.View, root: "lib/my_app/templates",
namespace: MyApp
use Hyperbuffs.View # <- add this
# ...
end
end
def router do
quote do
use Phoenix.Router
use Hyperbuffs.Router # <- add this
# ...
end
end
end
```
*or*, add Hyperbuffs to each controller, view and router:
`page_controller.ex`
```elixir
def MyApp.PageController do
use MyApp, :controller
use Hyperbuffs.Controller
end
```
`page_view.ex`
```elixir
def MyApp.PageView do
use MyApp, :view
use Hyperbuffs.View
end
```
`router.ex`
```elixir
defmodule API.Router do
use API, :router
use Hyperbuffs.Router
end
```
- Add
protobufs
mime type to your config:
`mix.exs`
defp deps do
# ...
{:mime, "~> 1.1"}
end
`config.exs`
```elixir
config :mime, :types, %{
"application/x-protobuf" => ["proto"]
}
```
- After adding that, you'll need to recompile
mime
:
```bash
mix deps.clean mime --build
mix deps.get
```
To use Hyperbuffs, you'll need to define some protobufs, add the service definitions to your routes, and then build your controller actions to take and return protobufs. The following walks through an example of this.
- Add your protobuf definitions, e.g.:
`priv/proto/services.proto`
```elixir
syntax = "proto3";
import "annotations.proto";
package MyApp;
service PingService {
rpc ping (Ping) returns (Pong) {
option (google.api.http) = { get: "/ping" };
}
}
message Ping {}
message Pong {
uint32 timestamp = 1;
}
```
- Generate your protobuf definitions (replace
my_app
with your app or package name)
mkdir ./lib/my_app/proto
protoc --proto_path="./priv/proto" --proto_path="./deps/hyperbuffs/priv/proto" --elixir_out="./lib/my_app/proto" ./priv/proto/**
Note: for an umbrella app, this would be:
protoc --proto_path="./priv/proto" --proto_path="../../deps/hyperbuffs/priv/proto" --elixir_out="./lib/my_app/proto" ./priv/proto/**
- Add proto config to your desired routes:
```elixir
defmodule MyApp.Router do
use MyApp, :router
# Add this section if you want to allow protobuf inputs and responses
pipeline :api do
plug Plug.Parsers, parsers: [Plug.Parsers.Protobuf] # allows Protobuf input
plug :accepts, ["json", "proto"] # allows for Protobuf response
end
scope "/" do
pipe_through :api
service MyApp.PingService, StatusController
end
end
```
- Build your actions in your controller:
```elixir
defmodule MyApp.StatusController do
use MyApp, :controller
@doc """
Responds Pong to Ping.
## Examples
iex> MyApp.StatusController.ping(%Plug.Conn{}, %MyApp.Ping{})
%MyApp.Pong{timestamp: 1508114537}
"""
@spec ping(Plug.Conn.t, %MyApp.Ping{}) :: %MyApp.Pong{}
def ping(_conn, _ping) do
MyApp.Pong.new(timestamp: :os.system_time(:seconds))
end
end
```
- Make sure you have a view for your controller:
```elixir
defmodule MyApp.StatusView do
use MyApp, :view
end
```
- That's all, run your app and view the endpoint.
```bash
$ mix phx.server
$ curl localhost:4000/ping
{"timestamp":1509119708}
```
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
- For bugs, please open an issue with steps to reproduce.
- For smaller feature requests, please either create an issue, or fork and create a PR.
- For larger feature requests, please create an issue before starting work so we can discuss the design decisions.