-module(livery_grpc_codec). -moduledoc """ Protobuf encode/decode glue over a gpb-generated module. A gpb module (e.g. `helloworld_pb`, built from `proto/helloworld.proto`) exposes `encode_msg/2` and `decode_msg/2`. This module wraps them behind a stable interface so the server and client do not call gpb directly, and so an alternative codec (for example JSON for `application/grpc+json`) can be slotted in later behind the same calls. Encoding errors are surfaced as `{error, {encode|decode, Reason}}` rather than raised, so the caller can turn them into a gRPC `internal` status. """. -export([encode/3, decode/3]). -export([content_type/0, content_subtypes/0, is_grpc_content_type/1]). -export_type([proto_module/0, msg_name/0]). -type proto_module() :: module(). -type msg_name() :: atom(). -doc "Encode a message map/record to protobuf bytes.". -spec encode(proto_module(), msg_name(), map() | tuple()) -> {ok, binary()} | {error, {encode, term()}}. encode(Mod, MsgName, Msg) -> try {ok, iolist_to_binary(Mod:encode_msg(Msg, MsgName))} catch Class:Reason:Stack -> {error, {encode, {Class, Reason, Stack}}} end. -doc "Decode protobuf bytes into a message map (gpb maps mode).". -spec decode(proto_module(), msg_name(), binary()) -> {ok, map() | tuple()} | {error, {decode, term()}}. decode(Mod, MsgName, Bin) -> try {ok, Mod:decode_msg(Bin, MsgName)} catch Class:Reason:Stack -> {error, {decode, {Class, Reason, Stack}}} end. %%==================================================================== %% Content types %%==================================================================== -doc "The default gRPC content type.". -spec content_type() -> binary(). content_type() -> <<"application/grpc+proto">>. -doc "Recognised gRPC content-type values for proto framing.". -spec content_subtypes() -> [binary()]. content_subtypes() -> [<<"application/grpc">>, <<"application/grpc+proto">>]. -doc """ Whether a `content-type` header value names a gRPC proto request. A trailing parameter (e.g. `;charset=...`) is tolerated; the base type is matched. """. -spec is_grpc_content_type(binary() | undefined) -> boolean(). is_grpc_content_type(undefined) -> false; is_grpc_content_type(Value) -> Base = base_type(Value), lists:member(Base, content_subtypes()). -spec base_type(binary()) -> binary(). base_type(Value) -> case binary:split(Value, <<";">>) of [Base | _] -> trim(Base); [] -> Value end. -spec trim(binary()) -> binary(). trim(Bin) -> string:trim(Bin).