Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating d2 files programmatically questions #2348

Open
ag4545 opened this issue Feb 13, 2025 · 5 comments
Open

Creating d2 files programmatically questions #2348

ag4545 opened this issue Feb 13, 2025 · 5 comments

Comments

@ag4545
Copy link

ag4545 commented Feb 13, 2025

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
@cyborg-ts cyborg-ts added this to D2 Feb 13, 2025
@alixander
Copy link
Collaborator

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

@ag4545
Copy link
Author

ag4545 commented Feb 26, 2025

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
Copy link
Author

ag4545 commented Feb 27, 2025

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
Copy link
Author

ag4545 commented Mar 22, 2025

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.

@alixander
Copy link
Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

No branches or pull requests

2 participants