diff --git a/d2ir/apply.go b/d2ir/apply.go new file mode 100644 index 000000000..893bda82a --- /dev/null +++ b/d2ir/apply.go @@ -0,0 +1,57 @@ +package d2ir + +import ( + "fmt" + + "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/d2parser" +) + +type compiler struct { + err d2parser.ParseError +} + +func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) { + f = "%v: " + f + v = append([]interface{}{n.GetRange()}, v...) + c.err.Errors = append(c.err.Errors, d2ast.Error{ + Range: n.GetRange(), + Message: fmt.Sprintf(f, v...), + }) +} + +func Apply(dst *Map, ast *d2ast.Map) error { + var c compiler + c.apply(dst, ast) + return c.err +} + +func (c *compiler) apply(dst *Map, ast *d2ast.Map) { + for _, n := range ast.Nodes { + if n.MapKey == nil { + continue + } + + c.applyKey(dst, n.MapKey) + } +} + +func (c *compiler) applyKey(dst *Map, k *d2ast.Key) { + if k.Key != nil && len(k.Key.Path) > 0 { + f, ok := dst.Ensure(d2graph.Key(k.Key)) + if !ok { + c.errorf(k.Key, "cannot index into array") + return + } + + if len(k.Edges) == 0 { + if k.Primary.Unbox() != nil { + f.Primary = &Scalar{ + parent: f, + Value: k.Primary.Unbox(), + } + } + } + } +} diff --git a/d2ir/apply_test.go b/d2ir/apply_test.go new file mode 100644 index 000000000..5787e882f --- /dev/null +++ b/d2ir/apply_test.go @@ -0,0 +1,199 @@ +package d2ir_test + +import ( + "fmt" + "math/big" + "path/filepath" + "strings" + "testing" + + "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) +} + +func TestApply(t *testing.T) { + t.Parallel() + + t.Run("simple", testApplySimple) +} + +func testApplySimple(t *testing.T) { + tcs := []testCase{ + { + name: "one", + text: `x`, + + exp: func(t testing.TB, m *d2ir.Map, err error) { + assert.Success(t, err) + assertField(t, m, 1, 0, nil) + + assertField(t, m, 0, 0, nil, "x") + }, + }, + { + name: "nested", + text: `x.y -> z.p`, + + exp: func(t testing.TB, m *d2ir.Map, err error) { + assert.Success(t, err) + assertField(t, m, 4, 1, nil) + + assertField(t, m, 1, 0, nil, "x") + assertField(t, m, 0, 0, nil, "x", "y") + + 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, + }) + }, + }, + { + name: "underscore_parent", + text: `x._ -> z`, + + exp: func(t testing.TB, m *d2ir.Map, err error) { + assert.Success(t, err) + assertField(t, m, 2, 1, nil) + + 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, + }) + }, + }, + } + + runa(t, tcs) +} + +func runa(t *testing.T, tcs []testCase) { + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + run(t, tc) + }) + } +} + +func run(t testing.TB, tc testCase) { + 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 + } + + 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 + } +} + +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 f *d2ir.Field + if len(ida) > 0 { + f = m.Get(ida) + if f == nil { + t.Fatalf("expected field %#v in map %#v but not found", ida, m) + } + p = f.Primary + m = d2ir.NodeToMap(f) + } + + 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()) + } + if !p.Equal(makeScalar(primary)) { + t.Fatalf("expected primary %#v but %#v", primary, p) + } + + return f +} + +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) + } + + 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()) + } + if !e.Primary.Equal(makeScalar(primary)) { + t.Fatalf("expected primary %#v but %#v", primary, e.Primary) + } + + return e +} + +func makeScalar(v interface{}) *d2ir.Scalar { + s := &d2ir.Scalar{} + switch v := v.(type) { + case bool: + s.Value = &d2ast.Boolean{ + Value: v, + } + case float64: + bv := &big.Rat{} + bv.SetFloat64(v) + s.Value = &d2ast.Number{ + Value: bv, + } + case int: + s.Value = &d2ast.Number{ + Value: big.NewRat(int64(v), 1), + } + case string: + s.Value = d2ast.FlatDoubleQuotedString(v) + default: + if v != nil { + panic(fmt.Sprintf("d2ir: unexpected type to makeScalar: %#v", v)) + } + s.Value = &d2ast.Null{} + } + return s +} diff --git a/d2ir/d2ir.go b/d2ir/d2ir.go new file mode 100644 index 000000000..20ecdd08e --- /dev/null +++ b/d2ir/d2ir.go @@ -0,0 +1,386 @@ +package d2ir + +import ( + "strings" + + "oss.terrastruct.com/d2/d2ast" +) + +type Node interface { + node() + Copy(newp Parent) Node +} + +var _ Node = &Scalar{} +var _ Node = &Field{} +var _ Node = &Edge{} +var _ Node = &Array{} +var _ Node = &Map{} + +type Parent interface { + Node + Parent() Parent +} + +var _ Parent = &Field{} +var _ Parent = &Edge{} +var _ Parent = &Array{} +var _ Parent = &Map{} + +type Value interface { + Node + value() +} + +var _ Value = &Scalar{} +var _ Value = &Array{} +var _ Value = &Map{} + +type Composite interface { + Node + Value + composite() +} + +var _ Composite = &Array{} +var _ Composite = &Map{} + +func (n *Scalar) node() {} +func (n *Field) node() {} +func (n *Edge) node() {} +func (n *Array) node() {} +func (n *Map) node() {} + +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() {} +func (n *Map) value() {} + +func (n *Array) composite() {} +func (n *Map) composite() {} + +type Scalar struct { + parent Parent + Value d2ast.Scalar `json:"value"` +} + +func (s *Scalar) Copy(newp Parent) Node { + tmp := *s + s = &tmp + + s.parent = newp + return s +} + +func (s *Scalar) Equal(s2 *Scalar) bool { + return s.Value.ScalarString() == s2.Value.ScalarString() && s.Value.Type() == s2.Value.Type() +} + +type Map struct { + parent Parent + Fields []*Field `json:"fields"` + Edges []*Edge `json:"edges"` +} + +func (m *Map) Copy(newp Parent) Node { + tmp := *m + m = &tmp + + m.parent = newp + m.Fields = append([]*Field(nil), m.Fields...) + for i := range m.Fields { + m.Fields[i] = m.Fields[i].Copy(m).(*Field) + } + m.Edges = append([]*Edge(nil), m.Edges...) + for i := range m.Edges { + m.Edges[i] = m.Edges[i].Copy(m).(*Edge) + } + return m +} + +// Root reports whether the Map is the root of the D2 tree. +// The root map has no parent. +func (m *Map) Root() bool { + return m.parent == nil +} + +type Field struct { + parent *Map + + Name string `json:"name"` + + Primary *Scalar `json:"primary"` + Composite Composite `json:"composite"` + + Refs []KeyReference `json:"refs"` +} + +func (f *Field) Copy(newp Parent) Node { + tmp := *f + f = &tmp + + f.parent = newp.(*Map) + f.Refs = append([]KeyReference(nil), f.Refs...) + if f.Primary != nil { + f.Primary = f.Primary.Copy(f).(*Scalar) + } + if f.Composite != nil { + f.Composite = f.Composite.Copy(f).(Composite) + } + return f +} + +type EdgeID struct { + SrcPath []string `json:"src_path"` + SrcArrow bool `json:"src_arrow"` + + DstPath []string `json:"dst_path"` + DstArrow bool `json:"dst_arrow"` + + Index int `json:"index"` +} + +func (eid *EdgeID) Copy() *EdgeID { + tmp := *eid + eid = &tmp + + eid.SrcPath = append([]string(nil), eid.SrcPath...) + eid.DstPath = append([]string(nil), eid.DstPath...) + return eid +} + +func (eid *EdgeID) Equal(eid2 *EdgeID) bool { + if eid.Index != eid2.Index { + return false + } + + if len(eid.SrcPath) != len(eid2.SrcPath) { + return false + } + if eid.SrcArrow != eid2.SrcArrow { + return false + } + for i, s := range eid.SrcPath { + if !strings.EqualFold(s, eid2.SrcPath[i]) { + return false + } + } + + if len(eid.DstPath) != len(eid2.DstPath) { + return false + } + if eid.DstArrow != eid2.DstArrow { + return false + } + for i, s := range eid.DstPath { + if !strings.EqualFold(s, eid2.DstPath[i]) { + return false + } + } + + return true +} + +func (eid *EdgeID) trimCommon() (common []string, _ *EdgeID) { + eid = eid.Copy() + for len(eid.SrcPath) > 1 && len(eid.DstPath) > 1 { + if !strings.EqualFold(eid.SrcPath[0], eid.DstPath[0]) { + return common, eid + } + common = append(common, eid.SrcPath[0]) + eid.SrcPath = eid.SrcPath[1:] + eid.DstPath = eid.DstPath[1:] + } + return common, eid +} + +type Edge struct { + parent *Map + + ID *EdgeID `json:"edge_id"` + + Primary *Scalar `json:"primary"` + Map *Map `json:"map"` + + Refs []EdgeReference `json:"refs"` +} + +func (e *Edge) Copy(newp Parent) Node { + tmp := *e + e = &tmp + + e.parent = newp.(*Map) + e.Refs = append([]EdgeReference(nil), e.Refs...) + if e.Primary != nil { + e.Primary = e.Primary.Copy(e).(*Scalar) + } + if e.Map != nil { + e.Map = e.Map.Copy(e).(*Map) + } + return e +} + +type Array struct { + parent Parent + Values []Value `json:"values"` +} + +func (a *Array) Copy(newp Parent) Node { + tmp := *a + a = &tmp + + a.parent = newp + a.Values = append([]Value(nil), a.Values...) + for i := range a.Values { + a.Values[i] = a.Values[i].Copy(a).(Value) + } + return a +} + +type KeyReference struct { + String *d2ast.StringBox `json:"string"` + KeyPath *d2ast.KeyPath `json:"key_path"` + + RefCtx *RefContext `json:"ref_ctx"` +} + +type EdgeReference struct { + RefCtx *RefContext `json:"ref_ctx"` +} + +type RefContext struct { + Key *d2ast.Key `json:"-"` + Edge *d2ast.Edge `json:"-"` + Scope *d2ast.Map `json:"-"` +} + +func (m *Map) FieldCount() int { + acc := len(m.Fields) + for _, f := range m.Fields { + if f_m, ok := f.Composite.(*Map); ok { + acc += f_m.FieldCount() + } + } + return acc +} + +func (m *Map) EdgeCount() int { + acc := len(m.Edges) + for _, e := range m.Edges { + if e.Map != nil { + acc += e.Map.EdgeCount() + } + } + return acc +} + +func (m *Map) Get(ida []string) (*Field, bool) { + if len(ida) == 0 { + return nil, false + } + + s := ida[0] + rest := ida[1:] + + for _, f := range m.Fields { + if !strings.EqualFold(f.Name, s) { + continue + } + if len(rest) == 0 { + return f, true + } + if f_m, ok := f.Composite.(*Map); ok { + return f_m.Get(rest) + } + } + return nil, false +} + +func (m *Map) Ensure(ida []string) (*Field, bool) { + if len(ida) == 0 { + return nil, false + } + + s := ida[0] + rest := ida[1:] + + for _, f := range m.Fields { + if !strings.EqualFold(f.Name, s) { + continue + } + if len(rest) == 0 { + return f, true + } + switch fc := f.Composite.(type) { + case *Map: + return fc.Ensure(rest) + case *Array: + return nil, false + } + f.Composite = &Map{ + parent: f, + } + return f.Composite.(*Map).Ensure(rest) + } + + f := &Field{ + parent: m, + Name: s, + } + m.Fields = append(m.Fields, f) + if len(rest) == 0 { + return f, true + } + f.Composite = &Map{ + parent: f, + } + return f.Composite.(*Map).Ensure(rest) +} + +func (m *Map) Delete(ida []string) bool { + if len(ida) == 0 { + return false + } + + s := ida[0] + rest := ida[1:] + + for i, f := range m.Fields { + if !strings.EqualFold(f.Name, s) { + continue + } + if len(rest) == 0 { + copy(m.Fields[i:], m.Fields[i+1:]) + return true + } + if f_m, ok := f.Composite.(*Map); ok { + return f_m.Delete(rest) + } + } + return false +} + +func (m *Map) GetEdge(eid *EdgeID) (*Edge, bool) { + common, eid := eid.trimCommon() + if len(common) > 0 { + f, ok := m.Get(common) + if !ok { + return nil, false + } + if f_m, ok := f.Composite.(*Map); ok { + return f_m.GetEdge(eid) + } + return nil, false + } + + for _, e := range m.Edges { + if e.ID.Equal(eid) { + return e, true + } + } + return nil, false +} diff --git a/d2ir/d2ir_test.go b/d2ir/d2ir_test.go new file mode 100644 index 000000000..9fab4f612 --- /dev/null +++ b/d2ir/d2ir_test.go @@ -0,0 +1,77 @@ +package d2ir_test + +import ( + "testing" + + "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2ir" + "oss.terrastruct.com/d2/internal/assert" +) + +func TestCopy(t *testing.T) { + t.Parallel() + + 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), + } + a := &d2ir.Array{ + Parent: nil, + Values: []d2ir.Value{ + &d2ir.Scalar{ + parent: nil, + Value: &d2ast.Boolean{ + Value: true, + }, + }, + }, + } + m2 := &d2ir.Map{ + Parent: nil, + Fields: []*d2ir.Field{ + {Primary: s}, + }, + } + + const keyStr = `Absence makes the heart grow frantic.` + f := &d2ir.Field{ + Parent: nil, + 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}, + } + + 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, 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].Composite, + 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.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) +}