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

d2ir: Globs #1479

Merged
merged 15 commits into from
Jul 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Layout capability also takes a subtle but important step forward: you can now cu
- Scale renders and disable fit to screen with `--scale` flag [#1413](https://github.com/terrastruct/d2/pull/1413)
- `null` keyword can be used to un-declare. See [docs](https://d2lang.com/tour/overrides#null) [#1446](https://github.com/terrastruct/d2/pull/1446)
- Develop multi-board diagrams in watch mode (links to layers/scenarios/steps work in `--watch`) [#1503](https://github.com/terrastruct/d2/pull/1503)
- Glob patterns have been implemented. See [docs](https://d2lang.com/tour/globs). [#1479](https://github.com/terrastruct/d2/pull/1479)

#### Improvements 🧹

Expand Down
31 changes: 31 additions & 0 deletions d2ast/d2ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,8 @@ type Number struct {
type UnquotedString struct {
Range Range `json:"range"`
Value []InterpolationBox `json:"value"`
// Pattern holds the parsed glob pattern if in a key and the unquoted string represents a valid pattern.
Pattern []string `json:"pattern,omitempty"`
}

func (s *UnquotedString) Coalesce() {
Expand Down Expand Up @@ -738,6 +740,31 @@ func (kp *KeyPath) IDA() (ida []string) {
return ida
}

func (kp *KeyPath) Copy() *KeyPath {
kp2 := *kp
kp2.Path = nil
kp2.Path = append(kp2.Path, kp.Path...)
return &kp2
}

func (kp *KeyPath) HasDoubleGlob() bool {
for _, el := range kp.Path {
if el.UnquotedString != nil && el.ScalarString() == "**" {
return true
}
}
return false
}

func (kp *KeyPath) HasGlob() bool {
for _, el := range kp.Path {
if el.UnquotedString != nil && len(el.UnquotedString.Pattern) > 0 {
return true
}
}
return false
}

type Edge struct {
Range Range `json:"range"`

Expand Down Expand Up @@ -1056,6 +1083,10 @@ func (sb *StringBox) Unbox() String {
}
}

func (sb *StringBox) ScalarString() string {
return sb.Unbox().ScalarString()
}

// InterpolationBox is used to select between strings and substitutions in unquoted and
// double quoted strings. There is no corresponding interface to avoid unnecessary
// abstraction.
Expand Down
127 changes: 68 additions & 59 deletions d2ir/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,19 +396,26 @@ func (c *compiler) compileKey(refctx *RefContext) {
}

func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext) {
if refctx.Key != nil && len(refctx.Key.Edges) == 0 && refctx.Key.Value.Null != nil {
fa, err := dst.EnsureField(kp, refctx, true)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return
}

for _, f := range fa {
c._compileField(f, refctx)
}
}

func (c *compiler) _compileField(f *Field, refctx *RefContext) {
if len(refctx.Key.Edges) == 0 && refctx.Key.Value.Null != nil {
// For vars, if we delete the field, it may just resolve to an outer scope var of the same name
// Instead we keep it around, so that resolveSubstitutions can find it
if !IsVar(dst) {
dst.DeleteField(kp.IDA()...)
if !IsVar(ParentMap(f)) {
ParentMap(f).DeleteField(f.Name)
return
}
}
f, err := dst.EnsureField(kp, refctx)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return
}

if refctx.Key.Primary.Unbox() != nil {
f.Primary_ = &Scalar{
Expand Down Expand Up @@ -602,12 +609,17 @@ func (c *compiler) compileLink(refctx *RefContext) {
}

func (c *compiler) compileEdges(refctx *RefContext) {
if refctx.Key.Key != nil {
f, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return
}
if refctx.Key.Key == nil {
c._compileEdges(refctx)
return
}

fa, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx, true)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return
}
for _, f := range fa {
if _, ok := f.Composite.(*Array); ok {
c.errorf(refctx.Key.Key, "cannot index into array")
return
Expand All @@ -617,9 +629,13 @@ func (c *compiler) compileEdges(refctx *RefContext) {
parent: f,
}
}
refctx.ScopeMap = f.Map()
refctx2 := *refctx
refctx2.ScopeMap = f.Map()
c._compileEdges(&refctx2)
}
}

func (c *compiler) _compileEdges(refctx *RefContext) {
eida := NewEdgeIDs(refctx.Key)
for i, eid := range eida {
if refctx.Key != nil && refctx.Key.Value.Null != nil {
Expand All @@ -630,66 +646,59 @@ func (c *compiler) compileEdges(refctx *RefContext) {
refctx = refctx.Copy()
refctx.Edge = refctx.Key.Edges[i]

var e *Edge
if eid.Index != nil {
ea := refctx.ScopeMap.GetEdges(eid)
var ea []*Edge
if eid.Index != nil || eid.Glob {
ea = refctx.ScopeMap.GetEdges(eid, refctx)
if len(ea) == 0 {
c.errorf(refctx.Edge, "indexed edge does not exist")
continue
}
e = ea[0]
e.References = append(e.References, &EdgeReference{
Context: refctx,
})
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Src, refctx)
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Dst, refctx)
} else {
_, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, refctx)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
continue
}
_, err = refctx.ScopeMap.EnsureField(refctx.Edge.Dst, refctx)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
continue
for _, e := range ea {
e.References = append(e.References, &EdgeReference{
Context: refctx,
})
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Src, refctx)
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Dst, refctx)
}

e, err = refctx.ScopeMap.CreateEdge(eid, refctx)
} else {
var err error
ea, err = refctx.ScopeMap.CreateEdge(eid, refctx)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
continue
}
}

if refctx.Key.EdgeKey != nil {
if e.Map_ == nil {
e.Map_ = &Map{
parent: e,
}
}
c.compileField(e.Map_, refctx.Key.EdgeKey, refctx)
} else {
if refctx.Key.Primary.Unbox() != nil {
e.Primary_ = &Scalar{
parent: e,
Value: refctx.Key.Primary.Unbox(),
}
}
if refctx.Key.Value.Array != nil {
c.errorf(refctx.Key.Value.Unbox(), "edges cannot be assigned arrays")
continue
} else if refctx.Key.Value.Map != nil {
for _, e := range ea {
if refctx.Key.EdgeKey != nil {
if e.Map_ == nil {
e.Map_ = &Map{
parent: e,
}
}
c.compileMap(e.Map_, refctx.Key.Value.Map, refctx.ScopeAST)
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
e.Primary_ = &Scalar{
parent: e,
Value: refctx.Key.Value.ScalarBox().Unbox(),
c.compileField(e.Map_, refctx.Key.EdgeKey, refctx)
} else {
if refctx.Key.Primary.Unbox() != nil {
e.Primary_ = &Scalar{
parent: e,
Value: refctx.Key.Primary.Unbox(),
}
}
if refctx.Key.Value.Array != nil {
c.errorf(refctx.Key.Value.Unbox(), "edges cannot be assigned arrays")
continue
} else if refctx.Key.Value.Map != nil {
if e.Map_ == nil {
e.Map_ = &Map{
parent: e,
}
}
c.compileMap(e.Map_, refctx.Key.Value.Map, refctx.ScopeAST)
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
e.Primary_ = &Scalar{
parent: e,
Value: refctx.Key.Value.ScalarBox().Unbox(),
}
}
}
}
Expand Down
23 changes: 16 additions & 7 deletions d2ir/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestCompile(t *testing.T) {
t.Run("scenarios", testCompileScenarios)
t.Run("steps", testCompileSteps)
t.Run("imports", testCompileImports)
t.Run("patterns", testCompilePatterns)
}

type testCase struct {
Expand Down Expand Up @@ -84,23 +85,31 @@ func assertQuery(t testing.TB, n d2ir.Node, nfields, nedges int, primary interfa
m := n.Map()
p := n.Primary()

var na []d2ir.Node
if idStr != "" {
var err error
n, err = m.Query(idStr)
na, err = m.QueryAll(idStr)
assert.Success(t, err)
assert.NotEqual(t, n, nil)
} else {
na = append(na, n)
}

p = n.Primary()
for _, n := range na {
m = n.Map()
p = n.Primary()
assert.Equal(t, nfields, m.FieldCountRecursive())
assert.Equal(t, nedges, m.EdgeCountRecursive())
if !makeScalar(p).Equal(makeScalar(primary)) {
t.Fatalf("expected primary %#v but got %s", primary, p)
}
}

assert.Equal(t, nfields, m.FieldCountRecursive())
assert.Equal(t, nedges, m.EdgeCountRecursive())
if !makeScalar(p).Equal(makeScalar(primary)) {
t.Fatalf("expected primary %#v but got %s", primary, p)
if len(na) == 0 {
return nil
}

return n
return na[0]
}

func makeScalar(v interface{}) *d2ir.Scalar {
Expand Down
Loading