mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			358 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			358 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Go
		
	
	
	
| package codegen
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"path/filepath"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 
 | |
| 	"cuelang.org/go/cue"
 | |
| 	"cuelang.org/go/cue/errors"
 | |
| 	"github.com/grafana/codejen"
 | |
| 	"github.com/grafana/cuetsy"
 | |
| 	"github.com/grafana/cuetsy/ts"
 | |
| 	"github.com/grafana/cuetsy/ts/ast"
 | |
| 	"github.com/grafana/grafana/pkg/cuectx"
 | |
| 	"github.com/grafana/kindsys"
 | |
| 	"github.com/grafana/thema"
 | |
| 	"github.com/grafana/thema/encoding/typescript"
 | |
| )
 | |
| 
 | |
| // TSVeneerIndexJenny generates an index.gen.ts file with references to all
 | |
| // generated TS types. Elements with the attribute @grafana(TSVeneer="type") are
 | |
| // exported from a handwritten file, rather than the raw generated types.
 | |
| //
 | |
| // The provided dir is the path, relative to the grafana root, to the directory
 | |
| // that should contain the generated index.
 | |
| //
 | |
| // Implicitly depends on output patterns in TSTypesJenny.
 | |
| // TODO this is wasteful; share-nothing generator model entails re-running the cuetsy gen that TSTypesJenny already did
 | |
