Current section

Files

Jump to
pbuf lib protoc context.ex
Raw

lib/protoc/context.ex

defmodule Pbuf.Protoc.Context do
@moduledoc """
Context holds information necessary for building types. The initial context
contains file-wide information (the package name, enums). This file-wide
context is used as the basis for message-specific contexts which include things
which are only valid for that message (like enums defined within the message)
"""
alias __MODULE__
alias Pbuf.Protoc
alias Pbuf.Protoc.{OneOf, Enumeration}
@enforce_keys [:package, :namespace, :enums, :maps, :oneofs, :version]
defstruct @enforce_keys
@type t :: %Context{
version: 2 | 3,
# The package as presented in the .proto file. We keep this around because
# references within the description use it everywhere. When looking for an
# enumeration (and maybe more) we can just use the reference as-is for our
# lookup, rather than having to convert it to the proper elixir namespace
package: String.t,
# The package converted to a Elixir namespace (pascal case)
namespace: String.t,
# Map name to {key type, value type}
maps: %{optional(String.t) => {any, any}},
# Enumertions in this file (initially global, then type-embedded)
enums: [Enumeration.t],
# OneOf index to our OneOf type
oneofs: %{optional(non_neg_integer) => OneOf.t}
}
@spec new(Protoc.proto_file) :: t
def new(%{package: package} = input) do
package = case package == nil do
true -> ""
false -> package <> "."
end
%Context{
maps: %{}, # only exists at the message level
oneofs: %{}, # only exists at the message level
package: package,
namespace: namespace(package),
version: version(input.syntax),
enums: extract_enums(input.enum_type, package)
}
end
# Expand the file context with message-specific information
@spec message(t, Protoc.proto_message) :: {t, %{optional(String.t) => Enumeration.t}}
def message(context, desc) do
package = context.package <> "." <> desc.name
enums = extract_enums(desc.enum_type, package)
context = %Context{context |
maps: maps(desc),
oneofs: oneofs(desc),
enums: enums ++ context.enums,
}
{context, enums}
end
# Expand the message-specific context with additional field information. This
# is needed to finish loading up our oneof information.
@spec fields(t, [any]) :: t
def fields(context, fields) do
oneofs = context.oneofs
oneofs = Enum.reduce(fields, oneofs, fn %{oneof_index: index} = field, oneofs ->
case index == nil do
true -> oneofs
false ->
oneof = Map.get(oneofs, index)
Map.put(oneofs, index, OneOf.add(oneof, field))
end
end)
%Context{context | oneofs: oneofs}
end
defp version("proto3") do
3
end
defp version(_) do
2
end
@spec namespace(nil | String.t) :: String.t
defp namespace(nil) do
""
end
defp namespace(package) do
package
|> String.split(".")
|> Enum.map(&Protoc.capitalize_first/1)
|> Enum.join(".")
end
defp extract_enums(values, package) do
namespace = namespace(package)
Enum.reduce(values, [], fn v, acc ->
e = Enumeration.new(v, namespace)
[e | acc]
end)
end
defp maps(desc) do
desc.nested_type
|> Enum.filter(&(&1.options))
|> Enum.reduce(%{}, fn type, maps ->
case type.options.map_entry do
false -> maps
true ->
{key_field, value_field} = extract_map_info(type.field)
Map.put(maps, type.name, {key_field, value_field})
end
end)
end
defp extract_map_info([a, b]) do
case a.name == "key" do
true -> {a, b}
false -> {b, a}
end
end
defp oneofs(desc) do
desc.oneof_decl
|> Enum.with_index()
|> Enum.into(%{}, fn {oneof, index} ->
{index, OneOf.new(oneof.name)}
end)
end
end