ref: 37c59d74571b90a623211a7ef11cf9ce2d90529b
dir: /workflow.go/
package main import ( "errors" "fmt" "net/url" "strings" ) type thing struct { Name string `json:"name"` } func BuildWorkflow1(jc *Client, project, issueTypeNo string) (*WorkflowGraph, error) { var t thing u := fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo) if err := jc.RPC("GET", u, nil, &t); err != nil { return nil, fmt.Errorf("could not query workflow for issue: %v", err) } var wr WorkflowResponse1 u = fmt.Sprintf("/rest/projectconfig/latest/workflow?workflowName=%s", url.QueryEscape(t.Name)) if err := jc.RPC("GET", u, nil, &wr); err != nil { return nil, fmt.Errorf("could not query workflow graph: %v", err) } var wg WorkflowGraph wg.Build1(&wr) return &wg, nil } func BuildWorkflow2(jc *Client, project, issueTypeNo string) (*WorkflowGraph, error) { var t thing u := fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo) if err := jc.RPC("GET", u, nil, &t); err != nil { return nil, fmt.Errorf("could not query workflow for issue: %v", err) } var wr WorkflowResponse2 u = fmt.Sprintf("/rest/workflowDesigner/latest/workflows?name=%s", url.QueryEscape(t.Name)) if err := jc.RPC("GET", u, nil, &wr); err != nil { return nil, fmt.Errorf("could not query workflow graph: %v", err) } var wg WorkflowGraph wg.Build2(&wr) return &wg, nil } type WorkflowResponse2 struct { Layout struct { Statuses []struct { ID string `json:"statusId"` TransitionID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Initial bool `json:"initial"` } `json:"statuses"` Transitions []struct { Name string `json:"name"` Description string `json:"description"` SourceID string `json:"sourceId"` TargetID string `json:"targetId"` ActionID int `json:"actionId"` Global bool `json:"globalTransition"` Looped bool `json:"loopedTransition"` } `json:"transitions"` } `json:"layout"` } type WorkflowResponse1 struct { Name string `json:"name"` Description string `json:"description"` ID int `json:"id"` DisplayName string `json:"displayName"` Admin bool `json:"admin"` Sources []struct { FromStatus WorkflowStatus `json:"fromStatus"` Targets []struct { ToStatus WorkflowStatus `json:"toStatus"` TransitionName string } `json:"targets"` } `json:"sources"` } type WorkflowStatus struct { StatusCategory struct { Sequence int `json:"sequence"` PrimaryAlias string `json:"primaryAlias"` TranslatedName string `json:"translatedName"` ColorName string `json:"colorName"` Aliases []string `json:"aliases"` Name string `json:"name"` Key string `json:"key"` ID int `json:"id"` } `json:"statusCategory"` IconURL string `json:"iconUrl"` Description string `json:"description"` Name string `json:"name"` ID string `json:"id"` } func (wf *WorkflowStatus) Status() *Status { return &Status{ Name: wf.Name, ID: wf.ID, Description: wf.Description, } } type Status struct { Name string Description string ID string Edges []StatusEdge } type StatusEdge struct { Name string Status *Status } type WorkflowGraph struct { // verteces is a map of lower-cased status named to their status struct. verteces map[string]*Status } func (wg *WorkflowGraph) Build2(wr *WorkflowResponse2) { if wg.verteces == nil { wg.verteces = make(map[string]*Status) } local := make(map[string]*Status) layout := wr.Layout for _, s := range layout.Statuses { l := &Status{ Name: s.Name, Description: s.Description, ID: s.ID, } wg.verteces[strings.ToLower(s.Name)] = l local[s.TransitionID] = l } for _, t := range layout.Transitions { a := local[t.SourceID] b := local[t.TargetID] edge := StatusEdge{ Name: t.Name, Status: b, } if t.Global { for _, v := range local { v.Edges = append(v.Edges, edge) } } else { a.Edges = append(a.Edges, edge) } } } func (wg *WorkflowGraph) Build1(wr *WorkflowResponse1) { if wg.verteces == nil { wg.verteces = make(map[string]*Status) } for _, elem := range wr.Sources { name := strings.ToLower(elem.FromStatus.Name) fromStatus, exists := wg.verteces[name] if !exists { fromStatus = elem.FromStatus.Status() wg.verteces[name] = fromStatus } for _, target := range elem.Targets { targetName := strings.ToLower(target.ToStatus.Name) targetStatus, exists := wg.verteces[targetName] if !exists { targetStatus = target.ToStatus.Status() wg.verteces[name] = targetStatus } targetEdge := StatusEdge{ Name: target.TransitionName, Status: targetStatus, } fromStatus.Edges = append(fromStatus.Edges, targetEdge) } } } func (wg *WorkflowGraph) Dump() string { var ss string for _, v := range wg.verteces { var s string for _, e := range v.Edges { s += fmt.Sprintf("%s (%s), ", e.Status.Name, e.Name) } ss += fmt.Sprintf("Status: %s, edges: %s\n", v.Name, s) } return ss } type path struct { from *path edge StatusEdge } // Path finds the shortest path in the workflow graph from A to B, searching at // most limit verteces. A negative limit results in path executing without a // limit. Cycles are detected and terminated, so the limit is just to avoid high // searching times in *very* large graphs. A and B are case insensitive for // convenience. func (wg *WorkflowGraph) Path(A, B string, limit int) ([]string, error) { statusA := wg.verteces[strings.ToLower(A)] statusB := wg.verteces[strings.ToLower(B)] if statusA == nil || statusB == nil { return nil, errors.New("no such status") } visited := make(map[string]bool) var search []path for _, edge := range statusA.Edges { search = append(search, path{edge: edge}) } for len(search) > 0 { limit-- if limit == 0 { break } p := search[0] search = search[1:] // FOUND! if p.edge.Status == statusB { var s []string start := &p for { s = append([]string{start.edge.Name}, s...) if start.from == nil { break } start = start.from } return s, nil } if visited[p.edge.Status.ID] { // We have already walked all edges of this vertice. continue } visited[p.edge.Status.ID] = true // Add the edges to the search. for _, edge := range p.edge.Status.Edges { search = append(search, path{from: &p, edge: edge}) } } return nil, errors.New("path not found") }