| func TSVeneerIndexJenny(dir string) ManyToOne {
 | |
| 	return &genTSVeneerIndex{
 | |
| 		dir: dir,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type genTSVeneerIndex struct {
 | |
| 	dir string
 | |
| }
 | |
| 
 | |
| func (gen *genTSVeneerIndex) JennyName() string {
 | |
| 	return "TSVeneerIndexJenny"
 | |
| }
 | |
| 
 | |
| func (gen *genTSVeneerIndex) Generate(kinds ...kindsys.Kind) (*codejen.File, error) {
 | |
| 	tsf := new(ast.File)
 | |
| 	for _, def := range kinds {
 | |
| 		sch := def.Lineage().Latest()
 | |
| 		f, err := typescript.GenerateTypes(sch, &typescript.TypeConfig{
 | |
| 			CuetsyConfig: &cuetsy.Config{
 | |
| 				ImportMapper: cuectx.MapCUEImportToTS,
 | |
| 			},
 | |
| 			RootName: def.Props().Common().Name,
 | |
| 			Group:    def.Props().Common().LineageIsGroup,
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("%s: %w", def.Props().Common().Name, err)
 | |
| 		}
 | |
| 		// The obvious approach would be calling renameSpecNode() here, same as in the ts resource jenny,
 | |
| 		// to rename the "spec" field to the name of the kind. But that was causing extra
 | |
| 		// default elements to generate that didn't actually exist. Instead,
 | |
| 		// findDeclNode() is aware of "spec" and does the change on the fly. Preserving this
 | |
| 		// as a reminder in case we want to switch back, though.
 | |
| 		// renameSpecNode(def.Props().Common().Name, f)
 | |
| 
 | |
| 		elems, err := gen.extractTSIndexVeneerElements(def, f)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("%s: %w", def.Props().Common().Name, err)
 | |
| 		}
 | |
| 		tsf.Nodes = append(tsf.Nodes, elems...)
 | |
| 	}
 | |
| 
 | |
| 	return codejen.NewFile(filepath.Join(gen.dir, "index.gen.ts"), []byte(tsf.String()), gen), nil
 | |
| }
 | |
| 
 | |
| func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(def kindsys.Kind, tf *ast.File) ([]ast.Decl, error) {
 | |
| 	lin := def.Lineage()
 | |
| 	comm := def.Props().Common()
 | |
| 
 | |
| 	// Check the root, then walk the tree
 | |
| 	rootv := lin.Latest().Underlying().LookupPath(schPath)
 | |
| 
 | |
| 	var raw, custom, rawD, customD ast.Idents
 | |
| 
 | |
| 	var terr errors.Error
 | |
| 	visit := func(p cue.Path, wv cue.Value) bool {
 | |
| 		var name string
 | |
| 		sels := p.Selectors()
 | |
| 		switch len(sels) {
 | |
| 		case 0:
 | |
| 			return true
 | |
| 
 | |
| 		case 1:
 | |
| 			// Only deal with subpaths that are definitions, for now
 | |
| 			// TODO incorporate smarts about grouped lineages here
 | |
| 			if name == "" {
 | |
| 				if !(sels[0].IsDefinition() || sels[0].String() == "spec") {
 | |
| 					return false
 | |
| 				}
 | |
| 				// It might seem to make sense that we'd strip out the leading # here for
 | |
| 				// definitions. However, cuetsy's tsast actually has the # still present in its
 | |
| 				// Ident types, stripping it out on the fly when stringifying.
 | |
| 				name = sels[0].String()
 | |
| 			}
 | |
| 
 | |
| 			// Search the generated TS AST for the type and default def nodes
 | |
| 			pair := findDeclNode(name, comm.Name, tf)
 | |
| 			if pair.T == nil {
 | |
| 				// No generated type for this item, skip it
 | |
| 				return false
 | |
| 			}
 | |
| 
 | |
| 			cust, perr := getCustomVeneerAttr(wv)
 | |
| 			if perr != nil {
 | |
| 				terr = errors.Append(terr, errors.Promote(perr, fmt.Sprintf("%s: ", p.String())))
 | |
| 			}
 | |
| 			var has bool
 | |
| 			for _, tgt := range cust {
 | |
| 				has = has || tgt.target == "type"
 | |
| 			}
 | |
| 			if has {
 | |
| 				// enums can't use 'export type'
 | |
| 				if pair.isEnum {
 | |
| 					customD = append(customD, *pair.T)
 | |
| 				} else {
 | |
| 					custom = append(custom, *pair.T)
 | |
| 				}
 | |
| 
 | |
| 				if pair.D != nil {
 | |
| 					customD = append(customD, *pair.D)
 | |
| 				}
 | |
| 			} else {
 | |
| 				// enums can't use 'export type'
 | |
| 				if pair.isEnum {
 | |
| 					rawD = append(rawD, *pair.T)
 | |
| 				} else {
 | |
| 					raw = append(raw, *pair.T)
 | |
| 				}
 | |
| 
 | |
| 				if pair.D != nil {
 | |
| 					rawD = append(rawD, *pair.D)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return true
 | |
| 	}
 | |
| 	walk(rootv, visit, nil)
 | |
| 
 | |
| 	if len(errors.Errors(terr)) != 0 {
 | |
| 		return nil, terr
 | |
| 	}
 | |
| 
 | |
| 	vpath := fmt.Sprintf("v%v", thema.Lineage.Latest(lin).Version())
 | |
| 	if def.Props().Common().Maturity.Less(kindsys.MaturityStable) {
 | |
| 		vpath = "x"
 | |
| 	}
 | |
| 
 | |
| 	ret := make([]ast.Decl, 0)
 | |
| 	if len(raw) > 0 {
 | |
| 		ret = append(ret, ast.ExportSet{
 | |
| 			CommentList: []ast.Comment{ts.CommentFromString(fmt.Sprintf("Raw generated types from %s kind.", comm.Name), 80, false)},
 | |
| 			TypeOnly:    true,
 | |
| 			Exports:     raw,
 | |
| 			From:        ast.Str{Value: fmt.Sprintf("./raw/%s/%s/%s_types.gen", comm.MachineName, vpath, comm.MachineName)},
 | |
| 		})
 | |
| 	}
 | |
| 	if len(rawD) > 0 {
 | |
| 		ret = append(ret, ast.ExportSet{
 | |
| 			CommentList: []ast.Comment{ts.CommentFromString(fmt.Sprintf("Raw generated enums and default consts from %s kind.", lin.Name()), 80, false)},
 | |
| 			TypeOnly:    false,
 | |
| 			Exports:     rawD,
 | |
| 			From:        ast.Str{Value: fmt.Sprintf("./raw/%s/%s/%s_types.gen", comm.MachineName, vpath, comm.MachineName)},
 | |
| 		})
 | |
| 	}
 | |
| 	vtfile := fmt.Sprintf("./veneer/%s.types", lin.Name())
 | |
| 	customstr := fmt.Sprintf(`// The following exported declarations correspond to types in the %s@%s kind's
 | |
| // schema with attribute @grafana(TSVeneer="type").
 | |
| //
 | |
| // The handwritten file for these type and default veneers is expected to be at
 | |
| // %s.ts.
 | |
| // This re-export declaration enforces that the handwritten veneer file exists,
 | |
| // and exports all the symbols in the list.
 | |
| //
 | |
| // TODO generate code such that tsc enforces type compatibility between raw and veneer decls`,
 | |
| 		lin.Name(), thema.Lineage.Latest(lin).Version(), filepath.ToSlash(filepath.Join(gen.dir, vtfile)))
 | |
| 
 | |
| 	customComments := []ast.Comment{{Text: customstr}}
 | |
| 	if len(custom) > 0 {
 | |
| 		ret = append(ret, ast.ExportSet{
 | |
| 			CommentList: customComments,
 | |
| 			TypeOnly:    true,
 | |
| 			Exports:     custom,
 | |
| 			From:        ast.Str{Value: vtfile},
 | |
| 		})
 | |
| 	}
 | |
| 	if len(customD) > 0 {
 | |
| 		ret = append(ret, ast.ExportSet{
 | |
| 			CommentList: customComments,
 | |
| 			TypeOnly:    false,
 | |
| 			Exports:     customD,
 | |
| 			From:        ast.Str{Value: vtfile},
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	// TODO emit a def in the index.gen.ts that ensures any custom veneer types are "compatible" with current version raw types
 | |
| 	return ret, nil
 | |
| }
 | |
| 
 | |
| type declPair struct {
 | |
| 	T, D   *ast.Ident
 | |
| 	isEnum bool
 | |
| }
 | |
| 
 | |
| type tsVeneerAttr struct {
 | |
| 	target string
 | |
| }
 | |
| 
 | |
| func findDeclNode(name, basename string, tf *ast.File) declPair {
 | |
| 	var p declPair
 | |
| 
 | |
| 	if name == basename {
 | |
| 		return declPair{}
 | |
| 	}
 | |
| 
 | |
| 	for _, def := range tf.Nodes {
 | |
| 		// Peer through export keywords
 | |
| 		if ex, is := def.(ast.ExportKeyword); is {
 | |
| 			def = ex.Decl
 | |
| 		}
 | |
| 
 | |
| 		switch x := def.(type) {
 | |
| 		case ast.TypeDecl:
 | |
| 			if x.Name.Name == name {
 | |
| 				p.T = &x.Name
 | |
| 				_, p.isEnum = x.Type.(ast.EnumType)
 | |
| 				if name == "spec" {
 | |
| 					p.T.Name = basename
 | |
| 				}
 | |
| 			}
 | |
| 		case ast.VarDecl:
 | |
| 			if x.Names.Idents[0].Name == "default"+name {
 | |
| 				p.D = &x.Names.Idents[0]
 | |
| 				if name == "spec" {
 | |
| 					p.D.Name = "default" + basename
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return p
 | |
| }
 | |
| 
 | |
| func walk(v cue.Value, before func(cue.Path, cue.Value) bool, after func(cue.Path, cue.Value)) {
 | |
| 	innerWalk(cue.MakePath(), v, before, after)
 | |
| }
 | |
| 
 | |
| func innerWalk(p cue.Path, v cue.Value, before func(cue.Path, cue.Value) bool, after func(cue.Path, cue.Value)) {
 | |
| 	switch v.Kind() {
 | |
| 	default:
 | |
| 		if before != nil && !before(p, v) {
 | |
| 			return
 | |
| 		}
 | |
| 	case cue.StructKind:
 | |
| 		if before != nil && !before(p, v) {
 | |
| 			return
 | |
| 		}
 | |
| 		iter, err := v.Fields(cue.All())
 | |
| 		if err != nil {
 | |
| 			panic(err)
 | |
| 		}
 | |
| 
 | |
| 		for iter.Next() {
 | |
| 			innerWalk(appendPath(p, iter.Selector()), iter.Value(), before, after)
 | |
| 		}
 | |
| 		if lv := v.LookupPath(cue.MakePath(cue.AnyString)); lv.Exists() {
 | |
| 			innerWalk(appendPath(p, cue.AnyString), lv, before, after)
 | |
| 		}
 | |
| 	case cue.ListKind:
 | |
| 		if before != nil && !before(p, v) {
 | |
| 			return
 | |
| 		}
 | |
| 		list, err := v.List()
 | |
| 		if err != nil {
 | |
| 			panic(err)
 | |
| 		}
 | |
| 		for i := 0; list.Next(); i++ {
 | |
| 			innerWalk(appendPath(p, cue.Index(i)), list.Value(), before, after)
 | |
| 		}
 | |
| 		if lv := v.LookupPath(cue.MakePath(cue.AnyIndex)); lv.Exists() {
 | |
| 			innerWalk(appendPath(p, cue.AnyString), lv, before, after)
 | |
| 		}
 | |
| 	}
 | |
| 	if after != nil {
 | |
| 		after(p, v)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func appendPath(p cue.Path, sel cue.Selector) cue.Path {
 | |
| 	return cue.MakePath(append(p.Selectors(), sel)...)
 | |
| }
 | |
| 
 | |
| func getCustomVeneerAttr(v cue.Value) ([]tsVeneerAttr, error) {
 | |
| 	var attrs []tsVeneerAttr
 | |
| 	for _, a := range v.Attributes(cue.ValueAttr) {
 | |
| 		if a.Name() != "grafana" {
 | |
| 			continue
 | |
| 		}
 | |
| 		for i := 0; i < a.NumArgs(); i++ {
 | |
| 			key, av := a.Arg(i)
 | |
| 			if key != "TSVeneer" {
 | |
| 				return nil, valError(v, "attribute 'grafana' only allows the arg 'TSVeneer'")
 | |
| 			}
 | |
| 
 | |
| 			aterr := valError(v, "@grafana(TSVeneer=\"x\") requires one or more of the following separated veneer types for x: %s", allowedTSVeneersString())
 | |
| 			var some bool
 | |
| 			for _, tgt := range strings.Split(av, "|") {
 | |
| 				some = true
 | |
| 				if !allowedTSVeneers[tgt] {
 | |
| 					return nil, aterr
 | |
| 				}
 | |
| 				attrs = append(attrs, tsVeneerAttr{
 | |
| 					target: tgt,
 | |
| 				})
 | |
| 			}
 | |
| 			if !some {
 | |
| 				return nil, aterr
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	sort.Slice(attrs, func(i, j int) bool {
 | |
| 		return attrs[i].target < attrs[j].target
 | |
| 	})
 | |
| 
 | |
| 	return attrs, nil
 | |
| }
 | |
| 
 | |
| var allowedTSVeneers = map[string]bool{
 | |
| 	"type": true,
 | |
| }
 | |
| 
 | |
| func allowedTSVeneersString() string {
 | |
| 	list := make([]string, 0, len(allowedTSVeneers))
 | |
| 	for tgt := range allowedTSVeneers {
 | |
| 		list = append(list, tgt)
 | |
| 	}
 | |
| 	sort.Strings(list)
 | |
| 
 | |
| 	return strings.Join(list, "|")
 | |
| }
 | |
| 
 | |
| func valError(v cue.Value, format string, args ...any) error {
 | |
| 	s := v.Source()
 | |
| 	if s == nil {
 | |
| 		return fmt.Errorf(format, args...)
 | |
| 	}
 | |
| 	return errors.Newf(s.Pos(), format, args...)
 | |
| }
 |