Skip to main content

src/livery_grpc_status.erl

-module(livery_grpc_status).
-moduledoc """
gRPC status codes and status metadata.

The 17 canonical codes (`0` `OK` through `16` `UNAUTHENTICATED`) as atoms,
with conversions to and from the integer used in the `grpc-status`
trailer/header. Also encodes and decodes the `grpc-message` value, which
uses gRPC's percent-encoding (only `%x20`-`%x7E` minus `%` pass through
literally; everything else is `%`-escaped).
""".

-export([code/1, name/1, is_code/1]).
-export([to_binary/1, from_binary/1]).
-export([encode_message/1, decode_message/1]).
-export([trailers/1, trailers/2]).

-export_type([code/0, status/0]).

-type status() ::
    ok
    | cancelled
    | unknown
    | invalid_argument
    | deadline_exceeded
    | not_found
    | already_exists
    | permission_denied
    | resource_exhausted
    | failed_precondition
    | aborted
    | out_of_range
    | unimplemented
    | internal
    | unavailable
    | data_loss
    | unauthenticated.

-type code() :: 0..16.

-define(IS_HEX(C),
    ((C >= $0 andalso C =< $9) orelse
        (C >= $a andalso C =< $f) orelse
        (C >= $A andalso C =< $F))
).

%%====================================================================
%% Code <-> name
%%====================================================================

-doc "The integer code for a status atom.".
-spec code(status()) -> code().
code(ok) -> 0;
code(cancelled) -> 1;
code(unknown) -> 2;
code(invalid_argument) -> 3;
code(deadline_exceeded) -> 4;
code(not_found) -> 5;
code(already_exists) -> 6;
code(permission_denied) -> 7;
code(resource_exhausted) -> 8;
code(failed_precondition) -> 9;
code(aborted) -> 10;
code(out_of_range) -> 11;
code(unimplemented) -> 12;
code(internal) -> 13;
code(unavailable) -> 14;
code(data_loss) -> 15;
code(unauthenticated) -> 16.

-doc """
The status atom for an integer code. Unknown integers map to `unknown`, so
a peer using a code we do not model still yields a usable value.
""".
-spec name(integer()) -> status().
name(0) -> ok;
name(1) -> cancelled;
name(2) -> unknown;
name(3) -> invalid_argument;
name(4) -> deadline_exceeded;
name(5) -> not_found;
name(6) -> already_exists;
name(7) -> permission_denied;
name(8) -> resource_exhausted;
name(9) -> failed_precondition;
name(10) -> aborted;
name(11) -> out_of_range;
name(12) -> unimplemented;
name(13) -> internal;
name(14) -> unavailable;
name(15) -> data_loss;
name(16) -> unauthenticated;
name(_) -> unknown.

-doc "Whether the term is a known status atom.".
-spec is_code(term()) -> boolean().
is_code(Atom) when is_atom(Atom) ->
    try
        _ = code(Atom),
        true
    catch
        error:function_clause -> false
    end;
is_code(_) ->
    false.

%%====================================================================
%% grpc-status header value
%%====================================================================

-doc "Render a status (atom or integer) as the `grpc-status` value.".
-spec to_binary(status() | code()) -> binary().
to_binary(Status) when is_atom(Status) ->
    integer_to_binary(code(Status));
to_binary(Code) when is_integer(Code) ->
    integer_to_binary(Code).

-doc "Parse a `grpc-status` value into a status atom.".
-spec from_binary(binary()) -> status().
from_binary(Bin) ->
    name(binary_to_integer(Bin)).

%%====================================================================
%% grpc-message percent-encoding
%%====================================================================

-doc """
Percent-encode a UTF-8 message for the `grpc-message` header. Printable
ASCII except `%` passes through; every other byte becomes `%XX`.
""".
-spec encode_message(binary()) -> binary().
encode_message(Msg) when is_binary(Msg) ->
    <<<<(encode_byte(B))/binary>> || <<B>> <= Msg>>.

-spec encode_byte(byte()) -> binary().
encode_byte($%) ->
    <<"%25">>;
encode_byte(B) when B >= 16#20, B =< 16#7E ->
    <<B>>;
encode_byte(B) ->
    <<$%, (hex(B bsr 4)), (hex(B band 16#0F))>>.

-spec hex(0..15) -> byte().
hex(N) when N < 10 -> $0 + N;
hex(N) -> $A + (N - 10).

-doc "Decode a percent-encoded `grpc-message` value.".
-spec decode_message(binary()) -> binary().
decode_message(Bin) ->
    decode_message(Bin, <<>>).

-spec decode_message(binary(), binary()) -> binary().
decode_message(<<$%, H, L, Rest/binary>>, Acc) when ?IS_HEX(H), ?IS_HEX(L) ->
    Byte = (unhex(H) bsl 4) bor unhex(L),
    decode_message(Rest, <<Acc/binary, Byte>>);
decode_message(<<B, Rest/binary>>, Acc) ->
    decode_message(Rest, <<Acc/binary, B>>);
decode_message(<<>>, Acc) ->
    Acc.

-spec unhex(byte()) -> 0..15.
unhex(C) when C >= $0, C =< $9 -> C - $0;
unhex(C) when C >= $a, C =< $f -> C - $a + 10;
unhex(C) when C >= $A, C =< $F -> C - $A + 10.

%%====================================================================
%% Trailers
%%====================================================================

-doc "Status trailers with no message.".
-spec trailers(status() | code()) -> [{binary(), binary()}].
trailers(Status) ->
    [{<<"grpc-status">>, to_binary(Status)}].

-doc "Status trailers carrying a (percent-encoded) message.".
-spec trailers(status() | code(), binary()) -> [{binary(), binary()}].
trailers(Status, <<>>) ->
    trailers(Status);
trailers(Status, Message) ->
    [
        {<<"grpc-status">>, to_binary(Status)},
        {<<"grpc-message">>, encode_message(Message)}
    ].