1package fsmtest 2 3import ( 4 "fmt" 5 "sort" 6 "strings" 7 8 "github.com/jawher/mow.cli/internal/fsm" 9 "github.com/jawher/mow.cli/internal/matcher" 10) 11 12// NopeMatcher is a matcher that always fails 13type NopeMatcher struct{} 14 15// Match always returns false without consuming any args 16func (NopeMatcher) Match(args []string, c *matcher.ParseContext) (bool, []string) { 17 return false, args 18} 19 20// Priority returns the magic value 666 21func (NopeMatcher) Priority() int { 22 return 666 23} 24 25func (NopeMatcher) String() string { 26 return "<nope>" 27} 28 29// YepMatcher is a matcher that always succeeds without consuming any args 30type YepMatcher struct{} 31 32// Match always returns true without consuming any args 33func (YepMatcher) Match(args []string, c *matcher.ParseContext) (bool, []string) { 34 return true, args 35} 36 37// Priority returns the magic value 666 38func (YepMatcher) Priority() int { 39 return 666 40} 41 42func (YepMatcher) String() string { 43 return "<yep>" 44} 45 46// TestMatcher is a matcher with a configurable match function and priority 47type TestMatcher struct { 48 MatchFunc func(args []string, c *matcher.ParseContext) (bool, []string) 49 TestPriority int 50} 51 52// Match executes the provided match func 53func (t TestMatcher) Match(args []string, c *matcher.ParseContext) (bool, []string) { 54 return t.MatchFunc(args, c) 55} 56 57// Priority returns the provided priority 58func (t TestMatcher) Priority() int { 59 return t.TestPriority 60} 61 62/* 63NewFsm constructs an FSM from the provided string spec and a list of defined matchers 64The spec syntax is: 65 66S1 t1 S2 67S2 t2 (S3) 68<source state name> <transition name> <target state name> 69 70states between parenthesis are final states 71*/ 72func NewFsm(spec string, matchers map[string]matcher.Matcher) *fsm.State { 73 states := map[string]*fsm.State{} 74 lines := strings.FieldsFunc(spec, func(r rune) bool { return r == '\n' }) 75 76 var res *fsm.State 77 78 for _, line := range lines { 79 if strings.TrimSpace(line) == "" { 80 continue 81 } 82 parts := strings.Fields(line) 83 if len(parts) != 3 { 84 panic(fmt.Sprintf("Invalid line %q: syntax: START TR END", line)) 85 } 86 sn, tn, en := parts[0], parts[1], parts[2] 87 sn, sterm := stateNameTerm(sn) 88 en, eterm := stateNameTerm(en) 89 90 s, ok := states[sn] 91 if !ok { 92 s = fsm.NewState() 93 states[sn] = s 94 } 95 s.Terminal = s.Terminal || sterm 96 97 if res == nil { 98 res = s 99 } 100 101 e, ok := states[en] 102 if !ok { 103 e = fsm.NewState() 104 states[en] = e 105 } 106 e.Terminal = e.Terminal || eterm 107 108 t, ok := matchers[tn] 109 if !ok { 110 panic(fmt.Sprintf("Unknown matcher %q in line %q", tn, line)) 111 } 112 113 s.T(t, e) 114 } 115 return res 116} 117 118// TransitionStrs returns a string slice with the transitions names 119func TransitionStrs(trs fsm.StateTransitions) []string { 120 var res []string 121 for _, tr := range trs { 122 res = append(res, fmt.Sprintf("%v", tr.Matcher)) 123 } 124 return res 125} 126 127func stateNameTerm(name string) (string, bool) { 128 if strings.HasPrefix(name, "(") { 129 if strings.HasSuffix(name, ")") { 130 name = name[1 : len(name)-1] 131 return name, true 132 } 133 panic(fmt.Sprintf("Invalid state name %q", name)) 134 } 135 return name, false 136} 137 138func mkStateNames() *stateNames { 139 return &stateNames{ 140 counter: 1, 141 ids: map[*fsm.State]int{}, 142 } 143} 144 145type stateNames struct { 146 counter int 147 ids map[*fsm.State]int 148} 149 150func (sn *stateNames) id(s *fsm.State) int { 151 res := sn.ids[s] 152 if res != 0 { 153 return res 154 } 155 res = sn.counter 156 sn.ids[s] = res 157 sn.counter++ 158 return res 159} 160 161func stateName(s *fsm.State, sn *stateNames) string { 162 id := sn.id(s) 163 164 if !s.Terminal { 165 return fmt.Sprintf("S%d", id) 166 } 167 return fmt.Sprintf("(S%d)", id) 168} 169 170// FsmStr generates a string representation of the provided FSM 171func FsmStr(s *fsm.State) string { 172 lines := fsmStrVis(s, mkStateNames(), map[*fsm.State]struct{}{}) 173 sort.Sort(lines) 174 return strings.Join(lines, "\n") 175} 176 177func fsmStrVis(s *fsm.State, sn *stateNames, visited map[*fsm.State]struct{}) fsmStrings { 178 if _, ok := visited[s]; ok { 179 return nil 180 } 181 visited[s] = struct{}{} 182 183 res := fsmStrings{} 184 for _, tr := range s.Transitions { 185 res = append(res, fmt.Sprintf("%s %v %s", stateName(s, sn), tr.Matcher, stateName(tr.Next, sn))) 186 res = append(res, fsmStrVis(tr.Next, sn, visited)...) 187 } 188 189 return res 190} 191 192type fsmStrings []string 193 194func (t fsmStrings) Len() int { return len(t) } 195func (t fsmStrings) Swap(i, j int) { t[i], t[j] = t[j], t[i] } 196func (t fsmStrings) Less(i, j int) bool { 197 a := strings.TrimFunc(t[i], isParen) 198 b := strings.TrimFunc(t[j], isParen) 199 return strings.Compare(a, b) < 0 200} 201 202func isParen(r rune) bool { 203 return r == '(' || r == ')' 204} 205