-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(<>, Bits)}; {error, E} -> {variant_encode_failed, E} end; {error, _} -> not_this_variant end end, fun(Data) -> case Data of <> 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, <>}; false -> not_this_variant end end, fun(Data) -> case Data of <> 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 => <>, 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 => <>, 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 <> -> 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; _ -> <<<>/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}.