README.md
1# Protocol Buffers for Elixir
2
3exprotobuf works by building module/struct definitions from a [Google Protocol Buffer](https://code.google.com/p/protobuf)
4schema. This allows you to work with protocol buffers natively in Elixir, with easy decoding/encoding for transport across the
5wire.
6
7[![Build Status](https://travis-ci.org/bitwalker/exprotobuf.svg?branch=master)](https://travis-ci.org/bitwalker/exprotobuf)
8[![Hex.pm Version](http://img.shields.io/hexpm/v/exprotobuf.svg?style=flat)](https://hex.pm/packages/exprotobuf)
9
10## Features
11
12* Load protobuf from file or string
13* Respects the namespace of messages
14* Allows you to specify which modules should be loaded in the definition of records
15* Currently uses [gpb](https://github.com/tomas-abrahamsson/gpb) for protobuf schema parsing
16
17TODO:
18
19* Clean up code/tests
20
21## Breaking Changes
22
23The 1.0 release removed the feature of handling `import "...";` statements.
24Please see [the imports upgrade guide](imports_upgrade_guide.md) for details if you were using this feature.
25
26## Getting Started
27
28Add exprotobuf as a dependency to your project:
29
30```elixir
31defp deps do
32 [{:exprotobuf, "~> x.x.x"}]
33end
34```
35
36Then run `mix deps.get` to fetch.
37
38Add exprotobuf to applications list:
39
40```elixir
41def application do
42 [applications: [:exprotobuf]]
43end
44```
45
46## Usage
47
48Usage of exprotobuf boils down to a single `use` statement within one or
49more modules in your project.
50
51Let's start with the most basic of usages:
52
53### Define from a string
54
55```elixir
56defmodule Messages do
57 use Protobuf, """
58 message Msg {
59 message SubMsg {
60 required uint32 value = 1;
61 }
62
63 enum Version {
64 V1 = 1;
65 V2 = 2;
66 }
67
68 required Version version = 2;
69 optional SubMsg sub = 1;
70 }
71 """
72end
73```
74
75```elixir
76iex> msg = Messages.Msg.new(version: :'V2')
77%Messages.Msg{version: :V2, sub: nil}
78iex> encoded = Messages.Msg.encode(msg)
79<<16, 2>>
80iex> Messages.Msg.decode(encoded)
81%Messages.Msg{version: :V2, sub: nil}
82```
83
84The above code takes the provided protobuf schema as a string, and
85generates modules/structs for the types it defines. In this case, there
86would be a Msg module, containing a SubMsg and Version module. The
87properties defined for those values are keys in the struct belonging to
88each. Enums do not generate structs, but a specialized module with two
89functions: `atom(x)` and `value(x)`. These will get either the name of
90the enum value, or it's associated value.
91
92### Define from a file
93
94```elixir
95defmodule Messages do
96 use Protobuf, from: Path.expand("../proto/messages.proto", __DIR__)
97end
98```
99
100This is equivalent to the above, if you assume that `messages.proto`
101contains the same schema as in the string of the first example.
102
103### Loading all definitions from a set of files
104
105```elixir
106defmodule Protobufs do
107 use Protobuf, from: Path.wildcard(Path.expand("../definitions/**/*.proto", __DIR__))
108end
109```
110
111```elixir
112iex> Protobufs.Msg.new(v: :V1)
113%Protobufs.Msg{v: :V1}
114iex> %Protobufs.OtherMessage{middle_name: "Danger"}
115%Protobufs.OtherMessage{middle_name: "Danger"}
116```
117
118This will load all the various definitions in your `.proto` files and
119allow them to share definitions like enums or messages between them.
120
121### Customizing Generated Module Names
122
123In some cases your library of protobuf definitions might already contain some
124namespaces that you would like to keep.
125In this case you will probably want to pass the `use_package_names: true` option.
126Let's say you had a file called `protobufs/example.proto` that contained:
127
128```protobuf
129package world;
130message Example {
131 enum Continent {
132 ANTARCTICA = 0;
133 EUROPE = 1;
134 }
135
136 optional Continent continent = 1;
137 optional uint32 id = 2;
138}
139```
140
141You could load that file (and everything else in the protobufs directory) by doing:
142
143```elixir
144defmodule Definitions do
145 use Protobuf, from: Path.wildcard("protobufs/*.proto"), use_package_names: true
146end
147```
148
149```elixir
150iex> Definitions.World.Example.new(continent: :EUROPE)
151%Definitions.World.Example{continent: :EUROPE}
152```
153
154You might also want to define all of these modules in the top-level namespace. You
155can do this by passing an explicit `namespace: :"Elixir"` option.
156
157```elixir
158defmodule Definitions do
159 use Protobuf, from: Path.wildcard("protobufs/*.proto"),
160 use_package_names: true,
161 namespace: :"Elixir"
162end
163```
164
165```elixir
166iex> World.Example.new(continent: :EUROPE)
167%World.Example{continent: :EUROPE}
168```
169
170Now you can use just the package names and message names that your team is already
171familiar with.
172
173### Inject a definition into an existing module
174
175This is useful when you only have a single type, or if you want to pull
176the module definition into the current module instead of generating a
177new one.
178
179```elixir
180defmodule Msg do
181 use Protobuf, from: Path.expand("../proto/messages.proto", __DIR__), inject: true
182
183 def update(msg, key, value), do: Map.put(msg, key, value)
184end
185```
186
187```elixir
188iex> %Msg{}
189%Msg{v: :V1}
190iex> Msg.update(%Msg{}, :v, :V2)
191%Msg{v: :V2}
192```
193
194As you can see, Msg is no longer created as a nested module, but is
195injected right at the top level. I find this approach to be a lot
196cleaner than `use_in`, but may not work in all use cases.
197
198### Inject a specific type from a larger subset of types
199
200When you have a large schema, but perhaps only care about a small subset
201of those types, you can use `:only`:
202
203```elixir
204defmodule Messages do
205 use Protobuf, from: Path.expand("../proto/messages.proto", __DIR__),
206only: [:TypeA, :TypeB]
207end
208```
209
210Assuming that the provided .proto file contains multiple type
211definitions, the above code would extract only TypeA and TypeB as nested
212modules. Keep in mind your dependencies, if you select a child type
213which depends on a parent, or another top-level type, exprotobuf may
214fail, or your code may fail at runtime.
215
216You may only combine `:only` with `:inject` when `:only` is a single
217type, or a list containing a single type. This is due to the restriction
218of one struct per module. Theoretically you should be able to pass `:only`
219with multiple types, as long all but one of the types is an enum, since
220enums are just generated as modules, this does not currently work
221though.
222
223### Extend generated modules via `use_in`
224
225If you need to add behavior to one of the generated modules, `use_in`
226will help you. The tricky part is that the struct for the module you
227`use_in` will not be defined yet, so you can't rely on it in your
228functions. You can still work with the structs via the normal Maps API,
229but you lose compile-time guarantees. I would recommend favoring
230`:inject` over this when possible, as it's a much cleaner solution.
231
232```elixir
233defmodule Messages do
234 use Protobuf, "
235 message Msg {
236 enum Version {
237 V1 = 1;
238 V2 = 1;
239 }
240 required Version v = 1;
241 }
242 "
243
244 defmodule MsgHelpers do
245 defmacro __using__(_opts) do
246 quote do
247 def convert_to_record(msg) do
248 msg
249 |> Map.to_list
250 |> Enum.reduce([], fn {_key, value}, acc -> [value | acc] end)
251 |> Enum.reverse
252 |> list_to_tuple
253 end
254 end
255 end
256 end
257
258 use_in "Msg", MsgHelpers
259end
260```
261
262```elixir
263iex> Messages.Msg.new |> Messages.Msg.convert_to_record
264{Messages.Msg, :V1}
265```
266
267## Attribution/License
268
269exprotobuf is a fork of the azukiaapp/elixir-protobuf project, both of which are released under Apache 2 License.
270
271Check LICENSE files for more information.
272