Skip to content

Instantly share code, notes, and snippets.

@kajogo777
Created March 22, 2023 22:42
Show Gist options
  • Save kajogo777/9c80ac8eb0663567ab93138214880b5f to your computer and use it in GitHub Desktop.
Save kajogo777/9c80ac8eb0663567ab93138214880b5f to your computer and use it in GitHub Desktop.
PoC for applying AST transformations to CUE files, the transformations themselves are written in CUE
package main
patch: {
pattern: {
abc: string
a: int
}
result: {
abc: pattern.abc
id: "id-\(pattern.a*2)"
}
}
stack: components: {
anything: {
abc: "my string"
a: r.a
}
r: a: 10
yo: abc: "as"
yo: a: 5
}
package main
import (
"fmt"
"log"
"os"
"reflect"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/ast/astutil"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/format"
"cuelang.org/go/cue/load"
)
const (
patchPath = "patch"
patternPath = "pattern"
resultPath = "result"
componentsPath = "stack.components"
)
type Patch struct {
Path []string
Node ast.Node
}
func main() {
ctx := cuecontext.New()
bi := load.Instances(nil, nil)
vs, err := ctx.BuildInstances(bi)
if err != nil {
fmt.Errorf(err.Error())
os.Exit(1)
}
v := vs[0]
patch := v.LookupPath(cue.ParsePath(patchPath))
components := v.LookupPath(cue.ParsePath(componentsPath))
iter, err := components.Fields()
if err != nil {
fmt.Errorf(err.Error())
os.Exit(1)
}
// Get fields to patch
patches := []Patch{}
for iter.Next() {
value := iter.Value()
// Figure out which components to transform
if patch.LookupPath(cue.ParsePath(patternPath)).Subsume(value) != nil {
continue
}
patchValue := patch.FillPath(cue.ParsePath(patternPath), value)
patchValueNode := patchValue.Syntax(cue.All(), cue.Docs(true), cue.Attributes(true))
// Get all filled "pattern" fields
patternRefs := map[string]ast.Node{}
astutil.Apply(patchValueNode, func(c astutil.Cursor) bool {
path := GetPath(c)
if len(path) < 2 {
return true
}
if path[0] == patternPath && !IsField(c) {
pathString := strings.Join(GetPath(c), ".")
patternRefs[pathString] = c.Node()
return false
}
return true
}, nil)
// Substitute references to "pattern" fields in "result" fields
patchValueNode = astutil.Apply(patchValueNode, func(c astutil.Cursor) bool {
switch n := c.Node().(type) {
case *ast.SelectorExpr:
path := GetPath(c)
if len(path) == 0 {
return true
}
selector := fmt.Sprintf("%s.%s", n.X, n.Sel)
if path[0] == resultPath && strings.HasPrefix(selector, patternPath) {
patternSelectorNode, ok := patternRefs[selector]
// Check if reference found
if ok {
c.Replace(patternSelectorNode)
return false
}
}
}
return true
}, nil)
// Extract the "result" field
var node ast.Node
astutil.Apply(patchValueNode, func(c astutil.Cursor) bool {
path := GetPath(c)
if IsEqualStringArray(path, []string{resultPath}) && !IsField(c) {
node = c.Node()
return false
}
return true
}, nil)
if node == nil {
log.Fatal("result node not found")
}
// astutil.CopyComments(node, value.Source())
path := []string{}
for _, sel := range value.Path().Selectors() {
path = append(path, sel.String())
}
patches = append(patches, Patch{
Path: path,
Node: node,
})
}
// Apply patches
patched := astutil.Apply(v.Syntax(), func(c astutil.Cursor) bool {
path := GetPath(c)
for _, patch := range patches {
if IsEqualStringArray(path, patch.Path) && !IsField(c) && !IsLabel(c) {
c.Replace(patch.Node)
return false
}
}
return true
}, nil)
// Validate patched values
value := ctx.BuildFile(patched.(*ast.File))
if value.Err() != nil {
log.Fatal(value.Err())
}
// value = value.Eval()
// Print patched file
fileBytes, err := format.Node(value.Syntax(cue.All(), cue.Docs(true), cue.Attributes(true)), format.Simplify())
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", fileBytes)
}
func IsField(c astutil.Cursor) bool {
switch c.Node().(type) {
case *ast.Field:
return true
}
return false
}
func IsLabel(c astutil.Cursor) bool {
switch c.Node().(type) {
case ast.Label:
return true
}
return false
}
func IsEqualStringArray(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func GetPath(c astutil.Cursor) []string {
if c == nil {
return []string{}
}
parent := GetPath(c.Parent())
switch n := c.Node().(type) {
case *ast.Field:
return append(parent, GetName(n.Label))
}
return parent
}
// Used for debugging
func GetTypePath(c astutil.Cursor) string {
if c == nil {
return ""
}
parent := GetTypePath(c.Parent())
typo := reflect.TypeOf(c.Node()).String()
switch n := c.Node().(type) {
case *ast.Field:
return fmt.Sprintf("%s %s:%s ", parent, typo, GetName(n.Label))
case *ast.Ident:
return fmt.Sprintf("%s %s:%s ", parent, typo, n.Name)
default:
return fmt.Sprintf("%s %s ", parent, typo)
}
}
func GetName(n ast.Node) string {
switch n := n.(type) {
case *ast.Ident:
return n.Name
case *ast.Field:
return GetName(n.Label)
}
return ""
}
@kajogo777
Copy link
Author

Expected output

package main

patch: {
        pattern: {
                abc: string
                a:   int
        }
        result: {
                abc: pattern.abc
                id:  "id-\(pattern.a*2)"
        }
}
stack: components: {
        anything: {
                abc: "my string"
                id:  "id-\((r.a & int)*2)"
        }
        r: a: 10
        yo: {
                abc: "as"
                id:  "id-\(5*2)"
        }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment