diff --git a/d2format/format.go b/d2format/format.go index a45d55f63..b7eca3bca 100644 --- a/d2format/format.go +++ b/d2format/format.go @@ -397,3 +397,16 @@ func (p *printer) edgeIndex(ei *d2ast.EdgeIndex) { } p.sb.WriteByte(']') } + +func KeyPath(kp *d2ast.KeyPath) []string { + var ids []string + for _, s := range kp.Path { + // We format each string of the key to ensure the resulting strings can be parsed + // correctly. + n := &d2ast.KeyPath{ + Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(s.Unbox().ScalarString(), true)).StringBox()}, + } + ids = append(ids, Format(n)) + } + return ids +} diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go index 3822299e2..aa0c78ab7 100644 --- a/d2graph/d2graph.go +++ b/d2graph/d2graph.go @@ -1238,16 +1238,7 @@ func (g *Graph) Texts() []*d2target.MText { } func Key(k *d2ast.KeyPath) []string { - var ids []string - for _, s := range k.Path { - // We format each string of the key to ensure the resulting strings can be parsed - // correctly. - n := &d2ast.KeyPath{ - Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(s.Unbox().ScalarString(), true)).StringBox()}, - } - ids = append(ids, d2format.Format(n)) - } - return ids + return d2format.KeyPath(k) } var ReservedKeywords = map[string]struct{}{ diff --git a/d2ir/apply.go b/d2ir/apply.go index 9fd025578..462c2cc88 100644 --- a/d2ir/apply.go +++ b/d2ir/apply.go @@ -4,7 +4,7 @@ import ( "fmt" "oss.terrastruct.com/d2/d2ast" - "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2parser" ) @@ -23,28 +23,29 @@ func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) { func Apply(dst *Map, ast *d2ast.Map) error { var c compiler - c.apply(dst, ast) + c.compileMap(dst, ast) if !c.err.Empty() { return c.err } return nil } -func (c *compiler) apply(dst *Map, ast *d2ast.Map) { +func (c *compiler) compileMap(dst *Map, ast *d2ast.Map) { for _, n := range ast.Nodes { - if n.MapKey == nil { - continue + switch { + case n.MapKey != nil: + c.compileField(dst, n.MapKey) + case n.Substitution != nil: + panic("TODO") } - - c.applyKey(dst, n.MapKey) } } -func (c *compiler) applyKey(dst *Map, k *d2ast.Key) { +func (c *compiler) compileField(dst *Map, k *d2ast.Key) { if k.Key != nil && len(k.Key.Path) > 0 { - f, ok := dst.Ensure(d2graph.Key(k.Key)) + f, ok := dst.Ensure(d2format.KeyPath(k.Key)) if !ok { - c.errorf(k.Key, "cannot index into array") + c.errorf(k, "cannot index into array") return } @@ -55,6 +56,28 @@ func (c *compiler) applyKey(dst *Map, k *d2ast.Key) { Value: k.Primary.Unbox(), } } + if k.Value.Array != nil { + a := &Array{ + parent: f, + } + c.compileArray(a, k.Value.Array) + f.Composite = a + } else if k.Value.Map != nil { + m := &Map{ + parent: f, + } + c.compileMap(m, k.Value.Map) + f.Composite = m + } else if k.Value.ScalarBox().Unbox() != nil { + f.Primary = &Scalar{ + parent: f, + Value: k.Value.ScalarBox().Unbox(), + } + } } } } + +func (c *compiler) compileArray(dst *Array, a *d2ast.Array) { + panic(fmt.Sprintf("TODO")) +} diff --git a/d2ir/apply_test.go b/d2ir/apply_test.go index 5c44020cf..9e53f0a40 100644 --- a/d2ir/apply_test.go +++ b/d2ir/apply_test.go @@ -11,6 +11,7 @@ import ( "oss.terrastruct.com/util-go/diff" "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2ir" "oss.terrastruct.com/d2/d2parser" ) @@ -31,7 +32,7 @@ func testApplySimple(t *testing.T) { tca := []testCase{ { - name: "one", + name: "field", run: func(t testing.TB, m *d2ir.Map) { err := parse(t, m, `x`) assert.Success(t, err) @@ -40,6 +41,62 @@ func testApplySimple(t *testing.T) { assertField(t, m, 0, 0, nil, "x") }, }, + { + name: "field/label", + run: func(t testing.TB, m *d2ir.Map) { + err := parse(t, m, `x: yes`) + assert.Success(t, err) + assertField(t, m, 1, 0, nil) + + assertField(t, m, 0, 0, "yes", "x") + }, + }, + { + name: "field/label/nested", + run: func(t testing.TB, m *d2ir.Map) { + err := parse(t, m, `x.y: yes`) + assert.Success(t, err) + assertField(t, m, 2, 0, nil) + + assertField(t, m, 1, 0, nil, "x") + assertField(t, m, 0, 0, "yes", "x", "y") + }, + }, + { + name: "primary", + run: func(t testing.TB, m *d2ir.Map) { + err := parse(t, m, `x: yes { pqrs }`) + assert.Success(t, err) + assertField(t, m, 2, 0, nil) + + assertField(t, m, 1, 0, "yes", "x") + assertField(t, m, 0, 0, nil, "x", "pqrs") + }, + }, + { + name: "primary/nested", + run: func(t testing.TB, m *d2ir.Map) { + err := parse(t, m, `x.y: yes { pqrs }`) + assert.Success(t, err) + assertField(t, m, 3, 0, nil) + + assertField(t, m, 2, 0, nil, "x") + assertField(t, m, 1, 0, "yes", "x", "y") + assertField(t, m, 0, 0, nil, "x", "y", "pqrs") + }, + }, + { + name: "edge", + run: func(t testing.TB, m *d2ir.Map) { + err := parse(t, m, `x -> y`) + assert.Success(t, err) + assertField(t, m, 2, 1, nil) + assertEdge(t, m, 0, nil, `(x -> y)[0]`) + + assertField(t, m, 0, 0, nil, "x") + assertField(t, m, 0, 0, nil, "y") + }, + }, { name: "nested", run: func(t testing.TB, m *d2ir.Map) { @@ -53,11 +110,7 @@ func testApplySimple(t *testing.T) { assertField(t, m, 1, 0, nil, "z") assertField(t, m, 0, 0, nil, "z", "p") - assertEdge(t, m, 0, nil, &d2ir.EdgeID{ - []string{"x", "y"}, false, - []string{"z", "p"}, true, - -1, - }) + assertEdge(t, m, 0, nil, "(x.y -> z.p)[0]") }, }, { @@ -70,11 +123,7 @@ func testApplySimple(t *testing.T) { assertField(t, m, 0, 0, nil, "x") assertField(t, m, 0, 0, nil, "z") - assertEdge(t, m, 0, nil, &d2ir.EdgeID{ - []string{"x"}, false, - []string{"z"}, true, - -1, - }) + assertEdge(t, m, 0, nil, "(x -> z)[0]") }, }, } @@ -150,14 +199,30 @@ func assertField(t testing.TB, n d2ir.Node, nfields, nedges int, primary interfa assert.Equal(t, nfields, m.FieldCount()) assert.Equal(t, nedges, m.EdgeCount()) if !makeScalar(p).Equal(makeScalar(primary)) { - t.Fatalf("expected primary %#v but %#v", primary, p) + t.Fatalf("expected primary %#v but got %s", primary, p) } return f } -func assertEdge(t testing.TB, n d2ir.Node, nfields int, primary interface{}, eid *d2ir.EdgeID) *d2ir.Edge { +func parseEdgeID(t testing.TB, eids string) *d2ir.EdgeID { t.Helper() + k, err := d2parser.ParseMapKey(eids) + assert.Success(t, err) + + return &d2ir.EdgeID{ + SrcPath: d2format.KeyPath(k.Edges[0].Src), + SrcArrow: k.Edges[0].SrcArrow == "<", + DstPath: d2format.KeyPath(k.Edges[0].Dst), + DstArrow: k.Edges[0].DstArrow == ">", + Index: *k.EdgeIndex.Int, + } +} + +func assertEdge(t testing.TB, n d2ir.Node, nfields int, primary interface{}, eids string) *d2ir.Edge { + t.Helper() + + eid := parseEdgeID(t, eids) var m *d2ir.Map switch n := n.(type) { @@ -181,7 +246,7 @@ func assertEdge(t testing.TB, n d2ir.Node, nfields int, primary interface{}, eid assert.Equal(t, nfields, e.Map.FieldCount()) if !makeScalar(e.Primary).Equal(makeScalar(primary)) { - t.Fatalf("expected primary %#v but %#v", primary, e.Primary) + t.Fatalf("expected primary %#v but %s", primary, e.Primary) } return e diff --git a/d2ir/d2ir.go b/d2ir/d2ir.go index 07ceccc98..c44c3430a 100644 --- a/d2ir/d2ir.go +++ b/d2ir/d2ir.go @@ -6,6 +6,7 @@ import ( "strings" "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2format" ) type Node interface { @@ -54,10 +55,10 @@ func (n *Array) node() {} func (n *Map) node() {} func (n *Scalar) Parent() Parent { return n.parent } -func (n *Field) Parent() Parent { return n.parent } -func (n *Edge) Parent() Parent { return n.parent } -func (n *Array) Parent() Parent { return n.parent } -func (n *Map) Parent() Parent { return n.parent } +func (n *Field) Parent() Parent { return n.parent } +func (n *Edge) Parent() Parent { return n.parent } +func (n *Array) Parent() Parent { return n.parent } +func (n *Map) Parent() Parent { return n.parent } func (n *Scalar) value() {} func (n *Array) value() {} @@ -80,7 +81,17 @@ func (s *Scalar) Copy(newp Parent) Node { } func (s *Scalar) Equal(s2 *Scalar) bool { - return s.Value.ScalarString() == s2.Value.ScalarString() && s.Value.Type() == s2.Value.Type() + if _, ok := s.Value.(d2ast.String); ok { + if _, ok = s2.Value.(d2ast.String); ok { + return s.Value.ScalarString() == s2.Value.ScalarString() + } + } + return s.Value.Type() == s2.Value.Type() && s.Value.ScalarString() == s2.Value.ScalarString() + +} + +func (s *Scalar) String() string { + return d2format.Format(s.Value) } type Map struct { diff --git a/testdata/d2ir/TestApply/simple/edge.exp.json b/testdata/d2ir/TestApply/simple/edge.exp.json new file mode 100644 index 000000000..fbfc21849 --- /dev/null +++ b/testdata/d2ir/TestApply/simple/edge.exp.json @@ -0,0 +1,4 @@ +{ + "fields": null, + "edges": null +} diff --git a/testdata/d2ir/TestApply/simple/field.exp.json b/testdata/d2ir/TestApply/simple/field.exp.json new file mode 100644 index 000000000..2dcf9ab44 --- /dev/null +++ b/testdata/d2ir/TestApply/simple/field.exp.json @@ -0,0 +1,8 @@ +{ + "fields": [ + { + "name": "x" + } + ], + "edges": null +} diff --git a/testdata/d2ir/TestApply/simple/field/label.exp.json b/testdata/d2ir/TestApply/simple/field/label.exp.json new file mode 100644 index 000000000..0e70b7b57 --- /dev/null +++ b/testdata/d2ir/TestApply/simple/field/label.exp.json @@ -0,0 +1,19 @@ +{ + "fields": [ + { + "name": "x", + "primary": { + "value": { + "range": "d2/testdata/d2ir/TestApply/simple/field/label.d2,0:3:3-0:6:6", + "value": [ + { + "string": "yes", + "raw_string": "yes" + } + ] + } + } + } + ], + "edges": null +} diff --git a/testdata/d2ir/TestApply/simple/field/label/nested.exp.json b/testdata/d2ir/TestApply/simple/field/label/nested.exp.json new file mode 100644 index 000000000..fec6ca0c6 --- /dev/null +++ b/testdata/d2ir/TestApply/simple/field/label/nested.exp.json @@ -0,0 +1,27 @@ +{ + "fields": [ + { + "name": "x", + "composite": { + "fields": [ + { + "name": "y", + "primary": { + "value": { + "range": "d2/testdata/d2ir/TestApply/simple/field/label/nested.d2,0:5:5-0:8:8", + "value": [ + { + "string": "yes", + "raw_string": "yes" + } + ] + } + } + } + ], + "edges": null + } + } + ], + "edges": null +} diff --git a/testdata/d2ir/TestApply/simple/label.exp.json b/testdata/d2ir/TestApply/simple/label.exp.json new file mode 100644 index 000000000..fd2267e5a --- /dev/null +++ b/testdata/d2ir/TestApply/simple/label.exp.json @@ -0,0 +1,19 @@ +{ + "fields": [ + { + "name": "x", + "primary": { + "value": { + "range": "d2/testdata/d2ir/TestApply/simple/label.d2,0:3:3-0:6:6", + "value": [ + { + "string": "yes", + "raw_string": "yes" + } + ] + } + } + } + ], + "edges": null +} diff --git a/testdata/d2ir/TestApply/simple/primary#01.exp.json b/testdata/d2ir/TestApply/simple/primary#01.exp.json new file mode 100644 index 000000000..c1e536d08 --- /dev/null +++ b/testdata/d2ir/TestApply/simple/primary#01.exp.json @@ -0,0 +1,27 @@ +{ + "fields": [ + { + "name": "x", + "primary": { + "value": { + "range": "d2/testdata/d2ir/TestApply/simple/primary#01.d2,0:3:3-0:6:6", + "value": [ + { + "string": "yes", + "raw_string": "yes" + } + ] + } + }, + "composite": { + "fields": [ + { + "name": "pqrs" + } + ], + "edges": null + } + } + ], + "edges": null +} diff --git a/testdata/d2ir/TestApply/simple/primary.exp.json b/testdata/d2ir/TestApply/simple/primary.exp.json new file mode 100644 index 000000000..8f512307c --- /dev/null +++ b/testdata/d2ir/TestApply/simple/primary.exp.json @@ -0,0 +1,27 @@ +{ + "fields": [ + { + "name": "x", + "primary": { + "value": { + "range": "d2/testdata/d2ir/TestApply/simple/primary.d2,0:3:3-0:6:6", + "value": [ + { + "string": "yes", + "raw_string": "yes" + } + ] + } + }, + "composite": { + "fields": [ + { + "name": "pqrs" + } + ], + "edges": null + } + } + ], + "edges": null +} diff --git a/testdata/d2ir/TestApply/simple/primary/nested.exp.json b/testdata/d2ir/TestApply/simple/primary/nested.exp.json new file mode 100644 index 000000000..ba3dcf560 --- /dev/null +++ b/testdata/d2ir/TestApply/simple/primary/nested.exp.json @@ -0,0 +1,35 @@ +{ + "fields": [ + { + "name": "x", + "composite": { + "fields": [ + { + "name": "y", + "primary": { + "value": { + "range": "d2/testdata/d2ir/TestApply/simple/primary/nested.d2,0:5:5-0:8:8", + "value": [ + { + "string": "yes", + "raw_string": "yes" + } + ] + } + }, + "composite": { + "fields": [ + { + "name": "pqrs" + } + ], + "edges": null + } + } + ], + "edges": null + } + } + ], + "edges": null +}