Current section

Files

Jump to
distribute src distribute@codec@variant.erl
Raw

src/distribute@codec@variant.erl

-module(distribute@codec@variant).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/distribute/codec/variant.gleam").
-export([new/0, add/6, unit/5, build/1]).
-export_type([variant_encode_outcome/0, variant_strategy/1, variant_builder/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type variant_encode_outcome() :: {encoded, bitstring()} |
not_this_variant |
{variant_encode_failed, distribute@codec:encode_error()}.
-type variant_strategy(JCK) :: {variant_strategy,
integer(),
binary(),
fun((JCK) -> variant_encode_outcome()),
fun((bitstring()) -> {ok, {JCK, bitstring()}} |
{error, distribute@codec:decode_error()})}.
-opaque variant_builder(JCL) :: {variant_builder, list(variant_strategy(JCL))}.
-file("src/distribute/codec/variant.gleam", 40).
?DOC(" Create a new builder for an ADT codec.\n").
-spec new() -> variant_builder(any()).
new() ->
{variant_builder, []}.
-file("src/distribute/codec/variant.gleam", 48).
?DOC(
" Add a variant with a payload to the codec.\n"
" id: unique identifier (0-255). name: for error messages.\n"
" inner: codec for the payload. wrap: ADT constructor.\n"
" unwrap: extract payload or return Error(Nil) if wrong variant.\n"
).
-spec add(
variant_builder(JCO),
integer(),
binary(),
distribute@codec:codec(JCQ),
fun((JCQ) -> JCO),
fun((JCO) -> {ok, JCQ} | {error, nil})
) -> variant_builder(JCO).
add(Builder, Id, Name, Inner, Wrap, Unwrap) ->
Strategy = {variant_strategy, Id, Name, fun(Value) -> case Unwrap(Value) of
{ok, Payload} ->
case (erlang:element(2, Inner))(Payload) of
{ok, Bits} ->
{encoded, gleam@bit_array:append(<<Id:8>>, Bits)};
{error, E} ->
{variant_encode_failed, E}
end;
{error, _} ->
not_this_variant
end end, fun(Data) -> case Data of
<<Tag:8, Rest/bitstring>> when Tag =:= Id ->
case (erlang:element(4, Inner))(Rest) of
{ok, {Val, Remaining}} ->
{ok, {Wrap(Val), Remaining}};
{error, E@1} ->
{error, E@1}
end;
_ ->
{error, {tag_mismatch, Name, <<"unknown"/utf8>>}}
end end},
{variant_builder, [Strategy | erlang:element(2, Builder)]}.
-file("src/distribute/codec/variant.gleam", 87).
?DOC(" Add a variant without a payload (unit/constant).\n").
-spec unit(
variant_builder(JCV),
integer(),
binary(),
JCV,
fun((JCV) -> boolean())
) -> variant_builder(JCV).
unit(Builder, Id, Name, Value, Match) ->
Strategy = {variant_strategy, Id, Name, fun(Val) -> case Match(Val) of
true ->
{encoded, <<Id:8>>};
false ->
not_this_variant
end end, fun(Data) -> case Data of
<<Tag:8, Rest/bitstring>> when Tag =:= Id ->
{ok, {Value, Rest}};
_ ->
{error, {tag_mismatch, Name, <<"unknown"/utf8>>}}
end end},
{variant_builder, [Strategy | erlang:element(2, Builder)]}.
-file("src/distribute/codec/variant.gleam", 172).
-spec find_encoder(list(variant_strategy(JDB)), JDB) -> variant_encode_outcome().
find_encoder(Strategies, Value) ->
case Strategies of
[] ->
not_this_variant;
[S | Rest] ->
case (erlang:element(4, S))(Value) of
not_this_variant ->
find_encoder(Rest, Value);
Outcome ->
Outcome
end
end.
-file("src/distribute/codec/variant.gleam", 186).
-spec validate_strategies(list(variant_strategy(any()))) -> nil.
validate_strategies(Strategies) ->
Ids = gleam@list:map(Strategies, fun(S) -> erlang:element(2, S) end),
case gleam@list:all(Ids, fun(Id) -> (Id >= 0) andalso (Id =< 255) end) of
true ->
nil;
false ->
erlang:error(#{gleam_error => panic,
message => <<"Variant codec: variant id out of range, must be 0-255 (the tag is a single wire byte)"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"distribute/codec/variant"/utf8>>,
function => <<"validate_strategies"/utf8>>,
line => 191})
end,
case begin
_pipe = gleam@list:unique(Ids),
erlang:length(_pipe)
end
=:= erlang:length(Ids) of
true ->
nil;
false ->
erlang:error(#{gleam_error => panic,
message => <<"Variant codec: Duplicate IDs detected in builder"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"distribute/codec/variant"/utf8>>,
function => <<"validate_strategies"/utf8>>,
line => 195})
end.
-file("src/distribute/codec/variant.gleam", 121).
?DOC(
" Finalize the builder and return a Codec(a).\n"
" Panics if duplicate IDs are found.\n"
).
-spec build(variant_builder(JCY)) -> distribute@codec:codec(JCY).
build(Builder) ->
Strategies = lists:reverse(erlang:element(2, Builder)),
validate_strategies(Strategies),
Encoder = fun(Value) -> case find_encoder(Strategies, Value) of
{encoded, Bits} ->
{ok, Bits};
{variant_encode_failed, E} ->
{error, E};
not_this_variant ->
{error,
{encode_failed,
<<"No variant matched for encoding ADT value"/utf8>>}}
end end,
Decoder_dict = gleam@list:fold(
Strategies,
maps:new(),
fun(Acc, S) -> gleam@dict:insert(Acc, erlang:element(2, S), S) end
),
Sized_decoder = fun(Data) -> case Data of
<<Tag:8, _/bitstring>> ->
case gleam_stdlib:map_get(Decoder_dict, Tag) of
{ok, Strategy} ->
(erlang:element(5, Strategy))(Data);
{error, _} ->
{error,
{tag_mismatch,
<<"one of "/utf8,
(begin
_pipe = gleam@list:map(
Strategies,
fun(S@1) ->
erlang:element(3, S@1)
end
),
_pipe@1 = gleam@list:unique(_pipe),
gleam@list:fold(
_pipe@1,
<<""/utf8>>,
fun(Acc@1, N) -> case Acc@1 of
<<""/utf8>> ->
N;
_ ->
<<<<Acc@1/binary,
", "/utf8>>/binary,
N/binary>>
end end
)
end)/binary>>,
erlang:integer_to_binary(Tag)}}
end;
_ ->
{error, {insufficient_data, <<"missing variant tag"/utf8>>}}
end end,
{codec, Encoder, distribute@codec:to_decoder(Sized_decoder), Sized_decoder}.