Skip to content

Commit 972cd5c

Browse files
committed
Initial commit
0 parents  commit 972cd5c

File tree

6 files changed

+344
-0
lines changed

6 files changed

+344
-0
lines changed

go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module go-uml
2+
3+
go 1.20
4+
5+
require (
6+
github.com/fogleman/gg v1.3.0 // indirect
7+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
8+
golang.org/x/image v0.7.0 // indirect
9+
)

go.sum

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
2+
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
3+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
4+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
5+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
6+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
7+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
8+
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
9+
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
10+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
11+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
12+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
13+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
14+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
15+
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
16+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
17+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
18+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
19+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
20+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
21+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
22+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
23+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
24+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
25+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
26+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
27+
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
28+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
29+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
30+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
31+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
32+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
33+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
34+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
35+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
36+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
37+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

main.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package main
2+
3+
import "go-uml/sequence"
4+
5+
func main() {
6+
d := sequence.NewDiagram()
7+
8+
client := "Client"
9+
backend := "Backend"
10+
db := "DynamoDB"
11+
12+
d.SetTitle("User initiates chat with another User")
13+
d.AddParticipant(client)
14+
d.AddParticipant(backend)
15+
d.AddParticipant(db)
16+
17+
d.AddDirectionalEdge(client, backend, "PUT /chat/user/<TO_ID>")
18+
d.AddDirectionalEdge(backend, db, "checks or create inbox for user")
19+
d.AddDirectionalEdge(backend, client, "return chat meta data + chat secret")
20+
d.AddDirectionalEdge(client, backend, "WS /joinchat/<CHAT_ID> payload: secret")
21+
d.AddDirectionalEdge(backend, db, "compare inbox secret")
22+
d.AddDirectionalEdge(backend, db, "set all unread messages to read")
23+
d.AddDirectionalEdge(backend, db, "store connection ID for user")
24+
d.AddDirectionalEdge(backend, client, "202 - You're good to go")
25+
d.AddDirectionalEdge(client, backend, "WS /sendmessage/<CHAT_ID> payload: hello!")
26+
d.AddDirectionalEdge(backend, db, "get connection ID for <TO_ID>")
27+
d.AddDirectionalEdge(backend, backend, "notify <TO_ID> with connection ID if present")
28+
d.AddDirectionalEdge(backend, db, "store message in chat")
29+
d.AddDirectionalEdge(client, backend, "disconnect")
30+
d.AddDirectionalEdge(backend, db, "remove connection ID for user")
31+
32+
d.Render()
33+
34+
}

out.png

14 KB
Loading

