diff --git a/d2ir/apply.go b/d2ir/apply.go index 893bda82a..9fd025578 100644 --- a/d2ir/apply.go +++ b/d2ir/apply.go @@ -24,7 +24,10 @@ 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) - return c.err + if !c.err.Empty() { + return c.err + } + return nil } func (c *compiler) apply(dst *Map, ast *d2ast.Map) { diff --git a/d2ir/apply_test.go b/d2ir/apply_test.go index 5787e882f..32ac9281d 100644 --- a/d2ir/apply_test.go +++ b/d2ir/apply_test.go @@ -7,20 +7,17 @@ import ( "strings" "testing" + "oss.terrastruct.com/util-go/assert" "oss.terrastruct.com/util-go/diff" "oss.terrastruct.com/d2/d2ast" "oss.terrastruct.com/d2/d2ir" "oss.terrastruct.com/d2/d2parser" - "oss.terrastruct.com/d2/internal/assert" ) type testCase struct { name string - text string - base *d2ir.Map - - exp func(testing.TB, *d2ir.Map, error) + run func(testing.TB, *d2ir.Map) } func TestApply(t *testing.T) { @@ -30,12 +27,13 @@ func TestApply(t *testing.T) { } func testApplySimple(t *testing.T) { - tcs := []testCase{ + t.Parallel() + + tca := []testCase{ { name: "one", - text: `x`, - - exp: func(t testing.TB, m *d2ir.Map, err error) { + run: func(t testing.TB, m *d2ir.Map) { + err := parse(t, m, `x`) assert.Success(t, err) assertField(t, m, 1, 0, nil) @@ -44,9 +42,8 @@ func testApplySimple(t *testing.T) { }, { name: "nested", - text: `x.y -> z.p`, - - exp: func(t testing.TB, m *d2ir.Map, err error) { + run: func(t testing.TB, m *d2ir.Map) { + err := parse(t, m, `x.y -> z.p`) assert.Success(t, err) assertField(t, m, 4, 1, nil) @@ -65,9 +62,8 @@ func testApplySimple(t *testing.T) { }, { name: "underscore_parent", - text: `x._ -> z`, - - exp: func(t testing.TB, m *d2ir.Map, err error) { + run: func(t testing.TB, m *d2ir.Map) { + err := parse(t, m, `x._ -> z`) assert.Success(t, err) assertField(t, m, 2, 1, nil) @@ -83,63 +79,76 @@ func testApplySimple(t *testing.T) { }, } - runa(t, tcs) + runa(t, tca) } -func runa(t *testing.T, tcs []testCase) { - for _, tc := range tcs { +func runa(t *testing.T, tca []testCase) { + for _, tc := range tca { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - - run(t, tc) + m := &d2ir.Map{} + tc.run(t, m) }) } } -func run(t testing.TB, tc testCase) { +func parse(t testing.TB, dst *d2ir.Map, text string) error { + t.Helper() + d2Path := fmt.Sprintf("d2/testdata/d2ir/%v.d2", t.Name()) - ast, err := d2parser.Parse(d2Path, strings.NewReader(tc.text), nil) - if err != nil { - tc.exp(t, nil, err) - t.FailNow() - return - } + ast, err := d2parser.Parse(d2Path, strings.NewReader(text), nil) + assert.Success(t, err) - dst := tc.base.Copy(nil).(*d2ir.Map) err = d2ir.Apply(dst, ast) - tc.exp(t, dst, err) - - err = diff.Testdata(filepath.Join("..", "testdata", "d2ir", t.Name()), dst) if err != nil { - tc.exp(t, nil, err) - t.FailNow() - return + return err } + + err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2ir", t.Name()), dst) + return err } func assertField(t testing.TB, n d2ir.Node, nfields, nedges int, primary interface{}, ida ...string) *d2ir.Field { t.Helper() - m := d2ir.NodeToMap(n) - p := d2ir.NodeToPrimary(n) + var m *d2ir.Map + p := &d2ir.Scalar{ + Value: &d2ast.Null{}, + } + switch n := n.(type) { + case *d2ir.Field: + mm, ok := n.Composite.(*d2ir.Map) + if ok { + m = mm + } else { + t.Fatalf("unexpected d2ir.Field.Composite %T", n.Composite) + } + p = n.Primary + case *d2ir.Map: + m = n + p.Value = &d2ast.Null{} + default: + t.Fatalf("unexpected d2ir.Node %T", n) + } var f *d2ir.Field + var ok bool if len(ida) > 0 { - f = m.Get(ida) - if f == nil { - t.Fatalf("expected field %#v in map %#v but not found", ida, m) + f, ok = m.Get(ida) + if !ok { + t.Fatalf("expected field %v in map %s", ida, m) } p = f.Primary - m = d2ir.NodeToMap(f) + if f_m, ok := f.Composite.(*d2ir.Map); ok { + m = f_m + } else { + m = &d2ir.Map{} + } } - if m.FieldCount() != nfields { - t.Fatalf("expected %d fields but got %d", nfields, m.FieldCount()) - } - if m.EdgeCount() != nedges { - t.Fatalf("expected %d edges but got %d", nedges, m.EdgeCount()) - } + assert.Equal(t, nfields, m.FieldCount()) + assert.Equal(t, nedges, m.EdgeCount()) if !p.Equal(makeScalar(primary)) { t.Fatalf("expected primary %#v but %#v", primary, p) } @@ -150,19 +159,27 @@ func assertField(t testing.TB, n d2ir.Node, nfields, nedges int, primary interfa func assertEdge(t testing.TB, n d2ir.Node, nfields int, primary interface{}, eid *d2ir.EdgeID) *d2ir.Edge { t.Helper() - m := d2ir.NodeToMap(n) - - e := m.GetEdge(eid) - if e == nil { - t.Fatalf("expected edge %#v in map %#v but not found", eid, m) + var m *d2ir.Map + switch n := n.(type) { + case *d2ir.Field: + mm, ok := n.Composite.(*d2ir.Map) + if ok { + m = mm + } else { + t.Fatalf("unexpected d2ir.Field.Composite %T", n.Composite) + } + case *d2ir.Map: + m = n + default: + t.Fatalf("unexpected d2ir.Node %T", n) } - if e.Map.FieldCount() != nfields { - t.Fatalf("expected %d fields but got %d", nfields, e.Map.FieldCount()) - } - if e.Map.EdgeCount() != 0 { - t.Fatalf("expected %d edges but got %d", 0, e.Map.EdgeCount()) + e, ok := m.GetEdge(eid) + if !ok { + t.Fatalf("expected edge %v in map %s but not found", eid, m) } + + assert.Equal(t, nfields, e.Map.FieldCount()) if !e.Primary.Equal(makeScalar(primary)) { t.Fatalf("expected primary %#v but %#v", primary, e.Primary) } diff --git a/d2ir/d2ir.go b/d2ir/d2ir.go index 20ecdd08e..8facce8b4 100644 --- a/d2ir/d2ir.go +++ b/d2ir/d2ir.go @@ -1,6 +1,8 @@ package d2ir import ( + "encoding/json" + "fmt" "strings" "oss.terrastruct.com/d2/d2ast" @@ -51,6 +53,7 @@ func (n *Edge) node() {} 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 } @@ -77,6 +80,17 @@ func (s *Scalar) Copy(newp Parent) Node { } func (s *Scalar) Equal(s2 *Scalar) bool { + if s == nil { + if s2 == nil { + return true + } + _, ok := s2.Value.(*d2ast.Null) + return ok + } + if s2 == nil { + _, ok := s.Value.(*d2ast.Null) + return ok + } return s.Value.ScalarString() == s2.Value.ScalarString() && s.Value.Type() == s2.Value.Type() } @@ -113,10 +127,10 @@ type Field struct { Name string `json:"name"` - Primary *Scalar `json:"primary"` - Composite Composite `json:"composite"` + Primary *Scalar `json:"primary,omitempty"` + Composite Composite `json:"composite,omitempty"` - Refs []KeyReference `json:"refs"` + Refs []KeyReference `json:"refs,omitempty"` } func (f *Field) Copy(newp Parent) Node { @@ -203,10 +217,10 @@ type Edge struct { ID *EdgeID `json:"edge_id"` - Primary *Scalar `json:"primary"` - Map *Map `json:"map"` + Primary *Scalar `json:"primary,omitempty"` + Map *Map `json:"map,omitempty"` - Refs []EdgeReference `json:"refs"` + Refs []EdgeReference `json:"refs,omitempty"` } func (e *Edge) Copy(newp Parent) Node { @@ -384,3 +398,11 @@ func (m *Map) GetEdge(eid *EdgeID) (*Edge, bool) { } return nil, false } + +func (m *Map) String() string { + b, err := json.Marshal(m) + if err != nil { + panic(fmt.Sprintf("d2ir: failed to marshal d2ir.Map: %v", err)) + } + return string(b) +} diff --git a/d2ir/d2ir_test.go b/d2ir/d2ir_test.go index 9fab4f612..52cae5aad 100644 --- a/d2ir/d2ir_test.go +++ b/d2ir/d2ir_test.go @@ -3,9 +3,10 @@ package d2ir_test import ( "testing" + "oss.terrastruct.com/util-go/assert" + "oss.terrastruct.com/d2/d2ast" "oss.terrastruct.com/d2/d2ir" - "oss.terrastruct.com/d2/internal/assert" ) func TestCopy(t *testing.T) { @@ -13,14 +14,11 @@ func TestCopy(t *testing.T) { const scalStr = `Those who claim the dead never return to life haven't ever been around.` s := &d2ir.Scalar{ - parent: nil, - Value: d2ast.FlatUnquotedString(scalStr), + Value: d2ast.FlatUnquotedString(scalStr), } a := &d2ir.Array{ - Parent: nil, Values: []d2ir.Value{ &d2ir.Scalar{ - parent: nil, Value: &d2ast.Boolean{ Value: true, }, @@ -28,7 +26,6 @@ func TestCopy(t *testing.T) { }, } m2 := &d2ir.Map{ - Parent: nil, Fields: []*d2ir.Field{ {Primary: s}, }, @@ -36,20 +33,17 @@ func TestCopy(t *testing.T) { const keyStr = `Absence makes the heart grow frantic.` f := &d2ir.Field{ - Parent: nil, - Name: keyStr, + Name: keyStr, Primary: s, Composite: a, } e := &d2ir.Edge{ - Parent: nil, Primary: s, Map: m2, } m := &d2ir.Map{ - Parent: nil, Fields: []*d2ir.Field{f}, Edges: []*d2ir.Edge{e}, @@ -58,20 +52,20 @@ func TestCopy(t *testing.T) { m = m.Copy(nil).(*d2ir.Map) f.Name = `Many a wife thinks her husband is the world's greatest lover.` - assert.Equal(t, m, m.Fields[0].Parent) + assert.Equal(t, m, m.Fields[0].Parent()) assert.Equal(t, keyStr, m.Fields[0].Name) - assert.Equal(t, m.Fields[0], m.Fields[0].Primary.parent) - assert.Equal(t, m.Fields[0], m.Fields[0].Composite.(*d2ir.Array).Parent) + assert.Equal(t, m.Fields[0], m.Fields[0].Primary.Parent()) + assert.Equal(t, m.Fields[0], m.Fields[0].Composite.(*d2ir.Array).Parent()) assert.Equal(t, m.Fields[0].Composite, - m.Fields[0].Composite.(*d2ir.Array).Values[0].(*d2ir.Scalar).parent, + m.Fields[0].Composite.(*d2ir.Array).Values[0].(*d2ir.Scalar).Parent(), ) - assert.Equal(t, m, m.Edges[0].Parent) - assert.Equal(t, m.Edges[0], m.Edges[0].Primary.parent) - assert.Equal(t, m.Edges[0], m.Edges[0].Map.Parent) + assert.Equal(t, m, m.Edges[0].Parent()) + assert.Equal(t, m.Edges[0], m.Edges[0].Primary.Parent()) + assert.Equal(t, m.Edges[0], m.Edges[0].Map.Parent()) - assert.Equal(t, m.Edges[0].Map, m.Edges[0].Map.Fields[0].Parent) - assert.Equal(t, m.Edges[0].Map.Fields[0], m.Edges[0].Map.Fields[0].Primary.parent) + assert.Equal(t, m.Edges[0].Map, m.Edges[0].Map.Fields[0].Parent()) + assert.Equal(t, m.Edges[0].Map.Fields[0], m.Edges[0].Map.Fields[0].Primary.Parent()) } diff --git a/d2parser/parse.go b/d2parser/parse.go index d8351dcbc..0e7e5eefc 100644 --- a/d2parser/parse.go +++ b/d2parser/parse.go @@ -45,7 +45,7 @@ func Parse(path string, r io.RuneReader, opts *ParseOptions) (*d2ast.Map, error) } m := p.parseMap(true) - if !p.err.empty() { + if !p.err.Empty() { return m, p.err } return m, nil @@ -57,7 +57,7 @@ func ParseKey(key string) (*d2ast.KeyPath, error) { } k := p.parseKey() - if !p.err.empty() { + if !p.err.Empty() { return nil, fmt.Errorf("failed to parse key %q: %w", key, p.err) } if k == nil { @@ -72,7 +72,7 @@ func ParseMapKey(mapKey string) (*d2ast.Key, error) { } mk := p.parseMapKey() - if !p.err.empty() { + if !p.err.Empty() { return nil, fmt.Errorf("failed to parse map key %q: %w", mapKey, p.err) } if mk == nil { @@ -87,7 +87,7 @@ func ParseValue(value string) (d2ast.Value, error) { } v := p.parseValue() - if !p.err.empty() { + if !p.err.Empty() { return nil, fmt.Errorf("failed to parse value %q: %w", value, p.err) } if v.Unbox() == nil { @@ -130,7 +130,7 @@ type ParseError struct { Errors []d2ast.Error `json:"errs"` } -func (pe ParseError) empty() bool { +func (pe ParseError) Empty() bool { return pe.IOError == nil && len(pe.Errors) == 0 } diff --git a/testdata/d2ir/TestApply/simple/nested.exp.json b/testdata/d2ir/TestApply/simple/nested.exp.json new file mode 100644 index 000000000..fbfc21849 --- /dev/null +++ b/testdata/d2ir/TestApply/simple/nested.exp.json @@ -0,0 +1,4 @@ +{ + "fields": null, + "edges": null +} diff --git a/testdata/d2ir/TestApply/simple/one.exp.json b/testdata/d2ir/TestApply/simple/one.exp.json new file mode 100644 index 000000000..2dcf9ab44 --- /dev/null +++ b/testdata/d2ir/TestApply/simple/one.exp.json @@ -0,0 +1,8 @@ +{ + "fields": [ + { + "name": "x" + } + ], + "edges": null +} diff --git a/testdata/d2ir/TestApply/simple/underscore_parent.exp.json b/testdata/d2ir/TestApply/simple/underscore_parent.exp.json new file mode 100644 index 000000000..fbfc21849 --- /dev/null +++ b/testdata/d2ir/TestApply/simple/underscore_parent.exp.json @@ -0,0 +1,4 @@ +{ + "fields": null, + "edges": null +}