The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/codegen/jenny_tsveneerindex.go

357 lines
9.6 KiB

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...)
}