src/lightspeed@protocol.erl

-module(lightspeed@protocol).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lightspeed/protocol.gleam").
-export([hello/0, ref/1, is_current_hello/1, encode/1, decode/1, decode_error_to_string/1]).
-export_type([frame/0, decode_error/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(" Versioned protocol frame model.\n").

-type frame() :: {hello, binary(), integer()} |
    {event, binary(), binary(), binary()} |
    {diff, binary(), binary()} |
    {ack, binary()} |
    {failure, binary(), binary()}.

-type decode_error() :: empty_frame |
    {unknown_frame_tag, binary()} |
    {bad_field_count, binary(), integer(), integer()} |
    {invalid_version, binary()} |
    {unsupported_version, integer()} |
    {unsupported_protocol, binary()} |
    invalid_escape_sequence.

-file("src/lightspeed/protocol.gleam", 32).
?DOC(" Construct a protocol hello frame.\n").
-spec hello() -> frame().
hello() ->
    {hello, <<"lightspeed"/utf8>>, 1}.

-file("src/lightspeed/protocol.gleam", 37).
?DOC(" Return the frame reference when one exists.\n").
-spec ref(frame()) -> binary().
ref(Frame) ->
    case Frame of
        {hello, _, _} ->
            <<""/utf8>>;

        {event, Ref, _, _} ->
            Ref;

        {diff, Ref@1, _} ->
            Ref@1;

        {ack, Ref@2} ->
            Ref@2;

        {failure, Ref@3, _} ->
            Ref@3
    end.

-file("src/lightspeed/protocol.gleam", 48).
?DOC(" True when the frame is part of the current protocol.\n").
-spec is_current_hello(frame()) -> boolean().
is_current_hello(Frame) ->
    case Frame of
        {hello, Protocol, Version} ->
            (Protocol =:= <<"lightspeed"/utf8>>) andalso (Version =:= 1);

        _ ->
            false
    end.

-file("src/lightspeed/protocol.gleam", 189).
-spec escape_chars(list(binary()), binary()) -> binary().
escape_chars(Chars, Acc) ->
    case Chars of
        [] ->
            Acc;

        [Char | Rest] ->
            case Char of
                <<"\\"/utf8>> ->
                    escape_chars(Rest, <<Acc/binary, "\\\\"/utf8>>);

                <<"|"/utf8>> ->
                    escape_chars(Rest, <<Acc/binary, "\\|"/utf8>>);

                _ ->
                    escape_chars(Rest, <<Acc/binary, Char/binary>>)
            end
    end.

-file("src/lightspeed/protocol.gleam", 185).
-spec escape_field(binary()) -> binary().
escape_field(Value) ->
    escape_chars(gleam@string:to_graphemes(Value), <<""/utf8>>).

-file("src/lightspeed/protocol.gleam", 178).
-spec join_fields_loop(list(binary()), binary()) -> binary().
join_fields_loop(Fields, Acc) ->
    case Fields of
        [] ->
            Acc;

        [Field | Rest] ->
            join_fields_loop(
                Rest,
                <<<<Acc/binary, "|"/utf8>>/binary,
                    (escape_field(Field))/binary>>
            )
    end.

-file("src/lightspeed/protocol.gleam", 171).
-spec join_fields(list(binary())) -> binary().
join_fields(Fields) ->
    case Fields of
        [] ->
            <<""/utf8>>;

        [Field | Rest] ->
            join_fields_loop(Rest, escape_field(Field))
    end.

-file("src/lightspeed/protocol.gleam", 57).
?DOC(" Encode a frame into a transport-safe textual format.\n").
-spec encode(frame()) -> binary().
encode(Frame) ->
    case Frame of
        {hello, Protocol, Version} ->
            join_fields(
                [<<"hello"/utf8>>, Protocol, erlang:integer_to_binary(Version)]
            );

        {event, Ref, Name, Payload} ->
            join_fields([<<"event"/utf8>>, Ref, Name, Payload]);

        {diff, Ref@1, Html} ->
            join_fields([<<"diff"/utf8>>, Ref@1, Html]);

        {ack, Ref@2} ->
            join_fields([<<"ack"/utf8>>, Ref@2]);

        {failure, Ref@3, Reason} ->
            join_fields([<<"failure"/utf8>>, Ref@3, Reason])
    end.

-file("src/lightspeed/protocol.gleam", 163).
-spec bad_field_count(binary(), integer(), list(binary())) -> {ok, frame()} |
    {error, decode_error()}.
bad_field_count(Tag, Expected, Fields) ->
    {error, {bad_field_count, Tag, Expected, erlang:length(Fields)}}.

-file("src/lightspeed/protocol.gleam", 156).
-spec decode_failure(list(binary())) -> {ok, frame()} | {error, decode_error()}.
decode_failure(Fields) ->
    case Fields of
        [<<"failure"/utf8>>, Ref, Reason] ->
            {ok, {failure, Ref, Reason}};

        _ ->
            bad_field_count(<<"failure"/utf8>>, 3, Fields)
    end.

-file("src/lightspeed/protocol.gleam", 149).
-spec decode_ack(list(binary())) -> {ok, frame()} | {error, decode_error()}.
decode_ack(Fields) ->
    case Fields of
        [<<"ack"/utf8>>, Ref] ->
            {ok, {ack, Ref}};

        _ ->
            bad_field_count(<<"ack"/utf8>>, 2, Fields)
    end.

-file("src/lightspeed/protocol.gleam", 142).
-spec decode_diff(list(binary())) -> {ok, frame()} | {error, decode_error()}.
decode_diff(Fields) ->
    case Fields of
        [<<"diff"/utf8>>, Ref, Html] ->
            {ok, {diff, Ref, Html}};

        _ ->
            bad_field_count(<<"diff"/utf8>>, 3, Fields)
    end.

-file("src/lightspeed/protocol.gleam", 134).
-spec decode_event(list(binary())) -> {ok, frame()} | {error, decode_error()}.
decode_event(Fields) ->
    case Fields of
        [<<"event"/utf8>>, Ref, Name, Payload] ->
            {ok, {event, Ref, Name, Payload}};

        _ ->
            bad_field_count(<<"event"/utf8>>, 4, Fields)
    end.

-file("src/lightspeed/protocol.gleam", 115).
-spec decode_hello(list(binary())) -> {ok, frame()} | {error, decode_error()}.
decode_hello(Fields) ->
    case Fields of
        [<<"hello"/utf8>>, Protocol, Version_text] ->
            case gleam_stdlib:parse_int(Version_text) of
                {error, _} ->
                    {error, {invalid_version, Version_text}};

                {ok, Version} ->
                    case Protocol =:= <<"lightspeed"/utf8>> of
                        false ->
                            {error, {unsupported_protocol, Protocol}};

                        true ->
                            case Version =:= 1 of
                                true ->
                                    {ok, {hello, Protocol, Version}};

                                false ->
                                    {error, {unsupported_version, Version}}
                            end
                    end
            end;

        _ ->
            bad_field_count(<<"hello"/utf8>>, 3, Fields)
    end.

-file("src/lightspeed/protocol.gleam", 100).
-spec decode_fields(list(binary())) -> {ok, frame()} | {error, decode_error()}.
decode_fields(Fields) ->
    case Fields of
        [] ->
            {error, empty_frame};

        [Tag | _] ->
            case Tag of
                <<"hello"/utf8>> ->
                    decode_hello(Fields);

                <<"event"/utf8>> ->
                    decode_event(Fields);

                <<"diff"/utf8>> ->
                    decode_diff(Fields);

                <<"ack"/utf8>> ->
                    decode_ack(Fields);

                <<"failure"/utf8>> ->
                    decode_failure(Fields);

                _ ->
                    {error, {unknown_frame_tag, Tag}}
            end
    end.

-file("src/lightspeed/protocol.gleam", 205).
-spec split_chars(list(binary()), binary(), list(binary()), boolean()) -> {ok,
        list(binary())} |
    {error, decode_error()}.
split_chars(Chars, Current, Fields_rev, Escaped) ->
    case Chars of
        [] ->
            case Escaped of
                true ->
                    {error, invalid_escape_sequence};

                false ->
                    {ok, lists:reverse([Current | Fields_rev])}
            end;

        [Char | Rest] ->
            case Escaped of
                true ->
                    split_chars(
                        Rest,
                        <<Current/binary, Char/binary>>,
                        Fields_rev,
                        false
                    );

                false ->
                    case Char of
                        <<"\\"/utf8>> ->
                            split_chars(Rest, Current, Fields_rev, true);

                        <<"|"/utf8>> ->
                            split_chars(
                                Rest,
                                <<""/utf8>>,
                                [Current | Fields_rev],
                                false
                            );

                        _ ->
                            split_chars(
                                Rest,
                                <<Current/binary, Char/binary>>,
                                Fields_rev,
                                false
                            )
                    end
            end
    end.

-file("src/lightspeed/protocol.gleam", 201).
-spec split_fields(binary()) -> {ok, list(binary())} | {error, decode_error()}.
split_fields(Payload) ->
    split_chars(gleam@string:to_graphemes(Payload), <<""/utf8>>, [], false).

-file("src/lightspeed/protocol.gleam", 69).
?DOC(" Decode a textual frame payload.\n").
-spec decode(binary()) -> {ok, frame()} | {error, decode_error()}.
decode(Payload) ->
    case Payload of
        <<""/utf8>> ->
            {error, empty_frame};

        _ ->
            case split_fields(Payload) of
                {error, Error} ->
                    {error, Error};

                {ok, Fields} ->
                    decode_fields(Fields)
            end
    end.

-file("src/lightspeed/protocol.gleam", 81).
?DOC(" Convert decode errors to stable strings for logs and adapter errors.\n").
-spec decode_error_to_string(decode_error()) -> binary().
decode_error_to_string(Error) ->
    case Error of
        empty_frame ->
            <<"empty_frame"/utf8>>;

        {unknown_frame_tag, Tag} ->
            <<"unknown_frame_tag:"/utf8, Tag/binary>>;

        {bad_field_count, Tag@1, Expected, Actual} ->
            <<<<<<<<<<"bad_field_count:"/utf8, Tag@1/binary>>/binary, ":"/utf8>>/binary,
                        (erlang:integer_to_binary(Expected))/binary>>/binary,
                    ":"/utf8>>/binary,
                (erlang:integer_to_binary(Actual))/binary>>;

        {invalid_version, Version} ->
            <<"invalid_version:"/utf8, Version/binary>>;

        {unsupported_version, Version@1} ->
            <<"unsupported_version:"/utf8,
                (erlang:integer_to_binary(Version@1))/binary>>;

        {unsupported_protocol, Protocol} ->
            <<"unsupported_protocol:"/utf8, Protocol/binary>>;

        invalid_escape_sequence ->
            <<"invalid_escape_sequence"/utf8>>
    end.