Packages
protox
2.0.1
2.0.9
2.0.8
2.0.7
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
2.0.0-dev
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
retired
1.7.3
1.7.2
1.7.1
1.7.0
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.0
1.3.2
1.3.1
1.3.0
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.1
1.1.0
1.0.0
0.25.0
0.24.0
0.23.1
0.23.0
0.22.0
0.21.0
0.20.0
0.19.1
0.19.0
0.18.0
0.17.0
0.16.2
0.16.1
0.16.0
0.15.2
0.15.1
0.15.0
0.14.0
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.0
0.7.1
0.7.0
A fast, easy to use and 100% conformant Elixir library for Google Protocol Buffers (aka protobuf)
Current section
Files
Jump to
Current section
Files
lib/protox/parse.ex
defmodule Protox.Parse do
@moduledoc false
# Internal.
# Creates definitions from a protobuf encoded description (Protox.Google.Protobuf.FileDescriptorSet)
# of a set of .proto files. This description is produced by `protoc`.
import Protox.Guards
alias Protox.Google.Protobuf.{
DescriptorProto,
FieldDescriptorProto,
FieldOptions,
FileDescriptorProto,
FileDescriptorSet,
MessageOptions
}
alias Protox.{Definition, Field, MessageSchema, OneOf, Scalar}
@spec parse(binary(), Keyword.t()) :: {:ok, Definition.t()}
def parse(file_descriptor_set, opts \\ []) do
{:ok, descriptor} = FileDescriptorSet.decode(file_descriptor_set)
definition =
%Definition{}
|> parse_files(descriptor.file)
|> post_process(opts)
|> add_file_options()
|> remove_google_types()
{:ok, definition}
end
# -- Private
defp parse_files(definition, descriptors) do
Enum.reduce(
descriptors,
definition,
fn %FileDescriptorProto{} = descriptor, definition -> parse_file(definition, descriptor) end
)
end
defp parse_file(definition, %FileDescriptorProto{} = descriptor) do
syntax =
case descriptor.syntax do
"proto3" -> :proto3
"proto2" -> :proto2
"" -> :proto2
end
prefix =
case descriptor.package do
"" -> []
p -> p |> String.split(".") |> camelize()
end
definition
|> make_enums(prefix, descriptor.enum_type)
|> make_messages(syntax, prefix, descriptor.message_type, descriptor.options)
|> add_extensions(syntax, descriptor.extension)
end
# Prepend with namespace, resolve pending types and set default values
defp post_process(definition, opts) do
namespace_or_nil = Keyword.get(opts, :namespace, nil)
processed_messages =
for {msg_name, %MessageSchema{} = msg} <- definition.messages_schemas, into: %{} do
name = Module.concat([namespace_or_nil | msg_name])
fields =
for {field_name, field} <- msg.fields, into: %{} do
field =
field
|> resolve_types(definition.enums_schemas)
|> set_default_value(definition.enums_schemas)
|> concat_names(namespace_or_nil)
{field_name, field}
end
{name, %{msg | name: name, fields: fields}}
end
processsed_enums =
for {ename, constants} <- definition.enums_schemas, into: %{} do
{Module.concat([namespace_or_nil | ename]), constants}
end
%Definition{enums_schemas: processsed_enums, messages_schemas: processed_messages}
end
# We remove all Google types as:
# - they're either well-known types (Any, Timestamp, etc.) which are provided with protox.
# - or they come from descriptor.proto, which are only relevant (as far as I know) protox.
# Removing these types permit to generate smaller code.
defp remove_google_types(definition) do
filtered_messages =
Map.reject(definition.messages_schemas, fn {msg_name, _message} ->
match?(["Google", "Protobuf" | _], Module.split(msg_name))
end)
filtered_enums =
Map.reject(definition.enums_schemas, fn {enum_name, _constants} ->
match?(["Google", "Protobuf" | _], Module.split(enum_name))
end)
%Definition{enums_schemas: filtered_enums, messages_schemas: filtered_messages}
end
defp add_file_options(definition) do
# Custom file options are defined by users, so they can't be described in
# the FileOptions of descriptor.ex. They are parsed as unknown fields.
# So, to decode these custom fields, we have to compile the FileOptions
# which comes with the current FileDescriptorSet.
# Look for FileOptions and related messages. If found, we will compile them
# to parse file options.
{file_options_messages, other_messages} =
Map.split(definition.messages_schemas, [
Google.Protobuf.FeatureSet,
Google.Protobuf.FileOptions,
Google.Protobuf.UninterpretedOption,
Google.Protobuf.UninterpretedOption.NamePart
])
case file_options_messages do
file_options_message when map_size(file_options_message) == 0 ->
definition
_ ->
# If FileOptions and other related message have been found, we also need
# to compile their associated enums.
{file_options_optimize_enum, other_enums} =
Map.split(definition.enums_schemas, [
Google.Protobuf.FeatureSet.EnumType,
Google.Protobuf.FeatureSet.FieldPresence,
Google.Protobuf.FeatureSet.JsonFormat,
Google.Protobuf.FeatureSet.MessageEncoding,
Google.Protobuf.FeatureSet.RepeatedFieldEncoding,
Google.Protobuf.FeatureSet.Utf8Validation,
Google.Protobuf.FileOptions.OptimizeMode
])
# Compile the needed modules.
%Definition{
messages_schemas: file_options_messages,
enums_schemas: file_options_optimize_enum
}
|> Protox.Define.define()
|> Code.eval_quoted()
# We can now parse the unknown fields with the modules compiled above.
# Also, we transform this FileOptions into a bare map so as to not depend
# on the FileOptions type which is not necessary for the end user.
other_messages =
for {msg_name, msg} <- other_messages, into: %{} do
file_options =
msg.file_options
|> Protox.Google.Protobuf.FileOptions.encode!()
|> elem(_bytes_position_in_tuple = 0)
|> IO.iodata_to_binary()
|> then(&apply(Google.Protobuf.FileOptions, :decode!, [&1]))
|> Map.from_struct()
{msg_name, %{msg | file_options: file_options}}
end
# It's no longer necessary to keep the compiled modules in memory as they are of
# no use for end user.
remove_module(Google.Protobuf.FeatureSet)
remove_module(Google.Protobuf.FileOptions)
remove_module(Google.Protobuf.UninterpretedOption)
remove_module(Google.Protobuf.UninterpretedOption.NamePart)
remove_module(Google.Protobuf.FeatureSet.EnumType)
remove_module(Google.Protobuf.FeatureSet.FieldPresence)
remove_module(Google.Protobuf.FeatureSet.JsonFormat)
remove_module(Google.Protobuf.FeatureSet.MessageEncoding)
remove_module(Google.Protobuf.FeatureSet.RepeatedFieldEncoding)
remove_module(Google.Protobuf.FeatureSet.Utf8Validation)
remove_module(Google.Protobuf.FileOptions.OptimizeMode)
# Finally, construct a new definition with the messages and enums that were not used
# to parse FileOptions.
definition
|> put_in([Access.key!(:messages_schemas)], other_messages)
|> put_in([Access.key!(:enums_schemas)], other_enums)
end
end
defp resolve_types(%Field{type: {:type_to_resolve, tname}} = field, enums) do
if Map.has_key?(enums, tname) do
%{field | type: {:enum, tname}}
else
%{field | type: {:message, tname}}
end
end
defp resolve_types(%Field{kind: :map, type: {key_type, {:type_to_resolve, tname}}} = field, enums) do
if Map.has_key?(enums, tname) do
%{field | type: {key_type, {:enum, tname}}}
else
%{field | type: {key_type, {:message, tname}}}
end
end
defp resolve_types(%Field{} = field, _enums), do: field
defp set_default_value(
%Field{kind: %Scalar{default_value: :enum_default_to_resolve}, type: {:enum, ename}} = field,
enums
) do
# proto2: the first entry is always the default value
# proto3: the entry with value 0 is the default value, and protoc mandates the first entry
# to have the value 0
[{_, first_is_default} | _] = Map.fetch!(enums, ename)
%{field | kind: %Scalar{default_value: first_is_default}, type: {:enum, ename}}
end
defp set_default_value(%Field{} = field, _enums), do: field
defp concat_names(%Field{type: {:enum, ename}} = field, namespace_or_nil) do
%{field | type: {:enum, Module.concat([namespace_or_nil | ename])}}
end
defp concat_names(%Field{type: {:message, mname}} = field, namespace_or_nil) do
%{field | type: {:message, Module.concat([namespace_or_nil | mname])}}
end
defp concat_names(%Field{kind: :map, type: {key_type, {:message, mname}}} = field, namespace_or_nil) do
%{field | type: {key_type, {:message, Module.concat([namespace_or_nil | mname])}}}
end
defp concat_names(%Field{type: {key_type, {:enum, ename}}} = field, namespace_or_nil) do
%{field | type: {key_type, {:enum, Module.concat([namespace_or_nil | ename])}}}
end
defp concat_names(%Field{} = field, _), do: field
defp make_enums(definition, prefix, descriptors) do
Enum.reduce(descriptors, definition, fn descriptor, definition ->
make_enum(definition, prefix, descriptor)
end)
end
defp make_enum(definition, prefix, descriptor) do
enum_name = prefix ++ camelize([descriptor.name])
enum_constants = Enum.map(descriptor.value, &{&1.number, String.to_atom(&1.name)})
put_in(definition, [Access.key!(:enums_schemas), enum_name], enum_constants)
end
defp make_messages(definition, syntax, prefix, descriptors, file_options) do
Enum.reduce(descriptors, definition, fn descriptor, definition ->
make_message(definition, syntax, prefix, descriptor, file_options)
end)
end
defp make_message(
definition,
_syntax,
_prefix,
%DescriptorProto{options: %MessageOptions{map_entry: map_entry}},
_file_options
)
when map_entry do
# This case has already been handled in the upper message with add_maps.
definition
end
defp make_message(definition, syntax, prefix, descriptor, file_options) do
msg_name = prefix ++ camelize([descriptor.name])
definition
|> add_message(syntax, msg_name, file_options)
|> make_messages(syntax, msg_name, descriptor.nested_type, file_options)
|> make_enums(msg_name, descriptor.enum_type)
|> add_fields(descriptor, msg_name, syntax, descriptor.field)
|> add_fields(descriptor, msg_name, syntax, descriptor.extension)
end
defp add_message(definition, syntax, name, file_options) do
put_in(definition, [Access.key!(:messages_schemas), name], %MessageSchema{
name: name,
syntax: syntax,
fields: %{},
file_options: file_options
})
end
defp add_extensions(definition, syntax, fields) do
Enum.reduce(fields, definition, fn field, definition ->
add_field(definition, syntax, _upper = nil, fully_qualified_name(field.extendee), field)
end)
end
defp add_fields(definition, upper, msg_name, syntax, fields) do
Enum.reduce(fields, definition, fn field, definition ->
add_field(definition, syntax, upper, msg_name, field)
end)
end
defp add_field(definition, syntax, upper, msg_name, descriptor) do
{label, kind, type} =
case map_entry(upper, msg_name, descriptor) do
nil ->
type = get_type(descriptor)
kind = get_kind(syntax, upper, descriptor)
{field_label(descriptor), kind, type}
map_type ->
{nil, :map, map_type}
end
field =
Field.new!(
tag: descriptor.number,
label: label,
name: String.to_atom(descriptor.name),
kind: kind,
type: type
)
put_in(
definition,
[Access.key!(:messages_schemas), msg_name, Access.key!(:fields), field.name],
field
)
end
defp field_label(%{proto3_optional: true}), do: :proto3_optional
defp field_label(%{label: label}), do: label
defp map_entry(nil, _prefix, _descriptor), do: nil
defp map_entry(upper, prefix, %FieldDescriptorProto{label: :repeated, type: :message} = descriptor) do
# Might be a map. Now find a nested type of upper that is the corresponding entry.
search_nested_type =
Enum.find(upper.nested_type, fn m ->
if m.options != nil and m.options.map_entry do
m_name = prefix ++ [m.name]
t_name = fully_qualified_name(descriptor.type_name)
# Test if the generated name of the MapEntry message is the same as the one
# referenced by the actual map field.
m_name == t_name
else
false
end
end)
case search_nested_type do
nil ->
nil
m ->
key_type = Enum.find(m.field, &(&1.name == "key")).type
value_type_field = Enum.find(m.field, &(&1.name == "value"))
value_type = get_type(value_type_field)
{key_type, value_type}
end
end
defp map_entry(_upper, _prefix, _descriptor), do: nil
defp fully_qualified_name(name) do
# Make sure first element is always ".".
true = String.starts_with?(name, ".")
name
|> String.split(".")
|> tl()
|> camelize()
end
defp get_kind(_syntax, upper, %FieldDescriptorProto{oneof_index: index}) when index != nil do
%OneOf{parent: String.to_atom(Enum.at(upper.oneof_decl, index).name)}
end
defp get_kind(_syntax, _upper, %FieldDescriptorProto{label: :repeated, options: %FieldOptions{packed: true}}) do
:packed
end
defp get_kind(_syntax, _upper, %FieldDescriptorProto{label: :repeated, options: %FieldOptions{packed: false}}) do
:unpacked
end
defp get_kind(:proto3, _upper, %FieldDescriptorProto{label: :repeated, type: :enum}) do
:packed
end
defp get_kind(:proto3, _upper, %FieldDescriptorProto{label: :repeated, type: ty}) when is_primitive(ty) do
:packed
end
defp get_kind(_syntax, _upper, %FieldDescriptorProto{label: :repeated}), do: :unpacked
defp get_kind(_syntax, _upper, %FieldDescriptorProto{label: label} = descriptor)
when label == :optional or label == :required do
%Scalar{default_value: get_default_value(descriptor)}
end
defp get_type(%FieldDescriptorProto{type_name: tyname}) when tyname != nil do
# Documentation in descriptor.proto says that it's possible that `type_name` is set, but not
# `type`. The type will be resolved in a post-process pass.
{:type_to_resolve, fully_qualified_name(tyname)}
end
defp get_type(descriptor), do: descriptor.type
defp get_default_value(%FieldDescriptorProto{type: :enum, default_value: nil}) do
:enum_default_to_resolve
end
defp get_default_value(%FieldDescriptorProto{type: :enum} = f) do
String.to_atom(f.default_value)
end
defp get_default_value(%FieldDescriptorProto{type: :message}), do: nil
defp get_default_value(%FieldDescriptorProto{type: ty, default_value: nil}) do
Protox.Default.default(ty)
end
defp get_default_value(%FieldDescriptorProto{type: :bool, default_value: "true"}), do: true
defp get_default_value(%FieldDescriptorProto{type: :bool, default_value: "false"}), do: false
defp get_default_value(%FieldDescriptorProto{type: :string} = f), do: f.default_value
defp get_default_value(%FieldDescriptorProto{type: :bytes} = f), do: f.default_value
defp get_default_value(%FieldDescriptorProto{type: :double} = f) do
f.default_value |> Float.parse() |> elem(0)
end
defp get_default_value(%FieldDescriptorProto{type: :float} = f) do
f.default_value |> Float.parse() |> elem(0)
end
defp get_default_value(f) do
f.default_value |> Integer.parse() |> elem(0)
end
defp camelize(name) when is_list(name) do
Enum.map(name, &Macro.camelize/1)
end
defp remove_module(module) when is_atom(module) do
:code.delete(module)
:code.purge(module)
end
end