{"id":16440239,"url":"https://github.com/soypat/go-maquina","last_synced_at":"2025-03-21T04:33:35.259Z","repository":{"id":57663412,"uuid":"480128719","full_name":"soypat/go-maquina","owner":"soypat","description":"Small finite-state machine library for Go","archived":false,"fork":false,"pushed_at":"2023-08-25T23:37:12.000Z","size":48,"stargazers_count":55,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-17T21:39:20.326Z","etag":null,"topics":["embedded","embedded-systems","finite-state-machine","fsm","go","golang","graphviz","state-machine","state-machine-diagram"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/soypat.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-04-10T20:12:28.000Z","updated_at":"2025-02-20T23:51:04.000Z","dependencies_parsed_at":"2024-06-20T05:49:30.466Z","dependency_job_id":"cb4ed4d9-c388-41e0-9964-61e4872675eb","html_url":"https://github.com/soypat/go-maquina","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soypat%2Fgo-maquina","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soypat%2Fgo-maquina/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soypat%2Fgo-maquina/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soypat%2Fgo-maquina/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soypat","download_url":"https://codeload.github.com/soypat/go-maquina/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244738808,"owners_count":20501920,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["embedded","embedded-systems","finite-state-machine","fsm","go","golang","graphviz","state-machine","state-machine-diagram"],"created_at":"2024-10-11T09:11:36.980Z","updated_at":"2025-03-21T04:33:33.370Z","avatar_url":"https://github.com/soypat.png","language":"Go","funding_links":[],"categories":["Embedded Systems"],"sub_categories":["General use"],"readme":"[![go.dev reference](https://pkg.go.dev/badge/github.com/soypat/go-maquina)](https://pkg.go.dev/github.com/soypat/go-maquina)\n[![Go Report Card](https://goreportcard.com/badge/github.com/soypat/go-maquina)](https://goreportcard.com/report/github.com/soypat/go-maquina)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![codecov](https://codecov.io/gh/soypat/go-maquina/branch/main/graph/badge.svg?token=5DH2RG1UVP)](https://codecov.io/gh/soypat/go-maquina)\n\n# go-maquina\n\nCreate delightfully simple finite-state machines in Go. Inspired by [stateless](https://github.com/qmuntal/stateless).\n\nIf you wish to build state machines that are extremely maintainable and stand the test of time\nyou have come to the right place. \n\n## Highlights\n- Labelled everything: Label your states, triggers, guard clauses and callbacks so that...\n\t- You can visualize state machines as a DOT generated graph. See examples below!\n\t- Deep introspection into what is going on in your state machine.\n- Decent performance, no allocations: see benchmark below.\n\n\n_Maquina_ is the spanish word for machine. It is pronounced maa-kee-nuh, much like _machina_ from the Latin calque [_deus-ex-machina_](https://en.wikipedia.org/wiki/Deus_ex_machina).\n\n### Benchmark\nBenchmarked below is the time it takes for a transition to complete when no callbacks or guard clauses are in place.\n```\n$ go test -test.bench=. -benchmem\ngoos: linux\ngoarch: amd64\npkg: github.com/soypat/go-maquina\ncpu: 12th Gen Intel(R) Core(TM) i5-12400F\nBenchmarkHyper-12       31407175                37.38 ns/op            0 B/op        0 allocs/op\nPASS\nok      github.com/soypat/go-maquina    2.192s\n```\n\n## Code organization\n\n* [`maquina.go`](./maquina.go) contains internal logic for the state machine such as the `fire()` functions triggered by a state transition.\n\n* [`state.go`](./state.go) contains most of the user visible exported methods on `State` type.\n\n* [`statemachine.go`](./statemachine.go) contains code relevant to the State manager StateMachine.\n\n\n## Toll booth example\n![toolbooth diagram](https://user-images.githubusercontent.com/26156425/238150418-c223b843-ae14-4694-a40c-c6b123c43886.png)\n```go\nconst (\n\tpassageCost                      = 10.00\n\tdefaultPay                       = 0.0\n\tpayUp            maquina.Trigger = \"customer pays\"\n\tcustomerAdvances maquina.Trigger = \"customer advances\"\n)\nvar (\n\ttollClosed = maquina.NewState(\"toll barrier closed\", defaultPay)\n\ttollOpen   = maquina.NewState(\"toll barrier open\", defaultPay)\n\tguardPay   = maquina.NewGuard(\"payment check\", func(ctx context.Context, pay float64) error {\n\t\tif pay \u003c passageCost {\n\t\t\t// Barrier remains closed unless customer pays up\n\t\t\treturn fmt.Errorf(\"customer underpaid with $%.2f\", pay)\n\t\t}\n\t\treturn nil\n\t})\n)\n\ntollClosed.Permit(payUp, tollOpen, guardPay)\ntollOpen.Permit(customerAdvances, tollClosed)\nSM := maquina.NewStateMachine(tollClosed)\nfor i := 0; i \u003c 5; i++ {\n\tpay := 2 * passageCost * rand.Float64()\n\terr := SM.FireBg(payUp, pay)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t} else {\n\t\tfmt.Printf(\"customer paid $%.2f, let them pass!\\n\", pay)\n\t\tSM.FireBg(customerAdvances, 0)\n\t}\n}\n```\nThe code above outputs:\n\n```\ncustomer paid $12.09, let them pass!\ncustomer paid $18.81, let them pass!\ncustomer paid $13.29, let them pass!\nguard clause failed: customer underpaid with $8.75\nguard clause failed: customer underpaid with $8.49\n```\n## Algorithmic trading graph\nThe code below outputs the following DOT graph code. Note how parent/super states can be crafted. Entry/Exit callbacks will be triggered on a superstate when entering/exiting a substate from outside/within the super state.\n\n![algorithmic trading example](https://user-images.githubusercontent.com/26156425/253708380-8095da85-c6f2-49eb-8721-bf2747dc5330.png)\n\n\n\n```go\n\tgetStock := func() string {\n\t\treturn string([]byte{byte(rand.Intn(26)) + 'A', byte(rand.Intn(26)) + 'A', byte(rand.Intn(26)) + 'A'})\n\t}\n\ttype tradeState struct {\n\t\ttargetStock   string\n\t\tquoteReceived time.Time\n\t}\n\ttype transition = maquina.Transition[*tradeState]\n\n\tconst (\n\t\ttrigRequestQuote     = \"request quote\"\n\t\ttrigExecute          = \"execute\"\n\t\ttrigExecuteFail      = \"execute failed\"\n\t\ttrigCancel           = \"cancel\"\n\t\ttrigQuoteReceived    = \"quote received\"\n\t\ttrigExecuteConfirmed = \"execute confirmed\"\n\t)\n\tvar (\n\t\tstateWaitingOnQuote = maquina.NewState(\"waiting on quote\", \u0026tradeState{})\n\t\tstateReadyToOperate = maquina.NewState(\"ready to operate\", \u0026tradeState{})\n\t\tstateIdle           = maquina.NewState(\"idle\", \u0026tradeState{})\n\t\tstateExecuting      = maquina.NewState(\"executing\", \u0026tradeState{})\n\t\tstateCritical       = maquina.NewState(\"critical section\", \u0026tradeState{})\n\n\t\tfringeStockSelect = maquina.NewFringeCallback(\"stock select\", func(_ context.Context, _ transition, state *tradeState) {\n\t\t\tstate.targetStock = getStock()\n\t\t})\n\n\t\tfringeStockClear = maquina.NewFringeCallback(\"stock clear\", func(_ context.Context, _ transition, state *tradeState) {\n\t\t\tstate.targetStock = \"\"\n\t\t})\n\n\t\tguardQuoteStale = maquina.NewGuard(\"quote stale\", func(ctx context.Context, state *tradeState) error {\n\t\t\tconst staleQuoteTimeout = 10 * time.Minute\n\t\t\telapsed := time.Since(state.quoteReceived)\n\t\t\tif elapsed \u003e staleQuoteTimeout || elapsed \u003c 1 { // Sanity check included.\n\t\t\t\treturn errors.New(\"quote is stale: \" + elapsed.String() + \" elapsed\")\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t)\n\n\tstateIdle.Permit(trigRequestQuote, stateWaitingOnQuote)\n\tstateIdle.OnExitThrough(trigRequestQuote, fringeStockSelect)\n\tstateIdle.OnEntry(fringeStockClear)\n\n\tstateWaitingOnQuote.Permit(trigCancel, stateIdle)\n\tstateWaitingOnQuote.Permit(trigQuoteReceived, stateReadyToOperate)\n\n\tstateReadyToOperate.Permit(trigExecute, stateExecuting, guardQuoteStale)\n\tstateReadyToOperate.Permit(trigCancel, stateIdle)\n\n\tstateExecuting.Permit(trigExecuteConfirmed, stateIdle)\n\tstateExecuting.Permit(trigExecuteFail, stateReadyToOperate)\n\n\t// Mark critical section as a superstate.\n\tstateCritical.LinkSubstates(stateWaitingOnQuote, stateReadyToOperate, stateExecuting)\n\n\tsm := maquina.NewStateMachine(stateIdle)\n\tvar buf bytes.Buffer\n\tmaquina.WriteDOT2(\u0026buf, sm)\n\tfmt.Println(buf.String())\n```\n\n## 3D Printer graphviz example\nThe code below outputs the following DOT graph code:\n![3d printer example](https://user-images.githubusercontent.com/26156425/238145938-6cf54057-ae07-4b47-ad54-d3997032d540.png)\n\n```go\ntype printerState struct {\n\tx, y, z int\n}\n// Declaration of triggers. These are actions.\n// In the example of a 3D printer one could think of them\n// as buttons exposed to the end user.\nconst (\n\ttrigHome      maquina.Trigger = \"home\"\n\ttrigCalibrate maquina.Trigger = \"calibrate\"\n\ttrigStop      maquina.Trigger = \"stop\"\n)\nvar (\n\t// stateSingleton contains the state of the printer at all times.\n\t// It is a singleton and is shared by all states.\n\tstateSingleton   = \u0026printerState{}\n\tstateIdleHome    = maquina.NewState(\"idle at home\", stateSingleton)\n\tstateIdle        = maquina.NewState(\"idle\", stateSingleton)\n\tstateCalibrating = maquina.NewState(\"calibrating\", stateSingleton)\n\tstateGoingHome   = maquina.NewState(\"going home\", stateSingleton)\n\t// guardNotAtHome is a guard clause that checks if the printer is at home position.\n\tguardNotAtHome = maquina.NewGuard(\"not at home\", func(ctx context.Context, state *printerState) error {\n\t\tif state.x != 0 || state.y != 0 || state.z != 0 {\n\t\t\treturn fmt.Errorf(\"not at home\")\n\t\t}\n\t\treturn nil\n\t})\n)\n// Declare Calibration and Stop transitions. These would be the actions taken\n// when user presses CALIBRATE or STOP button.\nstateIdleHome.Permit(trigCalibrate, stateCalibrating)\nstateIdle.Permit(trigCalibrate, stateCalibrating, guardNotAtHome)\n// Special case of STOP while home: we stay at home.\nstateIdleHome.Permit(trigStop, stateIdleHome)\n\n// Declare home transitions. These would be the actions taken when a user presses\n// the HOME button, as an example.\nstateCalibrating.Permit(trigHome, stateGoingHome)\nstateIdle.Permit(trigHome, stateGoingHome)\nstateGoingHome.Permit(trigHome, stateIdleHome, guardNotAtHome)\nsm := maquina.NewStateMachine(stateIdleHome)\n// In the case of stopping we go to Idle state since we are not\n// guaranteed to be at home position.\nsm.AlwaysPermit(trigStop, stateIdle)\nvar buf bytes.Buffer\nmaquina.WriteDOT(\u0026buf, sm)\nfmt.Println(buf.String())\n// With the code below one can also output a PNG file with the graph:\n// One must have graphviz installed and in the path: `sudo apt install graphviz`\n//\n//  cmd := exec.Command(\"dot\", \"-Tpng\", \"-o \", \"3dprinter.png\")\n//  cmd.Stdin = \u0026buf\n//  cmd.Run()\n```\n\n## Hyper connected state diagram\nA toy example of 8 states, all of them connected to illustrate capabilities of go-maquina when coupled to graphviz (code is below):\n![hyper-states](https://user-images.githubusercontent.com/26156425/238158584-b178ecce-ea0c-4a8b-987b-5e4cc7986ad8.png)\n\n```go\nconst n = 8\nhyperStates := make([]maquina.State[int], n)\nfor i := 0; i \u003c n; i++ {\n\thyperStates[i] = *maquina.NewState(\"S\"+strconv.Itoa(i), i)\n\tfor j := i - 1; j \u003e= 0; j-- {\n\t\ttrigger := maquina.Trigger(\"T\" + strconv.Itoa(i) + \"→\" + strconv.Itoa(j))\n\t\thyperStates[i].Permit(trigger, \u0026hyperStates[j])\n\t}\n}\nfor i := 0; i \u003c n; i++ {\n\tfor j := i + 1; j \u003c n; j++ {\n\t\ttrigger := maquina.Trigger(\"T\" + strconv.Itoa(i) + \"→\" + strconv.Itoa(j))\n\t\thyperStates[i].Permit(trigger, \u0026hyperStates[j])\n\t}\n}\nsourceState := maquina.NewState(\"source\", 0)\nsourceState.Permit(\"goto S0\", \u0026hyperStates[0])\nfailsafeState := maquina.NewState(\"sink failsafe\", -1)\nsm := maquina.NewStateMachine(sourceState)\nsm.AlwaysPermit(\"goto failsafe\", failsafeState)\nvar buf bytes.Buffer\nmaquina.WriteDOT(\u0026buf, sm)\ncmd := exec.Command(\"dot\", \"-Tpng\", \"-o\", \"hyper-states.png\")\ncmd.Stdin = \u0026buf\ncmd.Run()\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoypat%2Fgo-maquina","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoypat%2Fgo-maquina","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoypat%2Fgo-maquina/lists"}