-module(scamper@serialization). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/scamper/serialization.gleam"). -export([serialize/4, deserialize/5]). -export_type([serialization_error/0, decoded_machine/3]). -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( " JSON serialization and deserialization for scamper FSMs.\n" "\n" " Users must provide encoder/decoder functions for their custom\n" " state, context, and event types.\n" ). -type serialization_error() :: {json_encode_error, binary()} | {json_decode_error, binary()}. -type decoded_machine(IJC, IJD, IJE) :: {decoded_machine, IJC, IJD, integer(), integer(), list(scamper@history:transition_record(IJC, IJE, IJD))}. -file("src/scamper/serialization.gleam", 43). -spec encode_record( scamper@history:transition_record(IJL, IJM, IJN), fun((IJL) -> gleam@json:json()), fun((IJN) -> gleam@json:json()), fun((IJM) -> gleam@json:json()) ) -> gleam@json:json(). encode_record(Record, State_encoder, Context_encoder, Event_encoder) -> gleam@json:object( [{<<"from"/utf8>>, State_encoder(erlang:element(2, Record))}, {<<"event"/utf8>>, Event_encoder(erlang:element(3, Record))}, {<<"to"/utf8>>, State_encoder(erlang:element(4, Record))}, {<<"timestamp"/utf8>>, gleam@json:int(erlang:element(5, Record))}, {<<"context_snapshot"/utf8>>, gleam@json:nullable(erlang:element(6, Record), Context_encoder)}] ). -file("src/scamper/serialization.gleam", 21). ?DOC( " Serialize a machine's state to a JSON string.\n" " User must provide encoder functions for state, context, and event types.\n" ). -spec serialize( scamper:machine(IJF, IJG, IJH), fun((IJF) -> gleam@json:json()), fun((IJG) -> gleam@json:json()), fun((IJH) -> gleam@json:json()) ) -> binary(). serialize(Machine, State_encoder, Context_encoder, Event_encoder) -> History_json = begin _pipe = scamper:history(Machine), gleam@json:array( _pipe, fun(Record) -> encode_record( Record, State_encoder, Context_encoder, Event_encoder ) end ) end, _pipe@1 = gleam@json:object( [{<<"state"/utf8>>, State_encoder(scamper:current_state(Machine))}, {<<"context"/utf8>>, Context_encoder(scamper:current_context(Machine))}, {<<"created_at"/utf8>>, gleam@json:int(scamper:created_at(Machine))}, {<<"entered_at"/utf8>>, gleam@json:int(scamper:entered_at(Machine))}, {<<"history"/utf8>>, History_json}] ), gleam@json:to_string(_pipe@1). -file("src/scamper/serialization.gleam", 110). -spec build_machine_decoder( gleam@dynamic@decode:decoder(IKF), gleam@dynamic@decode:decoder(IKH), gleam@dynamic@decode:decoder(scamper@history:transition_record(IKF, IKJ, IKH)) ) -> gleam@dynamic@decode:decoder(decoded_machine(IKF, IKH, IKJ)). build_machine_decoder(State_decoder, Context_decoder, Record_decoder) -> gleam@dynamic@decode:field( <<"state"/utf8>>, State_decoder, fun(State) -> gleam@dynamic@decode:field( <<"context"/utf8>>, Context_decoder, fun(Context) -> gleam@dynamic@decode:field( <<"created_at"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_int/1}, fun(Created_at) -> gleam@dynamic@decode:field( <<"entered_at"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_int/1}, fun(Entered_at) -> gleam@dynamic@decode:field( <<"history"/utf8>>, gleam@dynamic@decode:list( Record_decoder ), fun(History) -> gleam@dynamic@decode:success( {decoded_machine, State, Context, Created_at, Entered_at, History} ) end ) end ) end ) end ) end ). -file("src/scamper/serialization.gleam", 129). -spec build_record_decoder( gleam@dynamic@decode:decoder(IKS), gleam@dynamic@decode:decoder(IKU), gleam@dynamic@decode:decoder(IKW) ) -> gleam@dynamic@decode:decoder(scamper@history:transition_record(IKS, IKW, IKU)). build_record_decoder(State_decoder, Context_decoder, Event_decoder) -> gleam@dynamic@decode:field( <<"from"/utf8>>, State_decoder, fun(From) -> gleam@dynamic@decode:field( <<"event"/utf8>>, Event_decoder, fun(Event) -> gleam@dynamic@decode:field( <<"to"/utf8>>, State_decoder, fun(To) -> gleam@dynamic@decode:field( <<"timestamp"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_int/1}, fun(Timestamp) -> gleam@dynamic@decode:field( <<"context_snapshot"/utf8>>, gleam@dynamic@decode:optional( Context_decoder ), fun(Context_snapshot) -> gleam@dynamic@decode:success( {transition_record, From, Event, To, Timestamp, Context_snapshot} ) end ) end ) end ) end ) end ). -file("src/scamper/serialization.gleam", 63). ?DOC( " Deserialize a machine from a JSON string and a config.\n" " User must provide decoder functions for state, context, and event types.\n" ). -spec deserialize( binary(), scamper@config:config(IJR, IJS, IJT), gleam@dynamic@decode:decoder(IJR), gleam@dynamic@decode:decoder(IJS), gleam@dynamic@decode:decoder(IJT) ) -> {ok, scamper:machine(IJR, IJS, IJT)} | {error, serialization_error()}. deserialize(Data, Config, State_decoder, Context_decoder, Event_decoder) -> Record_decoder = build_record_decoder( State_decoder, Context_decoder, Event_decoder ), Machine_decoder = build_machine_decoder( State_decoder, Context_decoder, Record_decoder ), _pipe = gleam@json:parse(Data, Machine_decoder), _pipe@1 = gleam@result:map_error(_pipe, fun(Err) -> case Err of unexpected_end_of_input -> {json_decode_error, <<"Unexpected end of input"/utf8>>}; {unexpected_byte, B} -> {json_decode_error, <<"Unexpected byte: "/utf8, B/binary>>}; {unexpected_sequence, S} -> {json_decode_error, <<"Unexpected sequence: "/utf8, S/binary>>}; {unable_to_decode, _} -> {json_decode_error, <<"Unable to decode JSON"/utf8>>} end end), gleam@result:map( _pipe@1, fun(Decoded) -> scamper:restore( Config, erlang:element(2, Decoded), erlang:element(3, Decoded), erlang:element(6, Decoded), erlang:element(4, Decoded), erlang:element(5, Decoded) ) end ).