sequence/diagram.go

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package sequence
2+
3+
import (
4+
"fmt"
5+
"github.com/fogleman/gg"
6+
"image/color"
7+
"log"
8+
)
9+
10+
const (
11+
participantBoxWidth = 100.0
12+
participantBoxHeight = 50.0
13+
participantsPadding = 32
14+
15+
rectangleStrokeWidth = 2.0
16+
lineStrokeWidth = 1.0
17+
18+
verticalSpaceBetweenEdges = 50
19+
20+
width = 1000
21+
height = 1000
22+
)
23+
24+
type Diagram struct {
25+
participants []participant
26+
edges []edge
27+
renderedParticipants []*participant
28+
participantsCoordMap map[string]participantCoord
29+
30+
dc *gg.Context
31+
title string
32+
}
33+
34+
func NewDiagram() *Diagram {
35+
coordMap := make(map[string]participantCoord)
36+
37+
return &Diagram{
38+
participantsCoordMap: coordMap,
39+
}
40+
}
41+
42+
func (d *Diagram) Render() {
43+
d.dc = gg.NewContext(width, height)
44+
d.dc.DrawRectangle(0, 0, width, height)
45+
d.dc.SetColor(color.White)
46+
d.dc.Fill()
47+
48+
d.renderTitle()
49+
d.renderParticipants()
50+
d.renderEdges()
51+
52+
d.dc.SavePNG("out.png")
53+
}
54+
55+
func (d *Diagram) renderTitle() {
56+
s := d.title
57+
textWidth, _ := d.dc.MeasureString(s)
58+
centerX := float64(d.dc.Width())/2.0 - float64(textWidth)/2.0
59+
log.Printf("title: %s", d.title)
60+
d.dc.SetColor(color.Black)
61+
d.dc.DrawString(s, centerX, height*0.05)
62+
d.dc.Stroke()
63+
}
64+
65+
func (d *Diagram) renderParticipants() {
66+
for idx := range d.participants {
67+
p := &d.participants[idx]
68+
69+
for rIdx := range d.renderedParticipants {
70+
if d.renderedParticipants[rIdx].Name == p.Name {
71+
return
72+
}
73+
}
74+
spacePerBlock := float64(d.dc.Width() / len(d.participants))
75+
startX := spacePerBlock*float64(len(d.renderedParticipants)+1) - spacePerBlock/2 - participantsPadding
76+
// startX := float64(participantsPadding + (len(d.renderedParticipants) * (participantBoxWidth + 1000/(len(d.participants)))))
77+
endX := startX + participantBoxWidth
78+
startY := 1000 * 0.1 // 10% from the top
79+
endY := startY + participantBoxHeight
80+
// draw the border
81+
d.dc.SetColor(color.Black)
82+
d.dc.SetLineWidth(rectangleStrokeWidth)
83+
d.dc.SetFillRule(gg.FillRuleEvenOdd)
84+
85+
d.dc.DrawLine(startX, startY, endX, startY)
86+
d.dc.Stroke()
87+
88+
d.dc.DrawLine(startX, endY, endX, endY)
89+
d.dc.Stroke()
90+
91+
d.dc.DrawLine(startX, startY, startX, endY)
92+
d.dc.Stroke()
93+
94+
d.dc.DrawLine(endX, startY, endX, endY)
95+
d.dc.Stroke()
96+
97+
d.dc.SetColor(color.Gray{Y: 230})
98+
d.dc.DrawRectangle(
99+
startX,
100+
startY,
101+
participantBoxWidth,
102+
participantBoxHeight,
103+
)
104+
d.dc.SetColor(color.Black)
105+
strWidth, strHeight := d.dc.MeasureString(p.Name)
106+
centerStrWidth := startX + ((endX - startX) / 2) - strWidth/2
107+
centerStrHeight := (endY-startY)/2 + startY + (strHeight / 2)
108+
109+
d.dc.DrawString(
110+
p.Name,
111+
centerStrWidth,
112+
centerStrHeight,
113+
)
114+
d.dc.Stroke()
115+
116+
// render vertical action line for each participant
117+
centerX := startX + (endX-startX)/2 - 2.5
118+
lineStartY := endY + 2.5
119+
lineEndY := float64(len(d.edges)*(verticalSpaceBetweenEdges)) + lineStartY + verticalSpaceBetweenEdges // padding
120+
121+
d.dc.SetLineWidth(lineStrokeWidth)
122+
d.dc.DrawLine(centerX, lineStartY, centerX, lineEndY)
123+
d.dc.Stroke()
124+
d.renderedParticipants = append(d.renderedParticipants, p)
125+
126+
d.participantsCoordMap[p.Name] = participantCoord{
127+
X: startX,
128+
Y: startY,
129+
}
130+
}
131+
}
132+
133+
func (d *Diagram) renderEdges() {
134+
renderedEdges := 0
135+
136+
for idx := range d.edges {
137+
e := &d.edges[idx]
138+
fromCords := d.participantsCoordMap[e.from.Name]
139+
toCords := d.participantsCoordMap[e.to.Name]
140+
startX := fromCords.X + participantBoxWidth/2 - 2.5 // 2.5 = half of stroke width
141+
startY := fromCords.Y + participantBoxHeight + 2.5 + float64((1+renderedEdges)*verticalSpaceBetweenEdges)
142+
endX := toCords.X + participantBoxWidth/2 - 2.5
143+
isReverseEdge := endX < startX
144+
145+
d.dc.SetDash(6)
146+
d.dc.DrawLine(
147+
startX,
148+
startY,
149+
endX,
150+
startY)
151+
d.dc.Stroke()
152+
153+
d.dc.SetDash()
154+
155+
if e.directional {
156+
arrowTipStartX := endX
157+
var arrowTipEndX float64
158+
159+
if isReverseEdge {
160+
arrowTipEndX = arrowTipStartX + 10
161+
} else {
162+
arrowTipEndX = arrowTipStartX - 10
163+
}
164+
d.dc.DrawLine(arrowTipStartX, startY, arrowTipEndX, startY-10)
165+
d.dc.DrawLine(arrowTipStartX, startY, arrowTipEndX, startY+10)
166+
d.dc.Stroke()
167+
}
168+
169+
if e.Label != "" {
170+
textWidth, textHeight := d.dc.MeasureString(e.Label)
171+
textY := startY + textHeight
172+
textX := startX
173+
if isReverseEdge {
174+
textX -= participantsPadding / 2
175+
textX -= textWidth
176+
} else {
177+
textX += participantsPadding / 2
178+
}
179+
180+
d.dc.DrawString(e.Label, textX, textY)
181+
}
182+
183+
renderedEdges++
184+
}
185+
}
186+
187+
func (d *Diagram) AddParticipant(name string) {
188+
for i := range d.participants {
189+
if d.participants[i].Name == name {
190+
return
191+
}
192+
}
193+
d.participants = append(d.participants, participant{Name: name})
194+
}
195+
196+
func (d *Diagram) AddDirectionalEdge(from, to string, label string) error {
197+
var fromPar *participant
198+
var toPar *participant
199+
for i := range d.participants {
200+
if d.participants[i].Name == from {
201+
fromPar = &d.participants[i]
202+
}
203+
if d.participants[i].Name == to {
204+
toPar = &d.participants[i]
205+
}
206+
}
207+
if fromPar == nil {
208+
panic(fmt.Sprintf("participant \"%s\" not found", from))
209+
}
210+
if toPar == nil {
211+
panic(fmt.Sprintf("participant \"%s not found", to))
212+
}
213+
214+
d.edges = append(d.edges, edge{from: *fromPar, to: *toPar, Label: label, directional: true})
215+
return nil
216+
}
217+
218+
func (d *Diagram) AddUndirectionalEdge(from, to string, label string) error {
219+
var fromPar *participant
220+
var toPar *participant
221+
for i := range d.participants {
222+
if d.participants[i].Name == from {
223+
fromPar = &d.participants[i]
224+
}
225+
if d.participants[i].Name == to {
226+
toPar = &d.participants[i]
227+
}
228+
}
229+
if fromPar == nil || toPar == nil {
230+
return fmt.Errorf("participant not found")
231+
}
232+
233+
d.edges = append(d.edges, edge{from: *fromPar, to: *toPar, Label: label, directional: false})
234+
return nil
235+
}
236+
237+
func (d *Diagram) SetTitle(s string) {
238+
d.title = s
239+
}

sequence/type.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package sequence
2+
3+
type participant struct {
4+
Name string
5+
}
6+
7+
type edge struct {
8+
from participant
9+
to participant
10+
directional bool
11+
Label string
12+
}
13+
14+
func (e *edge) From() participant {
15+
return e.from
16+
}
17+
18+
func (e *edge) To() participant {
19+
return e.to
20+
}
21+
22+
type participantCoord struct {
23+
X float64
24+
Y float64
25+
}

0 commit comments

Comments
 (0)