Current section

Files

Jump to
protobuf lib protobuf dsl typespecs.ex
Raw

lib/protobuf/dsl/typespecs.ex

defmodule Protobuf.DSL.Typespecs do
@moduledoc false
alias Protobuf.{FieldProps, MessageProps}
@spec quoted_enum_typespec(MessageProps.t()) :: Macro.t()
def quoted_enum_typespec(%MessageProps{field_props: field_props}) do
atom_specs =
field_props
|> Enum.sort_by(fn {fnum, _prop} -> fnum end)
|> Enum.map(fn {_fnum, %FieldProps{name_atom: name}} -> name end)
|> union_specs()
quote do
integer() | unquote(atom_specs)
end
end
@spec quoted_message_typespec(MessageProps.t()) :: Macro.t()
def quoted_message_typespec(%MessageProps{syntax: syntax} = message_props) do
regular_fields =
for {_fnum, %FieldProps{oneof: nil} = prop} <- message_props.field_props,
do: {prop.name_atom, field_prop_to_spec(syntax, prop)}
oneof_fields =
for {field_name, fnum} <- message_props.oneof do
possible_fields =
for {_fnum, %FieldProps{oneof: ^fnum} = prop} <- message_props.field_props, do: prop
{field_name, oneof_spec(syntax, possible_fields)}
end
extension_fields =
case message_props.extension_range do
[_ | _] -> [{:__pb_extensions__, quote(do: map())}]
_other -> []
end
unknown_fields = [
{:__unknown_fields__,
quote(
do: [
Protobuf.unknown_field()
]
)}
]
field_specs = regular_fields ++ oneof_fields ++ extension_fields ++ unknown_fields
quote do: %__MODULE__{unquote_splicing(field_specs)}
end
defp oneof_spec(syntax, possible_oneof_fields) do
possible_oneof_fields
|> Enum.map(fn prop -> {prop.name_atom, field_prop_to_spec(syntax, prop)} end)
|> Kernel.++([nil])
|> union_specs()
end
defp field_prop_to_spec(_syntax, %FieldProps{map?: true, type: map_mod} = prop) do
if not Code.ensure_loaded?(map_mod) do
raise "module #{inspect(map_mod)} was not loaded, but was expected to be since it's used as a map entry"
end
map_props = map_mod.__message_props__()
key_spec = scalar_type_to_spec(map_props.field_props[map_props.field_tags.key].type)
value_prop = map_props.field_props[map_props.field_tags.value]
value_spec = type_to_spec(value_prop.type, value_prop)
value_spec = if prop.embedded?, do: quote(do: unquote(value_spec) | nil), else: value_spec
quote do: %{optional(unquote(key_spec)) => unquote(value_spec)}
end
defp field_prop_to_spec(syntax, %FieldProps{type: type} = prop) do
spec = type_to_spec(type, prop)
cond do
prop.repeated? ->
quote do: [unquote(spec)]
prop.embedded? or (prop.optional? and is_nil(prop.oneof) and syntax != :proto3) or
prop.proto3_optional? ->
quote do: unquote(spec) | nil
true ->
spec
end
end
defp type_to_spec({:enum, enum_mod}, _prop), do: quote(do: unquote(enum_mod).t())
defp type_to_spec(mod, %FieldProps{embedded?: true}), do: quote(do: unquote(mod).t())
defp type_to_spec(:group, _prop), do: quote(do: term())
defp type_to_spec(type, _prop), do: scalar_type_to_spec(type)
defp scalar_type_to_spec(:string), do: quote(do: String.t())
defp scalar_type_to_spec(:bytes), do: quote(do: binary())
defp scalar_type_to_spec(:bool), do: quote(do: boolean())
defp scalar_type_to_spec(type)
when type in [:int32, :int64, :sint32, :sint64, :sfixed32, :sfixed64],
do: quote(do: integer())
defp scalar_type_to_spec(type)
when type in [:uint32, :uint64, :fixed32, :fixed64],
do: quote(do: non_neg_integer())
defp scalar_type_to_spec(type) when type in [:float, :double],
do: quote(do: float() | :infinity | :negative_infinity | :nan)
# We do this because the :| operator is left-associative, so if we just map and build "acc |
# spec" then we end up with "((foo | bar) | baz) | bong". By building it from right to left, it
# works just fine.
defp union_specs(specs) do
Enum.reduce(Enum.reverse(specs), fn spec, acc ->
quote do: unquote(spec) | unquote(acc)
end)
end
end