package helpers
import (
"context"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"log/slog"
"maps"
"os"
"reflect"
"slices"
"sort"
"strconv"
"strings"
"time"
"github.com/failsafe-go/failsafe-go"
"github.com/failsafe-go/failsafe-go/retrypolicy"
"golang.org/x/sync/errgroup"
"github.com/pancsta/asyncmachine-go/internal/utils"
am "github.com/pancsta/asyncmachine-go/pkg/machine"
ssam "github.com/pancsta/asyncmachine-go/pkg/states"
ampipe "github.com/pancsta/asyncmachine-go/pkg/states/pipes"
amtele "github.com/pancsta/asyncmachine-go/pkg/telemetry"
)
const (
EnvAmLogPrint = "AM_LOG_PRINT"
EnvAmHealthcheck = "AM_HEALTHCHECK"
EnvAmTestRunner = "AM_TEST_RUNNER"
EnvAmLogFull = "AM_LOG_FULL"
EnvAmLogSteps = "AM_LOG_STEPS"
EnvAmLogGraph = "AM_LOG_GRAPH"
EnvAmLogChecks = "AM_LOG_CHECKS"
EnvAmLogQueued = "AM_LOG_QUEUED"
EnvAmLogArgs = "AM_LOG_ARGS"
EnvAmLogWhen = "AM_LOG_WHEN"
EnvAmLogStateCtx = "AM_LOG_STATE_CTX"
EnvAmLogFile = "AM_LOG_FILE"
healthcheckInterval = 30 * time .Second
)
var ErrTestAutoDisable = errors .New ("feature disabled when AM_TEST_RUNNER" )
type (
S = am .S
A = am .A
Schema = am .Schema
)
func Add1Block (
ctx context .Context , mach am .Api , state string , args am .A ,
) am .Result {
if IsMulti (mach , state ) {
when := mach .WhenTicks (state , 2 , ctx )
res := mach .Add1 (state , args )
<-when
return res
}
if mach .Is1 (state ) {
return am .Executed
}
ctxWhen , cancel := context .WithCancel (ctx )
defer cancel ()
when := mach .WhenTicks (state , 1 , ctxWhen )
res := mach .Add1 (state , args )
if res == am .Canceled {
cancel ()
return res
}
select {
case <- when :
return am .Executed
case <- ctx .Done ():
return am .Canceled
}
}
func Add1Sync (
ctx context .Context , mach am .Api , state string , args am .A ,
) am .Result {
res := mach .Add1 (state , args )
switch res {
case am .Executed :
return res
case am .Canceled :
return res
default :
select {
case <- ctx .Done ():
return am .Canceled
case <- mach .WhenQueue (res ):
if mach .Is1 (state ) {
return am .Executed
}
return am .Canceled
}
}
}
func Add1Async (
ctx context .Context , mach am .Api , waitState string ,
addState string , args am .A ,
) am .Result {
ticks := 1
if IsMulti (mach , waitState ) {
ticks = 2
}
ctxWhen , cancel := context .WithCancel (ctx )
defer cancel ()
when := mach .WhenTicks (waitState , ticks , ctxWhen )
res := mach .Add1 (addState , args )
if res == am .Canceled {
cancel ()
return res
}
select {
case <- when :
return am .Executed
case <- ctx .Done ():
return am .Canceled
}
}
func IsMulti (mach am .Api , state string ) bool {
return mach .Schema ()[state ].Multi
}
func StatesToIndexes (allStates am .S , states am .S ) []int {
indexes := make ([]int , len (states ))
for i , state := range states {
indexes [i ] = slices .Index (allStates , state )
}
return indexes
}
func IndexesToStates (allStates am .S , indexes []int ) am .S {
states := make (am .S , len (indexes ))
for i , idx := range indexes {
if idx == -1 || idx >= len (allStates ) {
states [i ] = "unknown" + strconv .Itoa (i )
continue
}
states [i ] = allStates [idx ]
}
return states
}
func MachDebug (
mach am .Api , amDbgAddr string , logLvl am .LogLevel , stdout bool ,
semConfig *am .SemConfig ,
) error {
if IsTestRunner () {
return nil
}
semlog := mach .SemLogger ()
if stdout {
semlog .SetLevel (logLvl )
} else {
semlog .SetEmpty (logLvl )
}
if amDbgAddr == "" {
return nil
}
if semConfig .Steps {
semlog .EnableSteps (true )
}
if semConfig .Graph {
semlog .EnableGraph (true )
}
if semConfig .Can {
semlog .EnableCan (true )
}
if semConfig .Queued {
semlog .EnableQueued (true )
}
if semConfig .StateCtx {
semlog .EnableStateCtx (true )
}
if semConfig .Can {
semlog .EnableCan (true )
}
if semConfig .When {
semlog .EnableWhen (true )
}
if semConfig .Args {
semlog .EnableArgs (true )
}
err := amtele .TransitionsToDbg (mach , amDbgAddr )
if err != nil {
return err
}
if os .Getenv (EnvAmHealthcheck ) != "" {
Healthcheck (mach )
}
return nil
}
func SemConfigEnv (forceFull bool ) *am .SemConfig {
if os .Getenv (EnvAmLogFull ) != "" || forceFull {
return &am .SemConfig {
Steps : true ,
Graph : true ,
Can : true ,
Queued : true ,
StateCtx : true ,
When : true ,
Args : true ,
}
}
return &am .SemConfig {
Steps : os .Getenv (EnvAmLogSteps ) != "" ,
Graph : os .Getenv (EnvAmLogGraph ) != "" ,
Can : os .Getenv (EnvAmLogChecks ) != "" ,
Queued : os .Getenv (EnvAmLogQueued ) != "" ,
StateCtx : os .Getenv (EnvAmLogStateCtx ) != "" ,
When : os .Getenv (EnvAmLogWhen ) != "" ,
Args : os .Getenv (EnvAmLogArgs ) != "" ,
}
}
func MachDebugEnv (mach am .Api ) error {
amDbgAddr := os .Getenv (amtele .EnvAmDbgAddr )
logLvl := am .EnvLogLevel ("" )
stdout := os .Getenv (EnvAmLogPrint ) != ""
if amDbgAddr == "1" {
amDbgAddr = amtele .DbgAddr
}
return MachDebug (mach , amDbgAddr , logLvl , stdout , SemConfigEnv (false ))
}
func Healthcheck (mach am .Api ) {
if !mach .Has1 ("Healthcheck" ) {
return
}
go func () {
t := time .NewTicker (healthcheckInterval )
for {
select {
case <- t .C :
mach .Add1 (ssam .BasicStates .Healthcheck , nil )
case <- mach .Ctx ().Done ():
t .Stop ()
}
}
}()
}
func NewReqAdd (mach am .Api , states am .S , args am .A ) *MutRequest {
return NewMutRequest (mach , am .MutationAdd , states , args )
}
func NewReqAdd1 (mach am .Api , state string , args am .A ) *MutRequest {
return NewReqAdd (mach , am .S {state }, args )
}
func NewReqRemove (mach am .Api , states am .S , args am .A ) *MutRequest {
return NewMutRequest (mach , am .MutationRemove , states , args )
}
func NewReqRemove1 (mach am .Api , state string , args am .A ) *MutRequest {
return NewReqRemove (mach , am .S {state }, args )
}
type MutRequest struct {
Mach am .Api
MutType am .MutationType
States am .S
Args am .A
PolicyRetries int
PolicyDelay time .Duration
PolicyBackoff time .Duration
PolicyMaxDuration time .Duration
}
func NewMutRequest (
mach am .Api , mutType am .MutationType , states am .S , args am .A ,
) *MutRequest {
return &MutRequest {
Mach : mach ,
MutType : mutType ,
States : states ,
Args : args ,
PolicyRetries : 10 ,
PolicyDelay : 100 * time .Millisecond ,
PolicyBackoff : 5 * time .Second ,
PolicyMaxDuration : 5 * time .Second ,
}
}
func (r *MutRequest ) Clone (
mach am .Api , mutType am .MutationType , states am .S , args am .A ,
) *MutRequest {
return &MutRequest {
Mach : mach ,
MutType : mutType ,
States : states ,
Args : args ,
PolicyRetries : r .PolicyRetries ,
PolicyBackoff : r .PolicyBackoff ,
PolicyMaxDuration : r .PolicyMaxDuration ,
PolicyDelay : r .PolicyDelay ,
}
}
func (r *MutRequest ) Retries (retries int ) *MutRequest {
r .PolicyRetries = retries
return r
}
func (r *MutRequest ) Backoff (backoff time .Duration ) *MutRequest {
r .PolicyBackoff = backoff
return r
}
func (r *MutRequest ) MaxDuration (maxDuration time .Duration ) *MutRequest {
r .PolicyMaxDuration = maxDuration
return r
}
func (r *MutRequest ) Delay (delay time .Duration ) *MutRequest {
r .PolicyDelay = delay
return r
}
func (r *MutRequest ) Run (ctx context .Context ) (am .Result , error ) {
retry := retrypolicy .Builder [am .Result ]().
WithMaxDuration (r .PolicyMaxDuration ).
WithMaxRetries (r .PolicyRetries )
if r .PolicyBackoff != 0 {
retry = retry .WithBackoff (r .PolicyDelay , r .PolicyBackoff )
} else {
retry = retry .WithDelay (r .PolicyDelay )
}
res , err := failsafe .NewExecutor [am .Result ](retry .Build ()).WithContext (ctx ).
Get (r .get )
return res , err
}
func (r *MutRequest ) get () (am .Result , error ) {
res := r .Mach .Add (r .States , r .Args )
return res , ResultToErr (res )
}
func Wait (ctx context .Context , length time .Duration ) bool {
t := time .After (length )
select {
case <- ctx .Done ():
return false
case <- t :
return true
}
}
func Interval (
ctx context .Context , length time .Duration , interval time .Duration ,
fn func () bool ,
) error {
end := time .Now ().Add (length )
t := time .NewTicker (interval )
for {
select {
case <- ctx .Done ():
t .Stop ()
return ctx .Err ()
case <- t .C :
if time .Now ().After (end ) {
t .Stop ()
return nil
}
if !fn () {
t .Stop ()
return nil
}
}
}
}
func WaitForAll (
ctx context .Context , timeout time .Duration , chans ...<-chan struct {},
) error {
if len (chans ) == 0 {
return nil
}
if ctx .Err () != nil {
return ctx .Err ()
}
if IsDebug () {
timeout = 100 * timeout
}
t := time .After (timeout )
for _ , ch := range chans {
select {
case <- ctx .Done ():
return ctx .Err ()
case <- t :
return am .ErrTimeout
case <- ch :
}
}
return nil
}
func WaitForErrAll (
ctx context .Context , timeout time .Duration , mach am .Api ,
chans ...<-chan struct {},
) error {
if len (chans ) == 0 {
return nil
}
if ctx .Err () != nil {
return ctx .Err ()
}
if IsDebug () {
timeout = 100 * timeout
}
t := time .After (timeout )
whenErr := mach .WhenErr (ctx )
for _ , ch := range chans {
select {
case <- ctx .Done ():
return ctx .Err ()
case <- whenErr :
return fmt .Errorf ("%s: %w" , am .StateException , mach .Err ())
case <- t :
return am .ErrTimeout
case <- ch :
}
}
return nil
}
func WaitForAny (
ctx context .Context , timeout time .Duration , chans ...<-chan struct {},
) error {
if ctx .Err () != nil {
return ctx .Err ()
}
if IsDebug () {
timeout = 100 * timeout
}
t := time .After (timeout )
cases := make ([]reflect .SelectCase , 2 +len (chans ))
cases [0 ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (ctx .Done ()),
}
cases [1 ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (t ),
}
for i , ch := range chans {
cases [i +2 ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (ch ),
}
}
chosen , _ , _ := reflect .Select (cases )
switch chosen {
case 0 :
return ctx .Err ()
case 1 :
return am .ErrTimeout
default :
return nil
}
}
func WaitForErrAny (
ctx context .Context , timeout time .Duration , mach *am .Machine ,
chans ...<-chan struct {},
) error {
if ctx .Err () != nil {
return ctx .Err ()
}
if IsDebug () {
timeout = 100 * timeout
}
t := time .After (timeout )
predef := 3
cases := make ([]reflect .SelectCase , predef +len (chans ))
cases [0 ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (ctx .Done ()),
}
cases [1 ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (t ),
}
cases [2 ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (t ),
}
for i , ch := range chans {
cases [predef +i ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (ch ),
}
}
chosen , _ , _ := reflect .Select (cases )
switch chosen {
case 0 :
return ctx .Err ()
case 1 :
return am .ErrTimeout
case 2 :
return mach .Err ()
default :
return nil
}
}
func Activations (u uint64 ) int {
return int ((u + 1 ) / 2 )
}
func ExecAndClose (fn func ()) <-chan struct {} {
ch := make (chan struct {})
go func () {
fn ()
close (ch )
}()
return ch
}
func EnableDebugging (stdout bool ) {
if stdout {
_ = os .Setenv (am .EnvAmDebug , "2" )
} else {
_ = os .Setenv (am .EnvAmDebug , "1" )
}
_ = os .Setenv (amtele .EnvAmDbgAddr , "1" )
_ = os .Setenv (EnvAmLogFull , "1" )
SetEnvLogLevel (am .LogOps )
}
func SetEnvLogLevel (level am .LogLevel ) {
_ = os .Setenv (am .EnvAmLog , strconv .Itoa (int (level )))
}
func Implements (statesChecked , statesNeeded am .S ) error {
for _ , state := range statesNeeded {
if !slices .Contains (statesChecked , state ) {
return errors .New ("missing state: " + state )
}
}
return nil
}
func ArgsToLogMap (args interface {}, maxLen int ) map [string ]string {
if maxLen == 0 {
maxLen = max (4 , am .LogArgsMaxLen )
}
skipMaxLen := false
result := make (map [string ]string )
val := reflect .ValueOf (args ).Elem ()
if !val .IsValid () {
return result
}
typ := reflect .TypeOf (args ).Elem ()
for i := 0 ; i < val .NumField (); i ++ {
field := val .Field (i )
key := typ .Field (i ).Tag .Get ("log" )
if key == "" {
continue
}
switch v := field .Interface ().(type ) {
case string :
if v == "" {
continue
}
result [key ] = v
case []string :
if len (v ) == 0 {
continue
}
skipMaxLen = true
txt := ""
ii := 0
for _ , el := range v {
if reflect .ValueOf (v ).IsNil () {
continue
}
if txt != "" {
txt += ", "
}
txt += `"` + utils .TruncateStr (el , maxLen /2 ) + `"`
if ii >= maxLen /2 {
txt += fmt .Sprintf (" ... (%d more)" , len (v )-ii )
break
}
ii ++
}
if txt == "" {
continue
}
result [key ] = txt
case bool :
if !v {
continue
}
result [key ] = fmt .Sprintf ("%v" , v )
case []bool :
if len (v ) == 0 {
continue
}
result [key ] = fmt .Sprintf ("%v" , v )
result [key ] = strings .Trim (result [key ], "[]" )
case int :
if v == 0 {
continue
}
result [key ] = fmt .Sprintf ("%d" , v )
case []int :
if len (v ) == 0 {
continue
}
result [key ] = fmt .Sprintf ("%d" , v )
result [key ] = strings .Trim (result [key ], "[]" )
case time .Duration :
if v .Seconds () == 0 {
continue
}
result [key ] = v .String ()
case fmt .Stringer :
if reflect .ValueOf (v ).IsNil () {
continue
}
txt := v .String ()
if txt == "" {
continue
}
result [key ] = txt
default :
if field .Kind () != reflect .Slice {
continue
}
valLen := field .Len ()
skipMaxLen = true
txt := ""
ii := 0
for i := 0 ; i < valLen ; i ++ {
el := field .Index (i ).Interface ()
s , ok := el .(fmt .Stringer )
if ok && s .String () != "" {
if txt != "" {
txt += ", "
}
txt += `"` + utils .TruncateStr (s .String (), maxLen /2 ) + `"`
if i >= maxLen /2 {
txt += fmt .Sprintf (" ... (%d more)" , valLen -ii )
break
}
}
ii ++
}
if txt == "" {
continue
}
result [key ] = txt
}
result [key ] = strings .ReplaceAll (result [key ], "\n" , " " )
if !skipMaxLen && len (result [key ]) > maxLen {
result [key ] = utils .TruncateStr (result [key ], maxLen )
}
}
return result
}
func ArgsToArgs [T any ](src interface {}, dest T ) T {
srcVal := reflect .ValueOf (src ).Elem ()
destVal := reflect .ValueOf (dest ).Elem ()
for i := 0 ; i < srcVal .NumField (); i ++ {
srcField := srcVal .Field (i )
destField := destVal .FieldByName (srcVal .Type ().Field (i ).Name )
if destField .IsValid () && destField .CanSet () {
destField .Set (srcField )
}
}
return dest
}
func ArgsFromMap [T any ](args am .A , dest T ) T {
destVal := reflect .ValueOf (dest )
for field , val := range args {
destField := destVal .FieldByName (field )
if destField .IsValid () && destField .CanSet () {
valReflect := reflect .ValueOf (val )
if valReflect .Type ().AssignableTo (destField .Type ()) {
destField .Set (valReflect )
} else if valReflect .Type ().ConvertibleTo (destField .Type ()) {
destField .Set (valReflect .Convert (destField .Type ()))
}
}
}
return dest
}
func IsDebug () bool {
return os .Getenv (am .EnvAmDebug ) != "" && !IsTestRunner ()
}
func IsTelemetry () bool {
return os .Getenv (amtele .EnvAmDbgAddr ) != "" && !IsTestRunner ()
}
func IsTestRunner () bool {
return os .Getenv (EnvAmTestRunner ) != ""
}
func GroupWhen1 (
machs []am .Api , state string , ctx context .Context ,
) ([]<-chan struct {}, error ) {
for _ , mach := range machs {
if !mach .Has1 (state ) {
return nil , fmt .Errorf (
"%w: %s in machine %s" , am .ErrStateMissing , state , mach .Id ())
}
}
var chans []<-chan struct {}
for _ , mach := range machs {
chans = append (chans , mach .When1 (state , ctx ))
}
return chans , nil
}
func RemoveMulti (mach am .Api , state string ) am .HandlerFinal {
return func (_ *am .Event ) {
mach .Remove1 (state , nil )
}
}
func GetTransitionStates (
tx *am .Transition , index am .S ,
) (added am .S , removed am .S , touched am .S ) {
before := tx .TimeBefore
after := tx .TimeAfter
is := func (time am .Time , i int ) bool {
return time != nil && am .IsActiveTick (time .Tick (i ))
}
for i , name := range index {
if is (before , i ) && !is (after , i ) {
removed = append (removed , name )
} else if !is (before , i ) && is (after , i ) {
added = append (added , name )
} else if before != nil && before .Tick (i ) != after .Tick (i ) {
added = append (added , name )
}
}
touched = am .S {}
for _ , step := range tx .Steps {
if s := step .GetFromState (index ); s != "" {
touched = append (touched , s )
}
if s := step .GetToState (index ); s != "" {
touched = append (touched , s )
}
}
return added , removed , touched
}
func ResultToErr (result am .Result ) error {
switch result {
case am .Canceled :
return am .ErrCanceled
default :
return nil
}
}
type MachGroup []am .Api
func (g *MachGroup ) Is1 (state string ) bool {
if g == nil {
return false
}
for _ , m := range *g {
if m .Not1 (state ) {
return false
}
}
return true
}
func Pool (limit int ) *errgroup .Group {
g := &errgroup .Group {}
g .SetLimit (limit )
return g
}
type Cond struct {
Is S
Any []S
Any1 S
Not S
Clock am .Clock
}
func (c Cond ) String () string {
return fmt .Sprintf ("is: %s, any: %s, not: %s, clock: %v" ,
c .Is , c .Any1 , c .Not , c .Clock )
}
func (c Cond ) Check (mach am .Api ) bool {
if mach == nil {
return false
}
if !mach .Is (c .Is ) {
return false
}
if mach .Any1 (c .Not ...) {
return false
}
if len (c .Any1 ) > 0 && !mach .Any1 (c .Any1 ...) {
return false
}
if !mach .WasClock (c .Clock ) {
return false
}
return true
}
func (c Cond ) IsEmpty () bool {
return c .Is == nil && c .Any1 == nil && c .Not == nil && c .Clock == nil
}
type StateLoop struct {
ResetInterval time .Duration
Threshold int
loopState string
ctxStates am .S
mach am .Api
ended bool
check func () bool
lastSTime uint64
lastHTime time .Time
startSTime uint64
startHTime time .Time
}
func (l *StateLoop ) String () string {
ok := "ok"
if l .ended {
ok = "ended"
}
return fmt .Sprintf ("StateLoop: %s for %s/%s" , ok , l .mach .Id (), l .loopState )
}
func (l *StateLoop ) Break () {
l .ended = true
l .mach .Log (l .String ())
}
func (l *StateLoop ) Sum () uint64 {
return l .mach .Time (l .ctxStates ).Sum (nil )
}
func (l *StateLoop ) Ok (ctx context .Context ) bool {
if l .ended {
return false
} else if ctx != nil && ctx .Err () != nil {
err := fmt .Errorf ("loop: arg ctx expired for %s/%s" , l .mach .Id (),
l .loopState )
l .mach .AddErr (err , nil )
l .ended = true
return false
} else if l .mach .Not1 (l .loopState ) {
err := fmt .Errorf ("loop: state ctx expired for %s/%s" , l .mach .Id (),
l .loopState )
l .mach .AddErr (err , nil )
l .ended = true
return false
}
if l .check != nil && !l .check () {
l .ended = true
return false
}
sum := l .mach .Time (l .ctxStates ).Sum (nil )
if time .Since (l .lastHTime ) > l .ResetInterval {
l .lastHTime = time .Now ()
l .lastSTime = sum
return true
} else if int (sum ) > l .Threshold {
err := fmt .Errorf ("loop: threshold exceeded for %s/%s" , l .mach .Id (),
l .loopState )
l .mach .AddErr (err , nil )
l .ended = true
return false
}
l .lastSTime = sum
return true
}
func (l *StateLoop ) Ended () bool {
return l .ended
}
func NewStateLoop (
mach *am .Machine , loopState string , optCheck func () bool ,
) *StateLoop {
schema := mach .Schema ()
if !mach .Has1 (loopState ) {
return &StateLoop {ended : true }
}
ctxStates := S {loopState }
ctxStates = append (ctxStates , schema [loopState ].Require ...)
resolver := mach .Resolver ()
inbound , _ := resolver .InboundRelationsOf (loopState )
for _ , name := range inbound {
rels , _ := resolver .RelationsBetween (name , loopState )
if len (rels ) > 0 {
ctxStates = append (ctxStates , name )
}
}
l := &StateLoop {
ResetInterval : time .Second ,
Threshold : 500 ,
loopState : loopState ,
mach : mach ,
ctxStates : ctxStates ,
startHTime : time .Now (),
startSTime : mach .Time (ctxStates ).Sum (nil ),
check : optCheck ,
}
mach .Log (l .String ())
return l
}
var SlogToMachLogOpts = &slog .HandlerOptions {
ReplaceAttr : func (groups []string , a slog .Attr ) slog .Attr {
if a .Key == slog .TimeKey || a .Key == slog .LevelKey {
return slog .Attr {}
}
return a
},
}
type SlogToMachLog struct {
Mach am .Api
}
func (l SlogToMachLog ) Write (p []byte ) (n int , err error ) {
s , _ := strings .CutPrefix (string (p ), "msg=" )
l .Mach .Log (s )
return len (p ), nil
}
func TagValue (tags []string , key string ) string {
for _ , t := range tags {
if t == key {
return key
}
p := key + ":"
if !strings .HasPrefix (t , p ) {
continue
}
val , _ := strings .CutPrefix (t , p )
return val
}
return ""
}
func TagValueInt (tags []string , key string ) int {
v := TagValue (tags , key )
if v == "" {
return 0
}
i , _ := strconv .Atoi (v )
return i
}
func PrefixStates (
schema am .Schema , prefix string , removeDups bool , optWhitelist ,
optBlacklist S ,
) am .Schema {
schema = am .CloneSchema (schema )
for name , s := range schema {
if len (optWhitelist ) > 0 && !slices .Contains (optWhitelist , name ) {
continue
} else if len (optBlacklist ) > 0 && slices .Contains (optBlacklist , name ) {
continue
}
for i , r := range s .After {
newName := r
if !removeDups || !strings .HasPrefix (name , prefix ) {
newName = prefix + r
}
s .After [i ] = newName
}
for i , r := range s .Add {
newName := r
if !removeDups || !strings .HasPrefix (name , prefix ) {
newName = prefix + r
}
s .Add [i ] = newName
}
for i , r := range s .Remove {
newName := r
if !removeDups || !strings .HasPrefix (name , prefix ) {
newName = prefix + r
}
s .Remove [i ] = newName
}
for i , r := range s .Require {
newName := r
if !removeDups || !strings .HasPrefix (name , prefix ) {
newName = prefix + r
}
s .Require [i ] = newName
}
newName := name
if !removeDups || !strings .HasPrefix (name , prefix ) {
newName = prefix + name
}
delete (schema , name )
schema [newName ] = s
}
return schema
}
func CountRelations (state *am .State ) int {
return len (state .Remove ) + len (state .Add ) + len (state .Require ) +
len (state .After )
}
func NewMirror (
id string , flat bool , source *am .Machine , handlers any , states am .S ,
) (*am .Machine , error ) {
v := reflect .ValueOf (handlers )
if v .Kind () != reflect .Ptr || v .Elem ().Kind () != reflect .Struct {
return nil , errors .New ("BindHandlers expects a pointer to a struct" )
}
vElem := v .Elem ()
var methodNames []string
methodNames , err := am .ListHandlers (handlers , states )
if err != nil {
return nil , fmt .Errorf ("listing handlers: %w" , err )
}
if id == "" {
id = "mirror-" + source .Id ()
}
sourceSchema := source .Schema ()
names := am .S {am .StateException }
schema := am .Schema {}
for _ , name := range states {
schema [name ] = am .State {
Multi : sourceSchema [name ].Multi ,
}
names = append (names , name )
}
mirror := am .New (source .Ctx (), schema , &am .Opts {
Id : id ,
Parent : source ,
})
for _ , method := range methodNames {
var state string
var isAdd bool
field := vElem .FieldByName (method )
if strings .HasSuffix (method , am .SuffixState ) {
state = method [:len (method )-len (am .SuffixState )]
isAdd = true
} else if strings .HasSuffix (method , am .SuffixEnd ) {
state = method [:len (method )-len (am .SuffixEnd )]
} else {
return nil , fmt .Errorf ("unsupported handler %s for %s" , method , id )
}
var p am .HandlerFinal
if flat {
if source .Is1 (state ) {
mirror .Add1 (state , nil )
}
if isAdd {
p = ampipe .AddFlat (source , mirror , state , "" )
} else {
p = ampipe .RemoveFlat (source , mirror , state , "" )
}
} else {
if isAdd {
p = ampipe .Add (source , mirror , state , "" )
} else {
p = ampipe .Remove (source , mirror , state , "" )
}
}
field .Set (reflect .ValueOf (p ))
}
if err := source .BindHandlers (handlers ); err != nil {
return nil , err
}
return mirror , nil
}
func CopySchema (source am .Schema , target *am .Machine , states am .S ) error {
if len (states ) == 0 {
return nil
}
newSchema := target .Schema ()
for _ , name := range states {
if _ , ok := source [name ]; !ok {
return fmt .Errorf ("%w: state %s in source schema" ,
am .ErrStateMissing , name )
}
newSchema [name ] = source [name ]
}
newStates := utils .SlicesUniq (slices .Concat (target .StateNames (), states ))
return target .SetSchema (newSchema , newStates )
}
func SchemaHash (schema am .Schema ) string {
ret := ""
if schema == nil {
return ""
}
keys := slices .Collect (maps .Keys (schema ))
sort .Strings (keys )
for _ , k := range keys {
ret += k + ":"
if schema [k ].Auto {
ret += "a,"
}
if schema [k ].Multi {
ret += "m,"
}
after := slices .Clone (schema [k ].After )
sort .Strings (after )
for _ , r := range after {
ret += r + ","
}
ret += ";"
remove := slices .Clone (schema [k ].Remove )
sort .Strings (remove )
for _ , r := range remove {
ret += r + ","
}
ret += ";"
add := slices .Clone (schema [k ].Add )
sort .Strings (add )
for _ , r := range add {
ret += r + ","
}
ret += ";"
require := slices .Clone (schema [k ].Require )
sort .Strings (require )
for _ , r := range require {
ret += r + ","
}
ret += ";"
}
return Hash (ret , 6 )
}
func Hash (in string , l int ) string {
hasher := md5 .New ()
hasher .Write ([]byte (in ))
if l == 0 {
l = 6
}
hash := hex .EncodeToString (hasher .Sum (nil ))
return hash [:l ]
}
func EvalGetter [T any ](
ctx context .Context , source string , maxTries int , mach *am .Machine ,
eval func () (T , error ),
) (T , error ) {
var ret T
var retErr error
evalOuter := func () {
ret , retErr = eval ()
}
for range min (maxTries , 1 ) {
if !mach .Eval ("EvGe/" +source , evalOuter , ctx ) {
retErr = fmt .Errorf ("%w: EvGe/%s" , am .ErrEvalTimeout , source )
} else {
break
}
}
return ret , retErr
}
func CantAdd (mach am .Api , states am .S , args am .A ) bool {
done := &am .CheckDone {
Ch : make (chan struct {}),
}
mach .CanAdd (states , am .PassMerge (args , &am .AT {
CheckDone : done ,
}))
<-done .Ch
return !done .Canceled
}
func CantAdd1 (mach am .Api , state string , args am .A ) bool {
return mach .CanAdd (am .S {state }, args ) == am .Canceled
}
func CantRemove (mach am .Api , states am .S , args am .A ) bool {
done := &am .CheckDone {
Ch : make (chan struct {}),
}
mach .CanRemove (states , am .PassMerge (args , &am .AT {
CheckDone : done ,
}))
<-done .Ch
return done .Canceled
}
func CantRemove1 (mach am .Api , state string , args am .A ) bool {
return mach .CanRemove (am .S {state }, args ) == am .Canceled
}
func AskAdd (mach am .Api , states am .S , args am .A ) am .Result {
return AskEvAdd (nil , mach , states , args )
}
func AskEvAdd (e *am .Event , mach am .Api , states am .S , args am .A ) am .Result {
if !CantAdd (mach , states , args ) {
return mach .EvAdd (e , states , args )
}
return am .Canceled
}
func AskAdd1 (mach am .Api , state string , args am .A ) am .Result {
return AskAdd (mach , S {state }, args )
}
func AskEvAdd1 (e *am .Event , mach am .Api , state string , args am .A ) am .Result {
return AskEvAdd (e , mach , S {state }, args )
}
func AskRemove (mach am .Api , states am .S , args am .A ) am .Result {
return AskEvRemove (nil , mach , states , args )
}
func AskEvRemove (e *am .Event , mach am .Api , states am .S , args am .A ) am .Result {
if !CantRemove (mach , states , args ) {
return mach .EvRemove (e , states , args )
}
return am .Canceled
}
func AskRemove1 (mach am .Api , state string , args am .A ) am .Result {
return AskRemove (mach , S {state }, args )
}
func AskEvRemove1 (e *am .Event , mach am .Api , state string , args am .A ) am .Result {
return AskEvRemove (e , mach , S {state }, args )
}
func DisposeBind (mach am .Api , handler am .HandlerDispose ) {
state := ssam .DisposedStates .RegisterDisposal
if mach .Has1 (state ) {
mach .Add1 (state , am .A {
ssam .DisposedArgHandler : handler ,
})
return
}
mach .OnDispose (handler )
}
func Dispose (mach am .Api ) {
state := ssam .DisposedStates .Disposing
if mach .Has1 (state ) {
mach .Add1 (state , nil )
return
}
mach .Dispose ()
}
func SchemaStates (schema am .Schema ) am .S {
return slices .Collect (maps .Keys (schema ))
}
func SchemaImplements (schema am .Schema , states am .S ) error {
return Implements (SchemaStates (schema ), states )
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .