-module(scamper). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/scamper.gleam"). -export([new/3, restore/6, transition/2, can_transition/2, current_state/1, current_context/1, is_final/1, available_events/1, history/1, elapsed/1, get_config/1, created_at/1, entered_at/1]). -export_type([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( " Scamper — Type-safe finite state machine library for Gleam.\n" "\n" " Generic over `Machine(state, context, event)`. Build configs with the\n" " pipeline operator, then create machines and transition them with events.\n" "\n" " ```gleam\n" " let cfg =\n" " config.new(timestamp_fn)\n" " |> config.add_transition(from: Idle, on: Start, to: Running)\n" " |> config.add_transition(from: Running, on: Complete, to: Done)\n" " |> config.set_final_states([Done])\n" "\n" " let machine = scamper.new(cfg, Idle, initial_context)\n" " let assert Ok(machine) = scamper.transition(machine, Start)\n" " ```\n" ). -opaque machine(HTV, HTW, HTX) :: {machine, scamper@config:config(HTV, HTW, HTX), HTV, HTW, list(scamper@history:transition_record(HTV, HTX, HTW)), integer(), integer()}. -file("src/scamper.gleam", 37). ?DOC(" Create a new state machine with the given configuration, initial state, and context.\n"). -spec new(scamper@config:config(HTY, HTZ, HUA), HTY, HTZ) -> machine(HTY, HTZ, HUA). new(Config, Initial_state, Context) -> Now = scamper@config:get_timestamp(Config), {machine, Config, Initial_state, Context, [], Now, Now}. -file("src/scamper.gleam", 55). ?DOC( " Restore a machine from serialized components.\n" " Used by the serialization module to reconstruct a machine.\n" ). -spec restore( scamper@config:config(HUH, HUI, HUJ), HUH, HUI, list(scamper@history:transition_record(HUH, HUJ, HUI)), integer(), integer() ) -> machine(HUH, HUI, HUJ). restore(Config, State, Context, History_records, Created_at, Entered_at) -> {machine, Config, State, Context, History_records, Created_at, Entered_at}. -file("src/scamper.gleam", 76). ?DOC( " Attempt to transition the machine by processing an event.\n" " Returns a new machine on success, or a TransitionError on failure.\n" " The input machine is never modified.\n" ). -spec transition(machine(HUU, HUV, HUW), HUW) -> {ok, machine(HUU, HUV, HUW)} | {error, scamper@error:transition_error(HUU, HUW)}. transition(Machine, Event) -> _pipe = scamper@transition:execute( erlang:element(3, Machine), erlang:element(4, Machine), Event, erlang:element(2, Machine), erlang:element(5, Machine) ), gleam@result:map( _pipe, fun(R) -> {machine, erlang:element(2, Machine), erlang:element(2, R), erlang:element(3, R), erlang:element(4, R), erlang:element(6, Machine), erlang:element(5, R)} end ). -file("src/scamper.gleam", 100). ?DOC( " Check whether a transition is possible for the given event\n" " without actually executing it. Does not run callbacks or invariants.\n" ). -spec can_transition(machine(any(), any(), HVJ), HVJ) -> boolean(). can_transition(Machine, Event) -> scamper@transition:can_execute( erlang:element(3, Machine), Event, erlang:element(4, Machine), erlang:element(2, Machine) ). -file("src/scamper.gleam", 108). ?DOC(" Get the current state of the machine.\n"). -spec current_state(machine(HVN, any(), any())) -> HVN. current_state(Machine) -> erlang:element(3, Machine). -file("src/scamper.gleam", 113). ?DOC(" Get the current context of the machine.\n"). -spec current_context(machine(any(), HVU, any())) -> HVU. current_context(Machine) -> erlang:element(4, Machine). -file("src/scamper.gleam", 118). ?DOC(" Check whether the machine is in a final (terminal) state.\n"). -spec is_final(machine(any(), any(), any())) -> boolean(). is_final(Machine) -> gleam@list:contains( scamper@config:get_final_states(erlang:element(2, Machine)), erlang:element(3, Machine) ). -file("src/scamper.gleam", 124). ?DOC( " Get the list of events that have at least one matching transition rule\n" " from the current state. Does not evaluate guards.\n" ). -spec available_events(machine(any(), any(), HWH)) -> list(HWH). available_events(Machine) -> scamper@transition:available_events( erlang:element(3, Machine), erlang:element(2, Machine) ). -file("src/scamper.gleam", 129). ?DOC(" Get the full transition history (newest first).\n"). -spec history(machine(HWM, HWN, HWO)) -> list(scamper@history:transition_record(HWM, HWO, HWN)). history(Machine) -> erlang:element(5, Machine). -file("src/scamper.gleam", 137). ?DOC( " Get milliseconds elapsed since the last transition\n" " (or since creation if no transitions have occurred).\n" ). -spec elapsed(machine(any(), any(), any())) -> integer(). elapsed(Machine) -> scamper@config:get_timestamp(erlang:element(2, Machine)) - erlang:element( 7, Machine ). -file("src/scamper.gleam", 142). ?DOC(" Get the machine's configuration.\n"). -spec get_config(machine(HXC, HXD, HXE)) -> scamper@config:config(HXC, HXD, HXE). get_config(Machine) -> erlang:element(2, Machine). -file("src/scamper.gleam", 149). ?DOC(" Get the timestamp when the machine was created.\n"). -spec created_at(machine(any(), any(), any())) -> integer(). created_at(Machine) -> erlang:element(6, Machine). -file("src/scamper.gleam", 154). ?DOC(" Get the timestamp when the current state was entered.\n"). -spec entered_at(machine(any(), any(), any())) -> integer(). entered_at(Machine) -> erlang:element(7, Machine).