Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion lib/protobuf/any.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
defmodule Protobuf.Any do

@moduledoc """
Provides functions for working with the `google.protobuf.Any` type.
"""
Expand Down Expand Up @@ -28,6 +27,60 @@ defmodule Protobuf.Any do
}
end

@doc """
Unpacks a `Google.Protobuf.Any` message using a custom type provider.

The type provider module must implement the `Protobuf.Any.TypeProvider` behaviour,
which defines how to convert type URLs to their corresponding message modules.

## Example

defmodule MyApp.AnyTypeProvider do
@behaviour Protobuf.Any.TypeProvider

def to_module("type.googleapis.com/google.protobuf.Duration"), do: {:ok, Google.Protobuf.Duration}
def to_module("type.googleapis.com/myapp.events.UserCreated"), do: {:ok, MyApp.Events.UserCreated}
def to_module("myapp.internal/myapp.events.OrderPlaced"), do: {:ok, MyApp.Events.OrderPlaced}
def to_module(_), do: {:error, "Unknown type_url"}
end

any = %Google.Protobuf.Any{
type_url: "type.googleapis.com/myapp.events.UserCreated",
value: <<...>>
}
Protobuf.Any.unpack(any, MyApp.AnyTypeProvider)
#=> {:ok, %MyApp.Events.UserCreated{...}}
"""
@spec unpack(Google.Protobuf.Any.t(), module()) ::
{:ok, struct()} | {:error, reason :: any()}
def unpack(%Google.Protobuf.Any{type_url: type_url, value: value}, type_provider) do
with {:ok, module} <- resolve_module(type_provider, type_url) do
decode(module, value)
end
end

defp resolve_module(type_provider, type_url) do
case type_provider.to_module(type_url) do
{:ok, module} when is_atom(module) ->
{:ok, module}

{:ok, other} ->
{:error,
ArgumentError.exception(
"expected type provider to return an atom module, got: #{inspect(other)}"
)}

{:error, _} = error ->
error
end
end

defp decode(module, value) do
{:ok, module.decode(value)}
rescue
error -> {:error, error}
end

@doc false
@spec type_url_to_module(String.t()) :: module()
def type_url_to_module(type_url) when is_binary(type_url) do
Expand Down
32 changes: 32 additions & 0 deletions lib/protobuf/any/type_provider.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule Protobuf.Any.TypeProvider do
@moduledoc """
Behaviour for resolving type URLs to Protobuf message modules for `Google.Protobuf.Any`.

Implementations of this behaviour define how to convert a type URL to its corresponding module,
allowing customization of prefix handling and message routing.

## Example

defmodule MyApp.AnyTypeProvider do
@behaviour Protobuf.Any.TypeProvider

def to_module("type.googleapis.com/google.protobuf.Duration"), do: {:ok, Google.Protobuf.Duration}
def to_module("type.googleapis.com/myapp.events.UserCreated"), do: {:ok, MyApp.Events.UserCreated}
def to_module("myapp.internal/myapp.events.OrderPlaced"), do: {:ok, MyApp.Events.OrderPlaced}
def to_module(_), do: {:error, "Unknown type_url"}
end

Then use it with `Protobuf.Any.unpack/2`:

Protobuf.Any.unpack(any_message, MyApp.AnyTypeProvider)
#=> {:ok, decoded_struct}
"""

@doc """
Convert a type URL to its corresponding Protobuf message module.

Should return `{:ok, module}` if the type URL is recognized, or
`{:error, reason}` if it cannot be resolved.
"""
@callback to_module(type_url :: String.t()) :: {:ok, module()} | {:error, reason :: any()}
end
58 changes: 58 additions & 0 deletions test/protobuf/any_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,64 @@ defmodule Protobuf.AnyTest do
end
end

describe "unpack/2" do
test "unpacks a message from Any" do
message = %Google.Protobuf.Duration{seconds: 42}
any = Protobuf.Any.pack(message)

assert {:ok, unpacked} = Protobuf.Any.unpack(any, Protobuf.AnyTypeProviderSupport)
assert unpacked == message
end

test "returns error for unknown type_url" do
any = %Google.Protobuf.Any{
type_url: "custom.prefix/unknown.Type",
value: <<>>
}

assert {:error, "Unknown type_url"} =
Protobuf.Any.unpack(any, Protobuf.AnyTypeProviderSupport)
end

test "returns error for unmapped type_url" do
any = %Google.Protobuf.Any{
type_url: "type.googleapis.com/unknown.Message",
value: <<>>
}

assert {:error, "Unknown type_url"} =
Protobuf.Any.unpack(any, Protobuf.AnyTypeProviderSupport)
end

test "returns error when type provider returns a non-atom module" do
defmodule BadTypeProvider do
@behaviour Protobuf.Any.TypeProvider

def to_module(_type_url), do: {:ok, "not_an_atom"}
end

any = %Google.Protobuf.Any{
type_url: "type.googleapis.com/google.protobuf.Duration",
value: <<>>
}

assert {:error, %ArgumentError{message: message}} =
Protobuf.Any.unpack(any, BadTypeProvider)

assert message =~ "expected type provider to return an atom module"
end

test "returns error when decode fails" do
any = %Google.Protobuf.Any{
type_url: "type.googleapis.com/google.protobuf.Duration",
value: <<255, 255, 255>>
}

assert {:error, %Protobuf.DecodeError{}} =
Protobuf.Any.unpack(any, Protobuf.AnyTypeProviderSupport)
end
end

describe "type_url_to_module/1" do
test "returns the module for a valid type_url" do
assert Protobuf.Any.type_url_to_module("type.googleapis.com/google.protobuf.Duration") ==
Expand Down
3 changes: 2 additions & 1 deletion test/protobuf/protoc/cli_integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ defmodule Protobuf.Protoc.CLIIntegrationTest do
proto_path
])

assert [mod] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/my_type/timestamp_wrapper.pb.ex")
assert [mod] =
compile_file_and_clean_modules_on_exit("#{tmp_dir}/my_type/timestamp_wrapper.pb.ex")

assert mod == MyType.TimestampWrapper
assert Map.fetch!(mod.__message_props__().field_props, 1).type == Google.Protobuf.Timestamp
Expand Down
17 changes: 17 additions & 0 deletions test/support/any_type_provider.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Protobuf.AnyTypeProviderSupport do
@moduledoc false

@behaviour Protobuf.Any.TypeProvider

@mappings %{
"type.googleapis.com/google.protobuf.Duration" => Google.Protobuf.Duration,
"type.googleapis.com/test.Request.SomeGroup" => My.Test.Request.SomeGroup
}

def to_module(type_url) do
case Map.fetch(@mappings, type_url) do
{:ok, module} -> {:ok, module}
:error -> {:error, "Unknown type_url"}
end
end
end
8 changes: 6 additions & 2 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@ defmodule Protobuf.TestHelpers do
path = Path.join(dir, entry)

cond do
entry == filename -> {:ok, path}
entry == filename ->
{:ok, path}

File.dir?(path) ->
case find_file_in_dir(path, filename) do
{:ok, found_path} -> {:ok, found_path}
:not_found -> nil
end
true -> nil

true ->
nil
end
end)

Expand Down
Loading