diff --git a/.gitignore b/.gitignore
index 53c37a1..a1ed284 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-dist
\ No newline at end of file
+dist
+/isucrud
diff --git a/dbdoc/dbdoc.go b/dbdoc/dbdoc.go
index 3c478c6..b0aaa7c 100644
--- a/dbdoc/dbdoc.go
+++ b/dbdoc/dbdoc.go
@@ -3,7 +3,6 @@ package dbdoc
import (
"fmt"
"go/token"
- "os"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/ssa"
@@ -19,7 +18,7 @@ type Config struct {
DestinationFilePath string
}
-func Run(conf Config) error {
+func Run(conf Config) ([]*Node, error) {
ctx := &Context{
FileSet: token.NewFileSet(),
WorkDir: conf.WorkDir,
@@ -27,17 +26,17 @@ func Run(conf Config) error {
ssaProgram, pkgs, err := BuildSSA(ctx, conf.BuildArgs)
if err != nil {
- return fmt.Errorf("failed to build ssa: %w", err)
+ return nil, fmt.Errorf("failed to build ssa: %w", err)
}
loopRangeMap, err := BuildLoopRangeMap(ctx)
if err != nil {
- return fmt.Errorf("failed to build loop range map: %w", err)
+ return nil, fmt.Errorf("failed to build loop range map: %w", err)
}
funcs, err := BuildFuncs(ctx, pkgs, ssaProgram, loopRangeMap)
if err != nil {
- return fmt.Errorf("failed to build funcs: %w", err)
+ return nil, fmt.Errorf("failed to build funcs: %w", err)
}
nodes := BuildGraph(
@@ -46,18 +45,7 @@ func Run(conf Config) error {
conf.IgnoreMain, conf.IgnoreInitialize,
)
- f, err := os.Create(conf.DestinationFilePath)
- if err != nil {
- return fmt.Errorf("failed to make directory: %w", err)
- }
- defer f.Close()
-
- err = WriteMermaid(f, nodes)
- if err != nil {
- return fmt.Errorf("failed to write mermaid: %w", err)
- }
-
- return nil
+ return nodes, nil
}
func BuildSSA(ctx *Context, args []string) (*ssa.Program, []*packages.Package, error) {
diff --git a/dbdoc/mermaid.go b/dbdoc/mermaid.go
deleted file mode 100644
index a65944a..0000000
--- a/dbdoc/mermaid.go
+++ /dev/null
@@ -1,165 +0,0 @@
-package dbdoc
-
-import (
- "fmt"
- "io"
- "log"
- "strconv"
- "strings"
-)
-
-const (
- funcNodeColor = "1976D2"
- tableNodeColor = "795548"
- insertLinkColor = "CDDC39"
- deleteLinkColor = "F44336"
- selectLinkColor = "78909C"
- updateLinkColor = "FF9800"
- callLinkColor = "BBDEFB"
-)
-
-var (
- nodeTypes = []struct {
- name string
- label string
- color string
- valid bool
- }{
- NodeTypeTable: {"table", "テーブル", tableNodeColor, true},
- NodeTypeFunction: {"func", "関数", funcNodeColor, true},
- }
- edgeTypes = []struct {
- label string
- color string
- valid bool
- }{
- EdgeTypeInsert: {"INSERT", insertLinkColor, true},
- EdgeTypeUpdate: {"UPDATE", updateLinkColor, true},
- EdgeTypeDelete: {"DELETE", deleteLinkColor, true},
- EdgeTypeSelect: {"SELECT", selectLinkColor, true},
- EdgeTypeCall: {"関数呼び出し", callLinkColor, true},
- }
-)
-
-func WriteMermaid(w io.StringWriter, nodes []*Node) error {
- _, err := w.WriteString("# DB Graph\n")
- if err != nil {
- return fmt.Errorf("failed to write header: %w", err)
- }
-
- _, err = w.WriteString("node: ")
- if err != nil {
- return fmt.Errorf("failed to write node description start: %w", err)
- }
-
- for _, nodeType := range nodeTypes {
- if nodeType.valid {
- _, err = w.WriteString(fmt.Sprintf("![](https://via.placeholder.com/16/%s/FFFFFF/?text=%%20) `%s` ", nodeType.color, nodeType.label))
- if err != nil {
- return fmt.Errorf("failed to write node description: %w", err)
- }
- }
- }
-
- _, err = w.WriteString("\n\n")
- if err != nil {
- return fmt.Errorf("failed to write node description end: %w", err)
- }
-
- _, err = w.WriteString("edge: ")
- if err != nil {
- return fmt.Errorf("failed to write edge description start: %w", err)
- }
-
- for _, edgeType := range edgeTypes {
- if edgeType.valid {
- _, err = w.WriteString(fmt.Sprintf("![](https://via.placeholder.com/16/%s/FFFFFF/?text=%%20) `%s` ", edgeType.color, edgeType.label))
- if err != nil {
- return fmt.Errorf("failed to write edge description: %w", err)
- }
- }
- }
-
- _, err = w.WriteString("\n")
- if err != nil {
- return fmt.Errorf("failed to write edge description end: %w", err)
- }
-
- _, err = w.WriteString("```mermaid\n" +
- "graph LR\n")
- if err != nil {
- return fmt.Errorf("failed to write header: %w", err)
- }
-
- for _, nodeType := range nodeTypes {
- if nodeType.valid {
- _, err = w.WriteString(fmt.Sprintf(" classDef %s fill:#%s,fill-opacity:0.5\n", nodeType.name, nodeType.color))
- if err != nil {
- return fmt.Errorf("failed to write class def: %w", err)
- }
- }
- }
-
- edgeLinksMap := map[EdgeType][]string{}
- edgeID := 0
- for _, node := range nodes {
- var src string
- if nodeType := nodeTypes[node.NodeType]; nodeType.valid {
- src = fmt.Sprintf("%s[%s]:::%s", node.ID, node.Label, nodeType.name)
- } else {
- log.Printf("unknown node type: %v\n", node.NodeType)
- continue
- }
-
- for _, edge := range node.Edges {
- var dst string
- if nodeType := nodeTypes[edge.Node.NodeType]; nodeType.valid {
- dst = fmt.Sprintf("%s[%s]:::%s", edge.Node.ID, edge.Node.Label, nodeType.name)
- } else {
- log.Printf("unknown node type: %v\n", node.NodeType)
- continue
- }
-
- line := "--"
- if edge.InLoop {
- line = "=="
- }
-
- var edgeExpr string
- if edge.Label == "" {
- edgeExpr = fmt.Sprintf("%s>", line)
- } else {
- edgeExpr = fmt.Sprintf("%s %s %s>", line, edge.Label, line)
- }
- _, err = w.WriteString(fmt.Sprintf(" %s %s %s\n", src, edgeExpr, dst))
- if err != nil {
- return fmt.Errorf("failed to write edge: %w", err)
- }
-
- edgeLinksMap[edge.EdgeType] = append(edgeLinksMap[edge.EdgeType], strconv.Itoa(edgeID))
-
- edgeID++
- }
- }
-
- for edgeType, links := range edgeLinksMap {
- if len(links) == 0 {
- continue
- }
- if info := edgeTypes[edgeType]; info.valid {
- _, err = w.WriteString(fmt.Sprintf(" linkStyle %s stroke:#%s,stroke-width:2px\n", strings.Join(links, ","), info.color))
- if err != nil {
- return fmt.Errorf("failed to write link style: %w", err)
- }
- } else {
- log.Printf("unknown edge type: %v\n", edgeType)
- }
- }
-
- _, err = w.WriteString("```")
- if err != nil {
- return fmt.Errorf("failed to write footer: %w", err)
- }
-
- return nil
-}
diff --git a/go.mod b/go.mod
index c57c850..f917535 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/mazrean/isucrud
-go 1.22.0
+go 1.23
toolchain go1.22.9
diff --git a/internal/pkg/list/queue.go b/internal/pkg/list/queue.go
index 0c13853..1f76794 100644
--- a/internal/pkg/list/queue.go
+++ b/internal/pkg/list/queue.go
@@ -39,6 +39,19 @@ func (q Queue[T]) Peek() (T, bool) {
return e.Value()
}
+func (q *Queue[T]) Iter(yield func(T) bool) {
+ for e := q.l.Front(); e.e != nil; e = q.l.Front() {
+ q.l.Remove(e)
+
+ v, ok := e.Value()
+ if !ok {
+ break
+ }
+
+ yield(v)
+ }
+}
+
func (q Queue[T]) Clear() {
q.l.Init()
}
diff --git a/internal/ui/asset/template.html b/internal/ui/asset/template.html
new file mode 100644
index 0000000..55de7a6
--- /dev/null
+++ b/internal/ui/asset/template.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+ Mermaid
+
+
+
+
+
+ {{if .IsFiltered}}{{end}}
+
+ nodes:
+ {{range .NodeTypes}}
+ ■
+ {{.Label}}
+ {{end}}
+
+ edges:
+ {{range .EdgeTypes}}
+ ■
+ {{.Label}}
+ {{end}}
+
+ {{.MermaidData}}
+
+
+
\ No newline at end of file
diff --git a/internal/ui/asset/template.md b/internal/ui/asset/template.md
new file mode 100644
index 0000000..ccd3222
--- /dev/null
+++ b/internal/ui/asset/template.md
@@ -0,0 +1,7 @@
+# DB Graph
+node: {{range .NodeTypes}}![](https://via.placeholder.com/16/{{.Color}}/FFFFFF/?text=%20) `{{.Label}}` {{end}}
+
+edge: {{range .EdgeTypes}}![](https://via.placeholder.com/16/{{.Color}}/FFFFFF/?text=%20) `{{.Label}}` {{end}}
+```mermaid
+{{.MermaidData}}
+```
diff --git a/internal/ui/mermaid.go b/internal/ui/mermaid.go
new file mode 100644
index 0000000..51e41f9
--- /dev/null
+++ b/internal/ui/mermaid.go
@@ -0,0 +1,135 @@
+package ui
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "strconv"
+ "strings"
+
+ "github.com/mazrean/isucrud/dbdoc"
+)
+
+const (
+ funcNodeColor = "1976D2"
+ tableNodeColor = "795548"
+ insertLinkColor = "CDDC39"
+ deleteLinkColor = "F44336"
+ selectLinkColor = "78909C"
+ updateLinkColor = "FF9800"
+ callLinkColor = "BBDEFB"
+)
+
+type NodeType struct {
+ name string
+ Label string
+ Color string
+ valid bool
+}
+
+type EdgeType struct {
+ Label string
+ Color string
+ valid bool
+}
+
+var (
+ nodeTypes = []NodeType{
+ dbdoc.NodeTypeTable: {"table", "テーブル", tableNodeColor, true},
+ dbdoc.NodeTypeFunction: {"func", "関数", funcNodeColor, true},
+ }
+ edgeTypes = []EdgeType{
+ dbdoc.EdgeTypeInsert: {"INSERT", insertLinkColor, true},
+ dbdoc.EdgeTypeUpdate: {"UPDATE", updateLinkColor, true},
+ dbdoc.EdgeTypeDelete: {"DELETE", deleteLinkColor, true},
+ dbdoc.EdgeTypeSelect: {"SELECT", selectLinkColor, true},
+ dbdoc.EdgeTypeCall: {"関数呼び出し", callLinkColor, true},
+ }
+)
+
+func RenderMermaid(
+ w io.StringWriter,
+ nodes []*dbdoc.Node,
+ isHttp bool,
+) error {
+ _, err := w.WriteString("graph LR\n")
+ if err != nil {
+ return fmt.Errorf("failed to write header: %w", err)
+ }
+
+ for _, nodeType := range nodeTypes {
+ if nodeType.valid {
+ _, err = w.WriteString(fmt.Sprintf(" classDef %s fill:#%s,fill-opacity:0.5\n", nodeType.name, nodeType.Color))
+ if err != nil {
+ return fmt.Errorf("failed to write class def: %w", err)
+ }
+ }
+ }
+
+ edgeLinksMap := map[dbdoc.EdgeType][]string{}
+ edgeID := 0
+ for _, node := range nodes {
+ var src string
+ if nodeType := nodeTypes[node.NodeType]; nodeType.valid {
+ src = fmt.Sprintf("%s[%s]:::%s", node.ID, node.Label, nodeType.name)
+ } else {
+ log.Printf("unknown node type: %v\n", node.NodeType)
+ continue
+ }
+
+ for _, edge := range node.Edges {
+ var dst string
+ if nodeType := nodeTypes[edge.Node.NodeType]; nodeType.valid {
+ dst = fmt.Sprintf("%s[%s]:::%s", edge.Node.ID, edge.Node.Label, nodeType.name)
+ } else {
+ log.Printf("unknown node type: %v\n", node.NodeType)
+ continue
+ }
+
+ line := "--"
+ if edge.InLoop {
+ line = "=="
+ }
+
+ var edgeExpr string
+ if edge.Label == "" {
+ edgeExpr = fmt.Sprintf("%s>", line)
+ } else {
+ edgeExpr = fmt.Sprintf("%s %s %s>", line, edge.Label, line)
+ }
+ _, err = w.WriteString(fmt.Sprintf(" %s %s %s\n", src, edgeExpr, dst))
+ if err != nil {
+ return fmt.Errorf("failed to write edge: %w", err)
+ }
+
+ edgeLinksMap[edge.EdgeType] = append(edgeLinksMap[edge.EdgeType], strconv.Itoa(edgeID))
+
+ edgeID++
+ }
+ }
+
+ if isHttp {
+ for _, node := range nodes {
+ _, err = w.WriteString(fmt.Sprintf(" click %s \"/?node=%s\"\n", node.ID, node.ID))
+ if err != nil {
+ return fmt.Errorf("failed to write click event: %w", err)
+ }
+ }
+ }
+
+ for edgeType, links := range edgeLinksMap {
+ if len(links) == 0 {
+ continue
+ }
+ if info := edgeTypes[edgeType]; info.valid {
+ _, err = w.WriteString(fmt.Sprintf(" linkStyle %s stroke:#%s,stroke-width:2px\n", strings.Join(links, ","), info.Color))
+ if err != nil {
+ return fmt.Errorf("failed to write link style: %w", err)
+ }
+ } else {
+ log.Printf("unknown edge type: %v\n", edgeType)
+ }
+ }
+
+ return nil
+}
diff --git a/internal/ui/render.go b/internal/ui/render.go
new file mode 100644
index 0000000..ebdcde1
--- /dev/null
+++ b/internal/ui/render.go
@@ -0,0 +1,178 @@
+package ui
+
+import (
+ _ "embed"
+ "fmt"
+ htmlTemplate "html/template"
+ "io"
+ "os"
+ "strings"
+ "text/template"
+
+ "github.com/mazrean/isucrud/dbdoc"
+ "github.com/mazrean/isucrud/internal/pkg/list"
+)
+
+var (
+ //go:embed asset/template.md
+ templateMarkdown string
+ //go:embed asset/template.html
+ templateHTML string
+)
+
+type TemplateParam struct {
+ IsFiltered bool
+ NodeTypes []NodeType
+ EdgeTypes []EdgeType
+ Nodes []*dbdoc.Node
+ MermaidData string
+}
+
+func RenderMarkdown(dest string, nodes []*dbdoc.Node) error {
+ f, err := os.Create(dest)
+ if err != nil {
+ return fmt.Errorf("failed to make directory: %w", err)
+ }
+ defer f.Close()
+
+ sb := &strings.Builder{}
+ err = RenderMermaid(
+ sb,
+ nodes,
+ false,
+ )
+ if err != nil {
+ return fmt.Errorf("failed to write mermaid: %w", err)
+ }
+
+ tmpl, err := template.New("markdown").Parse(templateMarkdown)
+ if err != nil {
+ return fmt.Errorf("failed to parse template: %w", err)
+ }
+
+ err = tmpl.Execute(f, TemplateParam{
+ IsFiltered: false,
+ NodeTypes: nodeTypes[1:],
+ EdgeTypes: edgeTypes[1:],
+ Nodes: nodes,
+ MermaidData: sb.String(),
+ })
+ if err != nil {
+ return fmt.Errorf("failed to execute template: %w", err)
+ }
+
+ return nil
+}
+
+func RenderHTML(w io.Writer, nodes []*dbdoc.Node, targetNodeID string) error {
+ filtered := false
+ filteredNodes := nodes
+ if targetNodeID != "" {
+ filtered = true
+ filteredNodes = filterNodes(targetNodeID, nodes)
+ }
+
+ sb := &strings.Builder{}
+ err := RenderMermaid(
+ sb,
+ filteredNodes,
+ true,
+ )
+ if err != nil {
+ return fmt.Errorf("failed to write mermaid: %w", err)
+ }
+
+ tmpl, err := htmlTemplate.New("html").Parse(templateHTML)
+ if err != nil {
+ return fmt.Errorf("failed to parse template: %w", err)
+ }
+
+ err = tmpl.Execute(w, TemplateParam{
+ IsFiltered: filtered,
+ NodeTypes: nodeTypes[1:],
+ EdgeTypes: edgeTypes[1:],
+ Nodes: nodes,
+ MermaidData: sb.String(),
+ })
+ if err != nil {
+ return fmt.Errorf("failed to execute template: %w", err)
+ }
+
+ return nil
+}
+
+func filterNodes(targetNodeID string, nodes []*dbdoc.Node) []*dbdoc.Node {
+ nodeMap := make(map[string]*dbdoc.Node)
+ for _, node := range nodes {
+ nodeMap[node.ID] = node
+ }
+
+ targetNode, exists := nodeMap[targetNodeID]
+ if !exists {
+ return []*dbdoc.Node{}
+ }
+
+ parentMap := make(map[string][]*dbdoc.Node)
+ for _, node := range nodes {
+ for _, edge := range node.Edges {
+ if edge.Node != nil {
+ parentMap[edge.Node.ID] = append(parentMap[edge.Node.ID], node)
+ }
+ }
+ }
+
+ visited := make(map[string]struct{}, len(nodes))
+ relatedNodeMap := make(map[string]*dbdoc.Node)
+
+ childNodeQueue := list.NewQueue[*dbdoc.Node]()
+ childNodeQueue.Push(targetNode)
+ for current := range childNodeQueue.Iter {
+ // Skip if already visited.
+ if _, ok := visited[current.ID]; ok {
+ continue
+ }
+ visited[current.ID] = struct{}{}
+
+ relatedNodeMap[current.ID] = current
+
+ for _, edge := range current.Edges {
+ childNodeQueue.Push(edge.Node)
+ }
+ }
+
+ visited = make(map[string]struct{}, len(nodes))
+ parentNodeQueue := list.NewQueue[*dbdoc.Node]()
+ parentNodeQueue.Push(targetNode)
+ for current := range parentNodeQueue.Iter {
+ // Skip if already visited.
+ if _, ok := visited[current.ID]; ok {
+ continue
+ }
+ visited[current.ID] = struct{}{}
+
+ relatedNodeMap[current.ID] = current
+
+ for _, parent := range parentMap[current.ID] {
+ parentNodeQueue.Push(parent)
+ }
+ }
+
+ relatedNodes := make([]*dbdoc.Node, 0, len(relatedNodeMap))
+ for _, node := range relatedNodeMap {
+ newEdges := []dbdoc.Edge{}
+ for _, edge := range node.Edges {
+ if _, ok := relatedNodeMap[edge.Node.ID]; ok {
+ newEdges = append(newEdges, edge)
+ }
+ }
+
+ relatedNodes = append(relatedNodes, &dbdoc.Node{
+ ID: node.ID,
+ Label: node.Label,
+ Edges: newEdges,
+ NodeType: node.NodeType,
+ })
+ }
+
+ return relatedNodes
+}
diff --git a/main.go b/main.go
index b84f054..d1101e2 100644
--- a/main.go
+++ b/main.go
@@ -3,9 +3,12 @@ package main
import (
"flag"
"fmt"
+ "log"
+ "net/http"
"os"
"github.com/mazrean/isucrud/dbdoc"
+ "github.com/mazrean/isucrud/internal/ui"
)
var (
@@ -17,6 +20,8 @@ var (
ignores sliceString
ignorePrefixes sliceString
ignoreMain, ignoreInitialize bool
+ web bool
+ addr string
)
func init() {
@@ -27,6 +32,8 @@ func init() {
flag.Var(&ignorePrefixes, "ignorePrefix", "ignore function")
flag.BoolVar(&ignoreMain, "ignoreMain", true, "ignore main function")
flag.BoolVar(&ignoreInitialize, "ignoreInitialize", true, "ignore functions with 'initialize' in the name")
+ flag.BoolVar(&web, "web", false, "run as web server")
+ flag.StringVar(&addr, "addr", "localhost:7070", "address to listen on")
}
func main() {
@@ -42,7 +49,7 @@ func main() {
panic(fmt.Errorf("failed to get working directory: %w", err))
}
- err = dbdoc.Run(dbdoc.Config{
+ nodes, err := dbdoc.Run(dbdoc.Config{
WorkDir: wd,
BuildArgs: flag.Args(),
IgnoreFuncs: ignores,
@@ -54,4 +61,28 @@ func main() {
if err != nil {
panic(fmt.Errorf("failed to run dbdoc: %w", err))
}
+
+ if web {
+ mux := http.NewServeMux()
+ mux.Handle("GET /", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ r.Header.Set("Content-Type", "text/html")
+
+ targetNodeID := r.URL.Query().Get("node")
+
+ err := ui.RenderHTML(w, nodes, targetNodeID)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ }))
+
+ log.Printf("open http://%s/ in your browser\n", addr)
+ if err := http.ListenAndServe(addr, mux); err != nil {
+ panic(fmt.Errorf("server exit: %w", err))
+ }
+ } else {
+ err = ui.RenderMarkdown(dst, nodes)
+ if err != nil {
+ panic(fmt.Errorf("failed to render markdown: %w", err))
+ }
+ }
}