Skip to content

Commit

Permalink
Merge pull request terrastruct#766 from alixander/link-layers
Browse files Browse the repository at this point in the history
link to boards
  • Loading branch information
alixander authored Mar 3, 2023
2 parents 996a1c6 + e13be5c commit 7bc4955
Show file tree
Hide file tree
Showing 13 changed files with 4,511 additions and 6 deletions.
75 changes: 73 additions & 2 deletions d2compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ func compileIR(ast *d2ast.Map, m *d2ir.Map) (*d2graph.Graph, error) {
if len(c.err.Errors) > 0 {
return nil, c.err
}
c.validateBoardLinks(g)
if len(c.err.Errors) > 0 {
return nil, c.err
}
return g, nil
}

Expand Down Expand Up @@ -93,6 +97,7 @@ func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName
continue
}
g2 := d2graph.NewGraph()
g2.Parent = g
g2.AST = g.AST
c.compileBoard(g2, f.Map())
g2.Name = f.Name
Expand Down Expand Up @@ -193,7 +198,7 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
obj.Map = fr.Context.Key.Value.Map
}
}
scopeObjIDA := d2ir.IDA(fr.Context.ScopeMap)
scopeObjIDA := d2ir.BoardIDA(fr.Context.ScopeMap)
scopeObj := obj.Graph.Root.EnsureChildIDVal(scopeObjIDA)
obj.References = append(obj.References, d2graph.Reference{
Key: fr.KeyPath,
Expand Down Expand Up @@ -431,7 +436,7 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {

edge.Attributes.Label.MapKey = e.LastPrimaryKey()
for _, er := range e.References {
scopeObjIDA := d2ir.IDA(er.Context.ScopeMap)
scopeObjIDA := d2ir.BoardIDA(er.Context.ScopeMap)
scopeObj := edge.Src.Graph.Root.EnsureChildIDVal(scopeObjIDA)
edge.References = append(edge.References, d2graph.EdgeReference{
Edge: er.Context.Edge,
Expand Down Expand Up @@ -715,6 +720,72 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
}
}

func (c *compiler) validateBoardLinks(g *d2graph.Graph) {
for _, obj := range g.Objects {
if obj.Attributes.Link == nil {
continue
}

linkKey, err := d2parser.ParseKey(obj.Attributes.Link.Value)
if err != nil {
continue
}

if linkKey.Path[0].Unbox().ScalarString() != "root" {
continue
}

if !hasBoard(g.RootBoard(), linkKey.IDA()) {
c.errorf(obj.Attributes.Link.MapKey, "linked board not found")
continue
}
}
for _, b := range g.Layers {
c.validateBoardLinks(b)
}
for _, b := range g.Scenarios {
c.validateBoardLinks(b)
}
for _, b := range g.Steps {
c.validateBoardLinks(b)
}
}

func hasBoard(root *d2graph.Graph, ida []string) bool {
if len(ida) == 0 {
return true
}
if ida[0] == "root" {
return hasBoard(root, ida[1:])
}
id := ida[0]
if len(ida) == 1 {
return root.Name == id
}
nextID := ida[1]
switch id {
case "layers":
for _, b := range root.Layers {
if b.Name == nextID {
return hasBoard(b, ida[2:])
}
}
case "scenarios":
for _, b := range root.Scenarios {
if b.Name == nextID {
return hasBoard(b, ida[2:])
}
}
case "steps":
for _, b := range root.Steps {
if b.Name == nextID {
return hasBoard(b, ida[2:])
}
}
}
return false
}

func init() {
FullToShortLanguageAliases = make(map[string]string, len(ShortToFullLanguageAliases))
for k, v := range ShortToFullLanguageAliases {
Expand Down
115 changes: 115 additions & 0 deletions d2compiler/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2027,6 +2027,121 @@ Chinchillas_Collectibles.chinchilla -> Chinchillas.id`,
tassert.Equal(t, 2, *g.Edges[0].SrcTableColumnIndex)
},
},
{
name: "link-board-ok",
text: `x.link: layers.x
layers: {
x: {
y
}
}`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "root.layers.x", g.Objects[0].Attributes.Link.Value)
},
},
{
name: "link-board-mixed",
text: `question: How does the cat go?
question.link: layers.cat
layers: {
cat: {
the cat -> meeeowwww: goes
}
}
scenarios: {
green: {
question.style.fill: green
}
}`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "root.layers.cat", g.Objects[0].Attributes.Link.Value)
tassert.Equal(t, "root.layers.cat", g.Scenarios[0].Objects[0].Attributes.Link.Value)
},
},
{
name: "link-board-not-found",
text: `x.link: layers.x
`,
expErr: `d2/testdata/d2compiler/TestCompile/link-board-not-found.d2:1:1: linked board not found`,
},
{
name: "link-board-not-board",
text: `zzz
x.link: layers.x.y
layers: {
x: {
y
}
}`,
expErr: `d2/testdata/d2compiler/TestCompile/link-board-not-board.d2:2:1: linked board not found`,
},
{
name: "link-board-nested",
text: `x.link: layers.x.layers.x
layers: {
x: {
layers: {
x: {
hello
}
}
}
}`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "root.layers.x.layers.x", g.Objects[0].Attributes.Link.Value)
},
},
{
name: "link-board-key-nested",
text: `x: {
y.link: layers.x
}
layers: {
x: {
yo
}
}`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "root.layers.x", g.Objects[1].Attributes.Link.Value)
},
},
{
name: "link-board-underscore",
text: `x
layers: {
x: {
yo
layers: {
x: {
hello.link: _._.layers.x
hey.link: _
}
}
}
}`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.NotNil(t, g.Layers[0].Layers[0].Objects[0].Attributes.Link.Value)
tassert.Equal(t, "root.layers.x", g.Layers[0].Layers[0].Objects[0].Attributes.Link.Value)
tassert.Equal(t, "root.layers.x", g.Layers[0].Layers[0].Objects[1].Attributes.Link.Value)
},
},
{
name: "link-board-underscore-not-found",
text: `x
layers: {
x: {
yo
layers: {
x: {
hello.link: _._._
}
}
}
}`,
expErr: `d2/testdata/d2compiler/TestCompile/link-board-underscore-not-found.d2:7:9: invalid underscore usage`,
},
}

for _, tc := range testCases {
Expand Down
10 changes: 9 additions & 1 deletion d2graph/d2graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ const DEFAULT_SHAPE_SIZE = 100.
const MIN_SHAPE_SIZE = 5

type Graph struct {
Name string `json:"name"`
Parent *Graph `json:"-"`
Name string `json:"name"`
// IsFolderOnly indicates a board or scenario itself makes no modifications from its
// base. Folder only boards do not have a render and are used purely for organizing
// the board tree.
Expand All @@ -55,6 +56,13 @@ func NewGraph() *Graph {
return d
}

func (g *Graph) RootBoard() *Graph {
for g.Parent != nil {
g = g.Parent
}
return g
}

// TODO consider having different Scalar types
// Right now we'll hold any types in Value and just convert, e.g. floats
type Scalar struct {
Expand Down
67 changes: 67 additions & 0 deletions d2ir/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package d2ir

import (
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser"
)

Expand Down Expand Up @@ -127,13 +128,79 @@ func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext)
}
c.compileMap(f.Map(), refctx.Key.Value.Map)
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
// If the link is a board, we need to transform it into an absolute path.
if f.Name == "link" {
c.compileLink(refctx)
}
f.Primary_ = &Scalar{
parent: f,
Value: refctx.Key.Value.ScalarBox().Unbox(),
}
}
}

func (c *compiler) compileLink(refctx *RefContext) {
val := refctx.Key.Value.ScalarBox().Unbox().ScalarString()
link, err := d2parser.ParseKey(val)
if err != nil {
return
}

scopeIDA := IDA(refctx.ScopeMap)

if len(scopeIDA) == 0 {
return
}

linkIDA := link.IDA()
if len(linkIDA) == 0 {
return
}

if linkIDA[0] == "root" {
c.errorf(refctx.Key.Key, "cannot refer to root in link")
return
}

// If it doesn't start with one of these reserved words, the link is definitely not a board link.
if linkIDA[0] != "layers" && linkIDA[0] != "scenarios" && linkIDA[0] != "steps" && linkIDA[0] != "_" {
return
}

// Chop off the non-board portion of the scope, like if this is being defined on a nested object (e.g. `x.y.z`)
for i := len(scopeIDA) - 1; i > 0; i-- {
if scopeIDA[i-1] == "layers" || scopeIDA[i-1] == "scenarios" || scopeIDA[i-1] == "steps" {
scopeIDA = scopeIDA[:i+1]
break
}
if scopeIDA[i-1] == "root" {
scopeIDA = scopeIDA[:i]
break
}
}

// Resolve underscores
for len(linkIDA) > 0 && linkIDA[0] == "_" {
if len(scopeIDA) < 2 {
// IR compiler only validates bad underscore usage
// The compiler will validate if the target board actually exists
c.errorf(refctx.Key.Key, "invalid underscore usage")
return
}
// pop 2 off path per one underscore
scopeIDA = scopeIDA[:len(scopeIDA)-2]
linkIDA = linkIDA[1:]
}
if len(scopeIDA) == 0 {
scopeIDA = []string{"root"}
}

// Create the absolute path by appending scope path with value specified
scopeIDA = append(scopeIDA, linkIDA...)
kp := d2ast.MakeKeyPath(scopeIDA)
refctx.Key.Value = d2ast.MakeValueBox(d2ast.RawString(d2format.Format(kp), true))
}

func (c *compiler) compileEdges(refctx *RefContext) {
if refctx.Key.Key != nil {
f, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx)
Expand Down
26 changes: 23 additions & 3 deletions d2ir/d2ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ type Map struct {

func (m *Map) initRoot() {
m.parent = &Field{
Name: "",
Name: "root",
References: []*FieldReference{{
Context: &RefContext{
ScopeMap: m,
Expand Down Expand Up @@ -1033,8 +1033,8 @@ func parentPrimaryKey(n Node) *d2ast.Key {
return nil
}

// IDA returns the absolute path to n from the nearest board root.
func IDA(n Node) (ida []string) {
// BoardIDA returns the absolute path to n from the nearest board root.
func BoardIDA(n Node) (ida []string) {
for {
f, ok := n.(*Field)
if ok {
Expand All @@ -1053,6 +1053,26 @@ func IDA(n Node) (ida []string) {
}
}

// IDA returns the absolute path to n.
func IDA(n Node) (ida []string) {
for {
f, ok := n.(*Field)
if ok {
ida = append(ida, f.Name)
if f.Root() {
reverseIDA(ida)
return ida
}
}
f = ParentField(n)
if f == nil {
reverseIDA(ida)
return ida
}
n = f
}
}

func reverseIDA(ida []string) {
for i := 0; i < len(ida)/2; i++ {
tmp := ida[i]
Expand Down
Loading

0 comments on commit 7bc4955

Please sign in to comment.