ความคืบหน้างาน ไฟล์ต้นฉบับ

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)
}