Creating d2 files programmatically questions
I've been trying to create d2 files programmatically based on code from your blog post.
Summary
What I can do
See below for code.
- create nodes
- set classes of nodes (ie
child_a.class: db)
What I haven't been able to do
- directions (ie
child_a -> child_b) - links (ie
child_a: {link: layers.roles} - nested nodes eg:
Parent: some description {
child_a: {
link: layers.roles
}
}
Want
This is an example of what I'm trying to generate. Could you help steer me in the right direction?
Parent: some description {
child_a: {
class: db
link: layers.roles
}
child_b.class: db
# flow
direction: right
child_a -> child_b
}
layers: {
roles: {
Roles: some description {
child_a: {
read
write
}
}
}
}
Got
This is the best I've come up with so far.
Source
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2oracle"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/textmeasure"
)
func main() {
ctx := context.Background()
_, graph, _ := d2lib.Compile(ctx, "", nil)
ruler, _ := textmeasure.NewRuler()
var err error
dbs := []string{"child_a", "child_b"}
for _, db := range dbs {
graph, err = createGraph(db, graph)
if err != nil {
log.Error(ctx, err.Error())
}
}
var out []byte
out, err = RenderGraph(graph, ruler)
if err == nil {
err = os.WriteFile(filepath.Join("svgs", "database.svg"), out, 0600)
if err != nil {
log.Error(ctx, err.Error())
}
err = os.WriteFile("out.d2", []byte(d2format.Format(graph.AST)), 0600)
if err != nil {
log.Error(ctx, err.Error())
}
} else {
log.Error(ctx, err.Error())
}
}
func createGraph(db string, g *d2graph.Graph) (*d2graph.Graph, error) {
_, newKey, err := d2oracle.Create(g, db)
if err != nil {
return nil, err
}
class := "db"
return d2oracle.Set(g, fmt.Sprintf("%s.class", newKey), nil, &class)
}
func RenderGraph(graph *d2graph.Graph, ruler *textmeasure.Ruler) ([]byte, error) {
script := d2format.Format(graph.AST)
diagram, _, _ := d2lib.Compile(context.Background(), script, &d2lib.CompileOptions{
Layout: d2dagrelayout.DefaultLayout,
Ruler: ruler,
})
return d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: d2svg.DEFAULT_PADDING,
})
}
Output d2 file
child_a
child_a.class: db
child_b
child_b.class: db
The tests for our programmatic API is probably the most extensive out of any subpackage in D2. You'll find examples for anything you're looking to do with the API there: https://github.com/alixander/d2/blob/master/d2oracle/edit_test.go
For example, for directions (ie child_a -> child_b), this is a creation, you'll find a test under TestCreate, look up how the test uses those arguments, and apply it to your own: d2oracle.Create(g, nil, "Parent.child_a -> Parent.child_b")
Thanks for you help. I see that the package has been updated quite a lot since the blog post was written . I updated my go.mod to oss.terrastruct.com/d2 v0.6.9 and updated the script (see bleow) to match the new API (which has changed). I have the following but it's now erroring with the following:
ERROR failed to create "child_a": board [x] not found
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x30 pc=0x10136bbdc]
I can't see how the board is being created in the test cases in this file you linked to: https://github.com/alixander/d2/blob/master/d2oracle/edit_test.go
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2oracle"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/util-go/go2"
)
func layoutResolver(engine string) (d2graph.LayoutGraph, error) {
return d2dagrelayout.DefaultLayout, nil
}
func main() {
ctx := context.Background()
ruler, _ := textmeasure.NewRuler()
opts := &d2lib.CompileOptions{
Ruler: ruler,
LayoutResolver: layoutResolver,
Layout: go2.Pointer("dagre"),
}
_, graph, _ := d2lib.Compile(ctx, "", opts, nil)
var err error
dbs := []string{"child_a", "child_b"}
for _, db := range dbs {
graph, err = createGraph(db, graph)
if err != nil {
log.Error(ctx, err.Error())
}
}
var out []byte
out, err = RenderGraph(graph, ruler)
if err == nil {
err = os.WriteFile(filepath.Join("svgs", "database.svg"), out, 0600)
if err != nil {
log.Error(ctx, err.Error())
}
err = os.WriteFile("out.d2", []byte(d2format.Format(graph.AST)), 0600)
if err != nil {
log.Error(ctx, err.Error())
}
} else {
log.Error(ctx, err.Error())
}
}
func createGraph(db string, g *d2graph.Graph) (*d2graph.Graph, error) {
g, _, err := d2oracle.Create(g, []string{"x"}, db)
if err != nil {
return nil, err
}
return d2oracle.Set(g, []string{"x"}, fmt.Sprintf("%s.style.stroke-width", db), nil, go2.Pointer(`3`))
}
func RenderGraph(graph *d2graph.Graph, ruler *textmeasure.Ruler) ([]byte, error) {
script := d2format.Format(graph.AST)
opts := &d2lib.CompileOptions{
Ruler: ruler,
LayoutResolver: layoutResolver,
Layout: go2.Pointer("dagre"),
}
diagram, _, _ := d2lib.Compile(context.Background(), script, opts, nil)
return d2svg.Render(diagram, &d2svg.RenderOpts{})
}
Below is a simplified version of what I'm trying to create. Ultimately I want to loop through a JSON list of objects and dynamically create the diagram.
...@classes
DataWarehouse: Warehouse 2.0 {
ingest_src_a: {
class: db
link: layers.dbroles
}
datamart.class: db
# flow
direction: right
ingest_src_a -> datamart
}
layers: {
dbroles: {
DBRoles: Database Roles {
ingest_src_a: {
read
write
}
}
DBRoleGrants: Database Role Grants: {
ingest_src_a: {
read: {
shape: sql_table
database_privileges: USAGE
schemas_all_privileges: USAGE
}
}
}
IngestSrcAReadAccountRoles: Granted to Account Roles {
shape: sql_table
data_engineer
data_analyst
}
DBRoles.ingest_src_a.read -> DBRoleGrants.ingest_src_a.read -> IngestSrcAReadAccountRoles
}
}
Hi @alixander just wondering if you saw my last comments?
If you give me some pointers I'd be happy writing an updated blog post to showcase the new API and the extra functionality as I'm sure others will likely benefit from it.
Hi, to create a board in root:
g, _, err := d2oracle.Create(g, nil, "layers.dbroles")
After, to add things to that, you use that as the board path
g, _, err := d2oracle.Create(g, "layers.dbroles", "DBRoles")