tracker/diagram.go
package main
import (
"errors"
"fmt"
"math"
"os"
"sync"
)
type DiagramNode struct {
ID string `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
Description string `json:"description"`
X float64 `json:"x"`
Y float64 `json:"y"`
}
type DiagramEdge struct {
ID string `json:"id"`
Source string `json:"source"`
Target string `json:"target"`
Label string `json:"label"`
Head string `json:"head"`
Tail string `json:"tail"`
}
type Diagram struct {
Revision int `json:"revision"`
Nodes []DiagramNode `json:"nodes"`
Edges []DiagramEdge `json:"edges"`
}
type diagramStore struct {
dir string
mu sync.Mutex
}
var errDiagramConflict = errors.New("diagram revision conflict")
func draftDiagram(u UseCase) Diagram {
if u.Workflow != nil {
return workflowDiagram(u)
}
d := Diagram{Nodes: []DiagramNode{{ID: "start", Type: "start", Title: "เริ่มต้น", Description: u.Start, X: 100, Y: 60}}, Edges: []DiagramEdge{}}
for i, f := range u.Flow {
d.Nodes = append(d.Nodes, DiagramNode{ID: fmt.Sprintf("step-%d", i+1), Type: "process", Title: fmt.Sprintf("%d. %s", i+1, f[0]), Description: f[1], X: float64(440 + i*340), Y: 60})
}
d.Nodes = append(d.Nodes, DiagramNode{ID: "end", Type: "end", Title: "สิ้นสุด", Description: u.Outcome, X: float64(440 + len(u.Flow)*340), Y: 60})
for i := 1; i < len(d.Nodes); i++ {
d.Edges = append(d.Edges, DiagramEdge{ID: fmt.Sprintf("edge-%d", i), Source: d.Nodes[i-1].ID, Target: d.Nodes[i].ID, Head: "arrow", Tail: "none"})
}
return d
}
func validateDiagram(d Diagram) error {
bad := func() error {
return errors.New("diagram ไม่ถูกต้อง: ตรวจประเภท shape, รหัสซ้ำ, พิกัด หรือเส้นเชื่อม")
}
if d.Nodes == nil || d.Edges == nil || d.Revision < 0 || d.Revision > 1000000000 || len(d.Nodes) > 500 || len(d.Edges) > 2000 {
return bad()
}
ids := map[string]bool{}
edges := map[string]bool{}
for _, n := range d.Nodes {
if n.ID == "" || len(n.ID) > 100 || ids[n.ID] || len(n.Title) > 500 || len(n.Description) > 20000 {
return bad()
}
if n.Type != "start" && n.Type != "end" && n.Type != "process" && n.Type != "decision" {
return bad()
}
for _, p := range []float64{n.X, n.Y} {
if math.IsNaN(p) || math.IsInf(p, 0) || p < 0 || p > 100000 {
return bad()
}
}
ids[n.ID] = true
}
arrow := func(s string) bool { return s == "none" || s == "arrow" || s == "open" || s == "diamond" }
for _, e := range d.Edges {
if e.ID == "" || len(e.ID) > 100 || edges[e.ID] || !ids[e.Source] || !ids[e.Target] || !arrow(e.Head) || !arrow(e.Tail) || len(e.Label) > 2000 {
return bad()
}
edges[e.ID] = true
}
return nil
}
func (ds *diagramStore) load(id string, u UseCase) (Diagram, error) {
ds.mu.Lock()
defer ds.mu.Unlock()
d, err := ds.read(id)
if errors.Is(err, os.ErrNotExist) {
return draftDiagram(u), nil
}
return d, err
}
func (ds *diagramStore) read(id string) (Diagram, error) {
doc, err := ds.readDocument(id)
return doc.Diagram, err
}
func (ds *diagramStore) save(id string, d Diagram) (Diagram, error) {
ds.mu.Lock()
defer ds.mu.Unlock()
if err := validateDiagram(d); err != nil {
return d, err
}
doc, err := ds.readDocument(id)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return d, err
}
if doc.Revision != d.Revision {
return d, errDiagramConflict
}
return ds.writeRevision(id, doc, d, "save", 0)
}