d2 icon indicating copy to clipboard operation
d2 copied to clipboard

Creating d2 files programmatically questions

Open ag4545 opened this issue 11 months ago • 3 comments

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.

  1. create nodes
  2. set classes of nodes (ie child_a.class: db)

What I haven't been able to do

  1. directions (ie child_a -> child_b)
  2. links (ie child_a: {link: layers.roles}
  3. 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

ag4545 avatar Feb 13 '25 12:02 ag4545

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

alixander avatar Feb 13 '25 17:02 alixander

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

ag4545 avatar Feb 26 '25 10:02 ag4545

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

ag4545 avatar Feb 27 '25 11:02 ag4545

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.

ag4545 avatar Mar 22 '25 15:03 ag4545

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

alixander avatar Mar 25 '25 01:03 alixander