-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuilder.go
More file actions
290 lines (252 loc) · 8.46 KB
/
builder.go
File metadata and controls
290 lines (252 loc) · 8.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
package statechartx
import (
"fmt"
"strings"
)
// MachineBuilder provides a fluent API for constructing state machines using string-based state names
// instead of manual integer-based State struct creation.
type MachineBuilder struct {
nextID StateID
nameToID map[string]StateID
idToName map[StateID]string // For debugging/reverse lookup
states map[StateID]*State
root *State
rootName string
}
// StateBuilder provides fluent methods for configuring individual states.
type StateBuilder struct {
b *MachineBuilder
state *State
name string
}
// NewMachineBuilder creates a new builder for constructing a state machine.
// rootName is the name of the root compound state, and initialStateName is the initial state to enter.
func NewMachineBuilder(rootName, initialStateName string) *MachineBuilder {
b := &MachineBuilder{
nextID: 1, // Root gets ID 0
nameToID: make(map[string]StateID),
idToName: make(map[StateID]string),
states: make(map[StateID]*State),
rootName: rootName,
}
// Create root state
rootID := StateID(0)
b.nameToID[rootName] = rootID
b.idToName[rootID] = rootName
b.root = &State{
ID: rootID,
Initial: b.assignID(initialStateName), // Forward ref ok
Children: make(map[StateID]*State),
}
b.states[rootID] = b.root
return b
}
// State creates or retrieves a state by name.
// Supports dot notation for hierarchical states (e.g., "parent.child").
// If the parent doesn't exist, it will be auto-created as a compound state.
func (b *MachineBuilder) State(name string) *StateBuilder {
// Handle hierarchical names
parentPath, _ := splitPath(name)
// Get or create parent
parent := b.root
if parentPath != "" {
parentID := b.assignID(parentPath)
parent = b.states[parentID]
if parent == nil {
// Auto-create parent as compound
parent = &State{
ID: parentID,
Children: make(map[StateID]*State),
}
b.states[parentID] = parent
// Link to grandparent
grandparentPath, _ := splitPath(parentPath)
if grandparentPath == "" {
// Parent of root
b.root.Children[parentID] = parent
parent.Parent = b.root
} else {
grandparentID := b.assignID(grandparentPath)
if grandparent := b.states[grandparentID]; grandparent != nil {
if grandparent.Children == nil {
grandparent.Children = make(map[StateID]*State)
}
grandparent.Children[parentID] = parent
parent.Parent = grandparent
}
}
}
}
// Get or create state
id := b.assignID(name)
state := b.states[id]
if state == nil {
state = &State{ID: id}
b.states[id] = state
// Add to parent
if parent.Children == nil {
parent.Children = make(map[StateID]*State)
}
parent.Children[id] = state
state.Parent = parent
}
return &StateBuilder{b: b, state: state, name: name}
}
// Build validates the state machine configuration and constructs the Machine.
// Returns an error if the configuration is invalid.
func (b *MachineBuilder) Build() (*Machine, error) {
// Validate: all referenced states exist
if err := b.validate(); err != nil {
return nil, err
}
// Use existing NewMachine (tested)
return NewMachine(b.root)
}
// GetID returns the assigned StateID for a given state name.
// Returns 0 if the name hasn't been registered.
func (b *MachineBuilder) GetID(name string) StateID {
return b.nameToID[name]
}
// GetName returns the name for a given StateID.
// Returns empty string if the ID doesn't exist.
func (b *MachineBuilder) GetName(id StateID) string {
return b.idToName[id]
}
// assignID returns the existing ID for a name, or creates a new sequential ID.
// This ensures deterministic ID assignment.
func (b *MachineBuilder) assignID(name string) StateID {
if id, exists := b.nameToID[name]; exists {
return id
}
id := b.nextID
b.nextID++
b.nameToID[name] = id
b.idToName[id] = name
return id
}
// validate checks that the state machine configuration is valid.
func (b *MachineBuilder) validate() error {
// Check that all transition targets exist
for id, state := range b.states {
for _, trans := range state.Transitions {
if trans.Target != 0 { // 0 is internal transition
if _, exists := b.states[trans.Target]; !exists {
return fmt.Errorf("state %s has transition to unknown target state ID %d", b.idToName[id], trans.Target)
}
}
}
// Check compound states have valid Initial
if len(state.Children) > 0 && state.Initial == 0 && !state.IsParallel {
return fmt.Errorf("compound state %s must have an initial state", b.idToName[id])
}
if state.Initial != 0 {
if _, exists := b.states[state.Initial]; !exists {
return fmt.Errorf("state %s has invalid initial state ID %d", b.idToName[id], state.Initial)
}
}
}
return nil
}
// splitPath splits a hierarchical path into parent and name components.
// For example, "parent.child" returns ("parent", "child").
// For "child", returns ("", "child").
func splitPath(path string) (parent, name string) {
idx := strings.LastIndex(path, ".")
if idx == -1 {
return "", path
}
return path[:idx], path[idx+1:]
}
// StateBuilder fluent methods
// Atomic marks this state as atomic (no children).
// This is the default for states without children.
func (sb *StateBuilder) Atomic() *StateBuilder {
// State is already atomic by default
return sb
}
// Compound marks this state as a compound state with the given initial child state.
// The initial state will be entered when this compound state is entered.
func (sb *StateBuilder) Compound(initialStateName string) *StateBuilder {
initialID := sb.b.assignID(initialStateName)
sb.state.Initial = initialID
if sb.state.Children == nil {
sb.state.Children = make(map[StateID]*State)
}
return sb
}
// Parallel marks this state as a parallel state.
// All child states will be active concurrently when this state is entered.
func (sb *StateBuilder) Parallel() *StateBuilder {
sb.state.IsParallel = true
if sb.state.Children == nil {
sb.state.Children = make(map[StateID]*State)
}
return sb
}
// Final marks this state as a final state with optional data.
// When entered, this state will generate a done event.
func (sb *StateBuilder) Final(data any) *StateBuilder {
sb.state.IsFinal = true
sb.state.FinalStateData = data
return sb
}
// History marks this state as a history pseudo-state.
// historyType should be HistoryShallow or HistoryDeep.
// defaultStateName is the state to enter if no history exists.
func (sb *StateBuilder) History(historyType HistoryType, defaultStateName string) *StateBuilder {
sb.state.IsHistoryState = true
sb.state.HistoryType = historyType
sb.state.HistoryDefault = sb.b.assignID(defaultStateName)
return sb
}
// Entry sets the entry action for this state.
// The action will be executed when entering this state.
func (sb *StateBuilder) Entry(action Action) *StateBuilder {
sb.state.EntryAction = action
return sb
}
// Exit sets the exit action for this state.
// The action will be executed when exiting this state.
func (sb *StateBuilder) Exit(action Action) *StateBuilder {
sb.state.ExitAction = action
return sb
}
// InitialAction sets the initial action for this state.
// The action will be executed when first entering this state.
func (sb *StateBuilder) InitialAction(action Action) *StateBuilder {
sb.state.InitialAction = action
return sb
}
// On adds a transition from this state to the target state when the given event occurs.
// eventName is the string name of the event (will be prefixed with "event:" internally).
// targetName is the name of the target state.
// guard is an optional guard condition (can be nil).
// action is an optional transition action (can be nil).
func (sb *StateBuilder) On(eventName string, targetName string, guard Guard, action Action) *StateBuilder {
// Namespace events with "event:" prefix
eventID := EventID(sb.b.assignID("event:" + eventName))
targetID := sb.b.assignID(targetName)
transition := &Transition{
Event: eventID,
Source: sb.state,
Target: targetID,
Guard: guard,
Action: action,
}
sb.state.Transitions = append(sb.state.Transitions, transition)
return sb
}
// OnInternal adds an internal transition that doesn't change state.
// The transition action executes but no exit/entry actions are triggered.
func (sb *StateBuilder) OnInternal(eventName string, guard Guard, action Action) *StateBuilder {
eventID := EventID(sb.b.assignID("event:" + eventName))
transition := &Transition{
Event: eventID,
Source: sb.state,
Target: 0, // 0 indicates internal transition
Guard: guard,
Action: action,
}
sb.state.Transitions = append(sb.state.Transitions, transition)
return sb
}