Packages

A pure Elixir implementation of Google Protobuf. A fork of tony612's package.

Current section

Files

Jump to
protocol_buffers lib protobuf dsl.ex
Raw

lib/protobuf/dsl.ex

defmodule Protobuf.DSL do
defmacro field(name, fnum, options) do
quote do
@fields {unquote(name), unquote(fnum), unquote(options)}
end
end
defmacro field(name, fnum) do
quote do
@fields {unquote(name), unquote(fnum), []}
end
end
defmacro oneof(name, index) do
quote do
@oneofs {unquote(name), unquote(index)}
end
end
defmacro __before_compile__(env) do
fields = Module.get_attribute(env.module, :fields)
options = Module.get_attribute(env.module, :options)
syntax = Keyword.get(options, :syntax, :proto2)
oneofs = Module.get_attribute(env.module, :oneofs)
msg_props = generate_msg_props(fields, oneofs, options)
default_fields = generate_default_fields(syntax, msg_props)
enum_fields = enum_fields(msg_props, false)
default_struct = Map.put(default_fields, :__struct__, env.module)
quote do
def __message_props__ do
unquote(Macro.escape(msg_props))
end
unquote(def_enum_functions(msg_props))
if unquote(syntax == :proto3) do
def __default_struct__ do
struct = unquote(Macro.escape(default_struct))
Enum.reduce(unquote(Macro.escape(enum_fields)), struct, fn {name, type}, acc ->
Map.put(acc, name, type.key(0))
end)
end
else
def __default_struct__ do
unquote(Macro.escape(default_struct))
end
end
end
end
defp def_enum_functions(%{syntax: syntax, enum?: true, field_props: props}) do
if syntax == :proto3 do
found = Enum.find(props, fn {_, %{fnum: fnum}} ->
fnum == 0
end)
if !found, do: raise "The first enum value must be zero in proto3"
end
mapping = Enum.reduce(props, %{}, fn {_, %{fnum: fnum, name_atom: name_atom}}, acc ->
Map.put(acc, name_atom, fnum)
end)
Enum.map(props, fn {_, %{fnum: fnum, name_atom: name_atom}} ->
quote do
def value(unquote(name_atom)), do: unquote(fnum)
end
end) ++ [
quote do
def value(v) when is_integer(v), do: v
end
] ++
Enum.map(props, fn {_, %{fnum: fnum, name_atom: name_atom}} ->
quote do
def key(unquote(fnum)), do: unquote(name_atom)
end
end) ++ [
quote do
def mapping(), do: unquote(Macro.escape(mapping))
end
]
end
defp def_enum_functions(_), do: nil
defp generate_msg_props(fields, oneofs, options) do
syntax = Keyword.get(options, :syntax, :proto2)
field_props = field_props_map(syntax, fields)
repeated_fields =
field_props
|> Map.values()
|> Enum.filter(fn props -> props.repeated? end)
|> Enum.map(fn props -> Map.get(props, :name_atom) end)
embedded_fields =
field_props
|> Map.values()
|> Enum.filter(fn props -> props.embedded? && !props.map? end)
|> Enum.map(fn props -> Map.get(props, :name_atom) end)
%Protobuf.MessageProps{
tags_map: tags_map(fields),
ordered_tags: ordered_tags(fields),
field_props: field_props,
field_tags: field_tags(fields),
repeated_fields: repeated_fields,
embedded_fields: embedded_fields,
syntax: syntax,
oneof: Enum.reverse(oneofs),
enum?: Keyword.get(options, :enum) == true,
map?: Keyword.get(options, :map) == true
}
end
defp tags_map(fields) do
fields
|> Enum.map(fn {_, fnum, _} -> {fnum, fnum} end)
|> Enum.into(%{})
end
defp ordered_tags(fields) do
fields
|> Enum.map(fn {_, fnum, _} -> fnum end)
|> Enum.sort()
end
defp field_props_map(syntax, fields) do
fields
|> Enum.map(fn {name, fnum, opts} -> {fnum, field_props(syntax, name, fnum, opts)} end)
|> Enum.into(%{})
end
defp field_tags(fields) do
fields
|> Enum.map(fn {name, fnum, _} -> {name, fnum} end)
|> Enum.into(%{})
end
defp field_props(syntax, name, fnum, opts) do
props = %Protobuf.FieldProps{
fnum: fnum,
name: to_string(name),
name_atom: name
}
opts_map = Enum.into(opts, %{})
# parse simple fields then calculate others in cal_*
parts =
opts
|> parse_field_opts(opts_map)
|> cal_label(syntax)
|> cal_type()
|> cal_default(syntax)
|> cal_embedded()
|> cal_packed(syntax)
|> cal_repeated(opts_map)
struct(props, parts)
|> cal_encoded_fnum()
end
defp parse_field_opts([{:optional, true} | t], acc) do
parse_field_opts(t, Map.put(acc, :optional?, true))
end
defp parse_field_opts([{:required, true} | t], acc) do
parse_field_opts(t, Map.put(acc, :required?, true))
end
defp parse_field_opts([{:enum, true} | t], acc) do
parse_field_opts(t, Map.put(acc, :enum?, true))
end
defp parse_field_opts([{:map, true} | t], acc) do
parse_field_opts(t, Map.put(acc, :map?, true))
end
defp parse_field_opts([{:default, default} | t], acc) do
parse_field_opts(t, Map.put(acc, :default, default))
end
defp parse_field_opts([{:oneof, oneof} | t], acc) do
parse_field_opts(t, Map.put(acc, :oneof, oneof))
end
# skip unknown option
defp parse_field_opts([{_, _} | t], acc) do
parse_field_opts(t, acc)
end
defp parse_field_opts(_, acc), do: acc
defp cal_label(%{required?: true}, :proto3) do
raise Protobuf.InvalidError, message: "required can't be used in proto3"
end
defp cal_label(props, :proto3) do
Map.put(props, :optional?, true)
end
defp cal_label(props, _), do: props
defp cal_type(%{enum?: true, type: type} = props) do
Map.merge(props, %{type: {:enum, type}, wire_type: Protobuf.Encoder.wire_type(:enum)})
end
defp cal_type(%{type: type} = props) do
Map.merge(props, %{type: type, wire_type: Protobuf.Encoder.wire_type(type)})
end
defp cal_type(props), do: props
defp cal_default(%{default: default}, :proto3) when not is_nil(default) do
raise Protobuf.InvalidError, message: "default can't be used in proto3"
end
defp cal_default(props, _), do: props
defp cal_embedded(%{type: type} = props) when is_atom(type) do
case to_string(type) do
"Elixir." <> _ -> Map.put(props, :embedded?, !props[:enum?])
_ -> props
end
end
defp cal_embedded(props), do: props
defp cal_packed(%{packed: true, repeated: repeated} = props, _) do
cond do
props[:embedded?] -> raise ":packed can't be used with :embedded field"
repeated -> Map.put(props, :packed?, true)
true -> raise ":packed must be used with :repeated"
end
end
defp cal_packed(%{packed: false} = props, _) do
Map.put(props, :packed?, false)
end
defp cal_packed(%{repeated: repeated, type: type} = props, :proto3) do
packed = (props[:enum?] || !props[:embedded?]) && type_numeric?(type)
if packed && !repeated do
raise ":packed must be used with :repeated"
else
Map.put(props, :packed?, packed)
end
end
defp cal_packed(props, _), do: Map.put(props, :packed?, false)
defp cal_repeated(%{map?: true} = props, _), do: Map.put(props, :repeated?, false)
defp cal_repeated(props, %{repeated: true}), do: Map.put(props, :repeated?, true)
defp cal_repeated(_props, %{repeated: true, oneof: true}), do:
raise ":oneof can't be used with repeated"
defp cal_repeated(props, _), do: props
defp cal_encoded_fnum(%{fnum: fnum, packed?: true} = props) do
encoded_fnum = Protobuf.Encoder.encode_fnum(fnum, Protobuf.Encoder.wire_type(:bytes))
Map.put(props, :encoded_fnum, encoded_fnum)
end
defp cal_encoded_fnum(%{fnum: fnum, wire_type: wire} = props) when is_integer(wire) do
encoded_fnum = Protobuf.Encoder.encode_fnum(fnum, wire)
Map.put(props, :encoded_fnum, encoded_fnum)
end
defp cal_encoded_fnum(props) do
props
end
def generate_default_fields(syntax, msg_props) do
fields =
msg_props.field_props
|> Map.values()
|> Enum.reduce(%{}, fn props, acc ->
if props.oneof do
acc
else
Map.put(acc, props.name_atom, Protobuf.Builder.field_default(syntax, props))
end
end)
Enum.reduce(msg_props.oneof, fields, fn {key, _}, acc ->
Map.put(acc, key, nil)
end)
end
def embedded_fields(msg_props) do
msg_props.field_props
|> Map.values()
|> Enum.filter(fn props -> props.embedded? && !props.repeated? && !props.map? end)
|> Enum.map(fn props -> {props.name_atom, props.type} end)
end
def enum_fields(msg_props, include_oneof? \\ true)
def enum_fields(%{syntax: :proto3} = msg_props, include_oneof?) do
msg_props.field_props
|> Map.values()
|> Enum.filter(fn props -> props.enum? && !props.default && !props.repeated? && (!props.oneof || include_oneof?) end)
|> Enum.map(fn props ->
{props.name_atom, elem(props.type, 1)}
end)
end
def enum_fields(%{syntax: _}, _include_oneof?), do: %{}
def type_numeric?(:int32), do: true
def type_numeric?(:int64), do: true
def type_numeric?(:uint32), do: true
def type_numeric?(:uint64), do: true
def type_numeric?(:sint32), do: true
def type_numeric?(:sint64), do: true
def type_numeric?(:bool), do: true
def type_numeric?({:enum, _}), do: true
def type_numeric?(:fixed32), do: true
def type_numeric?(:sfixed32), do: true
def type_numeric?(:fixed64), do: true
def type_numeric?(:sfixed64), do: true
def type_numeric?(:float), do: true
def type_numeric?(:double), do: true
def type_numeric?(_), do: false
end