mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Using the prefix "Model" on all generated types was adding more verbosity to already lengthy generated types. This removed that prefix, only replacing the base generated struct with the literal "Model" to avoid a redundant name, e.g. dashboard.Dashboard.
460 lines
13 KiB
Go
460 lines
13 KiB
Go
package codegen
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing/fstest"
|
|
"text/template"
|
|
|
|
"cuelang.org/go/pkg/encoding/yaml"
|
|
"github.com/deepmap/oapi-codegen/pkg/codegen"
|
|
"github.com/getkin/kin-openapi/openapi3"
|
|
"github.com/grafana/cuetsy"
|
|
"github.com/grafana/grafana/pkg/cuectx"
|
|
"github.com/grafana/thema"
|
|
"github.com/grafana/thema/encoding/openapi"
|
|
"golang.org/x/tools/imports"
|
|
)
|
|
|
|
// ExtractedLineage contains the results of statically analyzing a Grafana
|
|
// directory for a Thema lineage.
|
|
type ExtractedLineage struct {
|
|
Lineage thema.Lineage
|
|
// Absolute path to the coremodel's coremodel.cue file.
|
|
LineagePath string
|
|
// Path to the coremodel's coremodel.cue file relative to repo root.
|
|
RelativePath string
|
|
// Indicates whether the coremodel is considered canonical or not. Generated
|
|
// code from not-yet-canonical coremodels should include appropriate caveats in
|
|
// documentation and possibly be hidden from external public API surface areas.
|
|
IsCanonical bool
|
|
}
|
|
|
|
// ExtractLineage loads a Grafana Thema lineage from the filesystem.
|
|
//
|
|
// The provided path must be the absolute path to the file containing the
|
|
// lineage to be loaded.
|
|
//
|
|
// This loading approach is intended primarily for use with code generators, or
|
|
// other use cases external to grafana-server backend. For code within
|
|
// grafana-server, prefer lineage loaders provided in e.g. pkg/coremodel/*.
|
|
func ExtractLineage(path string, lib thema.Library) (*ExtractedLineage, error) {
|
|
if !filepath.IsAbs(path) {
|
|
return nil, fmt.Errorf("must provide an absolute path, got %q", path)
|
|
}
|
|
|
|
ec := &ExtractedLineage{
|
|
LineagePath: path,
|
|
}
|
|
|
|
var find func(path string) (string, error)
|
|
find = func(path string) (string, error) {
|
|
parent := filepath.Dir(path)
|
|
if parent == path {
|
|
return "", errors.New("grafana root directory could not be found")
|
|
}
|
|
fp := filepath.Join(path, "go.mod")
|
|
if _, err := os.Stat(fp); err == nil {
|
|
return path, nil
|
|
}
|
|
return find(parent)
|
|
}
|
|
groot, err := find(path)
|
|
if err != nil {
|
|
return ec, err
|
|
}
|
|
|
|
f, err := os.Open(ec.LineagePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not open lineage file at %s: %w", path, err)
|
|
}
|
|
|
|
byt, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fs := fstest.MapFS{
|
|
"coremodel.cue": &fstest.MapFile{
|
|
Data: byt,
|
|
},
|
|
}
|
|
|
|
ec.RelativePath, err = filepath.Rel(groot, filepath.Dir(path))
|
|
if err != nil {
|
|
// should be unreachable, since we rootclimbed to find groot above
|
|
panic(err)
|
|
}
|
|
ec.Lineage, err = cuectx.LoadGrafanaInstancesWithThema(ec.RelativePath, fs, lib)
|
|
if err != nil {
|
|
return ec, err
|
|
}
|
|
ec.IsCanonical = isCanonical(ec.Lineage.Name())
|
|
return ec, nil
|
|
}
|
|
|
|
// toTemplateObj extracts creates a struct with all the useful strings for template generation.
|
|
func (ls *ExtractedLineage) toTemplateObj() tplVars {
|
|
lin := ls.Lineage
|
|
sch := thema.SchemaP(lin, thema.LatestVersion(lin))
|
|
|
|
return tplVars{
|
|
Name: lin.Name(),
|
|
LineagePath: ls.RelativePath,
|
|
PkgPath: filepath.ToSlash(filepath.Join("github.com/grafana/grafana", ls.RelativePath)),
|
|
TitleName: strings.Title(lin.Name()), // nolint
|
|
LatestSeqv: sch.Version()[0],
|
|
LatestSchv: sch.Version()[1],
|
|
}
|
|
}
|
|
|
|
func isCanonical(name string) bool {
|
|
return canonicalCoremodels[name]
|
|
}
|
|
|
|
// FIXME specifying coremodel canonicality DOES NOT belong here - it should be part of the coremodel declaration.
|
|
var canonicalCoremodels = map[string]bool{
|
|
"dashboard": false,
|
|
}
|
|
|
|
// GenerateGoCoremodel generates a standard Go model struct and coremodel
|
|
// implementation from a coremodel CUE declaration.
|
|
//
|
|
// The provided path must be a directory. Generated code files will be written
|
|
// to that path. The final element of the path must match the Lineage.Name().
|
|
func (ls *ExtractedLineage) GenerateGoCoremodel(path string) (WriteDiffer, error) {
|
|
lin, lib := ls.Lineage, ls.Lineage.Library()
|
|
_, name := filepath.Split(path)
|
|
if name != lin.Name() {
|
|
return nil, fmt.Errorf("lineage name %q must match final element of path, got %q", lin.Name(), path)
|
|
}
|
|
|
|
sch := thema.SchemaP(lin, thema.LatestVersion(lin))
|
|
f, err := openapi.GenerateSchema(sch, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("thema openapi generation failed: %w", err)
|
|
}
|
|
|
|
str, err := yaml.Marshal(lib.Context().BuildFile(f))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cue-yaml marshaling failed: %w", err)
|
|
}
|
|
|
|
loader := openapi3.NewLoader()
|
|
oT, err := loader.LoadFromData([]byte(str))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading generated openapi failed; %w", err)
|
|
}
|
|
|
|
gostr, err := codegen.Generate(oT, lin.Name(), codegen.Options{
|
|
GenerateTypes: true,
|
|
SkipPrune: true,
|
|
SkipFmt: true,
|
|
UserTemplates: map[string]string{
|
|
"imports.tmpl": fmt.Sprintf(tmplImports, ls.RelativePath),
|
|
"typedef.tmpl": tmplTypedef,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("openapi generation failed: %w", err)
|
|
}
|
|
|
|
vars := ls.toTemplateObj()
|
|
var buuf bytes.Buffer
|
|
err = tmplAddenda.Execute(&buuf, vars)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
fset := token.NewFileSet()
|
|
fname := fmt.Sprintf("%s_gen.go", lin.Name())
|
|
gf, err := parser.ParseFile(fset, fname, gostr+buuf.String(), parser.ParseComments)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generated go file parsing failed: %w", err)
|
|
}
|
|
ast.Walk(prefixDropper(strings.Title(lin.Name())), gf)
|
|
|
|
var buf bytes.Buffer
|
|
err = format.Node(&buf, fset, gf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ast printing failed: %w", err)
|
|
}
|
|
|
|
byt, err := imports.Process(fname, buf.Bytes(), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("goimports processing failed: %w", err)
|
|
}
|
|
|
|
wd := NewWriteDiffer()
|
|
wd[filepath.Join(path, fname)] = byt
|
|
|
|
return wd, nil
|
|
}
|
|
|
|
type tplVars struct {
|
|
Name string
|
|
LineagePath, PkgPath string
|
|
TitleName string
|
|
LatestSeqv, LatestSchv uint
|
|
IsComposed bool
|
|
}
|
|
|
|
func (ls *ExtractedLineage) GenerateTypescriptCoremodel(path string) (WriteDiffer, error) {
|
|
_, name := filepath.Split(path)
|
|
if name != ls.Lineage.Name() {
|
|
return nil, fmt.Errorf("lineage name %q must match final element of path, got %q", ls.Lineage.Name(), path)
|
|
}
|
|
|
|
schv := thema.SchemaP(ls.Lineage, thema.LatestVersion(ls.Lineage)).UnwrapCUE()
|
|
|
|
parts, err := cuetsy.GenerateAST(schv, cuetsy.Config{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cuetsy parts gen failed: %w", err)
|
|
}
|
|
|
|
top, err := cuetsy.GenerateSingleAST(strings.Title(ls.Lineage.Name()), schv, cuetsy.TypeInterface)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cuetsy top gen failed: %w", err)
|
|
}
|
|
|
|
// TODO until cuetsy can toposort its outputs, put the top/parent type at the bottom of the file.
|
|
parts.Nodes = append(parts.Nodes, top.T)
|
|
if top.D != nil {
|
|
parts.Nodes = append(parts.Nodes, top.D)
|
|
}
|
|
|
|
var strb strings.Builder
|
|
var str string
|
|
fpath := ls.Lineage.Name() + ".gen.ts"
|
|
strb.WriteString(fmt.Sprintf(genHeader, ls.RelativePath))
|
|
|
|
if !ls.IsCanonical {
|
|
fpath = fmt.Sprintf("%s_experimental.gen.ts", ls.Lineage.Name())
|
|
strb.WriteString(`
|
|
// This model is a WIP and not yet canonical. Consequently, its members are
|
|
// not exported to exclude it from grafana-schema's public API surface.
|
|
|
|
`)
|
|
strb.WriteString(fmt.Sprint(parts))
|
|
// TODO replace this regexp with cuetsy config for whether members are exported
|
|
re := regexp.MustCompile(`(?m)^export `)
|
|
str = re.ReplaceAllLiteralString(strb.String(), "")
|
|
} else {
|
|
strb.WriteString(fmt.Sprint(parts))
|
|
str = strb.String()
|
|
}
|
|
|
|
wd := NewWriteDiffer()
|
|
wd[filepath.Join(path, fpath)] = []byte(str)
|
|
return wd, nil
|
|
}
|
|
|
|
type prefixDropper string
|
|
|
|
func (d prefixDropper) Visit(n ast.Node) ast.Visitor {
|
|
asstr := string(d)
|
|
switch x := n.(type) {
|
|
case *ast.Ident:
|
|
if x.Name != asstr {
|
|
x.Name = strings.TrimPrefix(x.Name, asstr)
|
|
} else {
|
|
x.Name = "Model"
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
// GenerateCoremodelRegistry produces Go files that define a registry with
|
|
// references to all the Go code that is expected to be generated from the
|
|
// provided lineages.
|
|
func GenerateCoremodelRegistry(path string, ecl []*ExtractedLineage) (WriteDiffer, error) {
|
|
var cml []tplVars
|
|
for _, ec := range ecl {
|
|
cml = append(cml, ec.toTemplateObj())
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err := tmplRegistry.Execute(&buf, struct {
|
|
Coremodels []tplVars
|
|
}{
|
|
Coremodels: cml,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed generating template: %w", err)
|
|
}
|
|
|
|
byt, err := imports.Process(path, buf.Bytes(), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("goimports processing failed: %w", err)
|
|
}
|
|
|
|
wd := NewWriteDiffer()
|
|
wd[path] = byt
|
|
return wd, nil
|
|
}
|
|
|
|
var genHeader = `// This file is autogenerated. DO NOT EDIT.
|
|
//
|
|
// Run "make gen-cue" from repository root to regenerate.
|
|
//
|
|
// Derived from the Thema lineage at %s
|
|
|
|
`
|
|
|
|
var tmplImports = genHeader + `package {{ .PackageName }}
|
|
|
|
import (
|
|
"embed"
|
|
"path/filepath"
|
|
|
|
"github.com/grafana/grafana/pkg/cuectx"
|
|
"github.com/grafana/grafana/pkg/framework/coremodel"
|
|
"github.com/grafana/thema"
|
|
)
|
|
`
|
|
|
|
var tmplAddenda = template.Must(template.New("addenda").Parse(`
|
|
//go:embed coremodel.cue
|
|
var cueFS embed.FS
|
|
|
|
// codegen ensures that this is always the latest Thema schema version
|
|
var currentVersion = thema.SV({{ .LatestSeqv }}, {{ .LatestSchv }})
|
|
|
|
// Lineage returns the Thema lineage representing a Grafana {{ .Name }}.
|
|
//
|
|
// The lineage is the canonical specification of the current {{ .Name }} schema,
|
|
// all prior schema versions, and the mappings that allow migration between
|
|
// schema versions.
|
|
{{- if .IsComposed }}//
|
|
// This is the base variant of the schema. It does not include any composed
|
|
// plugin schemas.{{ end }}
|
|
func Lineage(lib thema.Library, opts ...thema.BindOption) (thema.Lineage, error) {
|
|
return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("pkg", "coremodel", "{{ .Name }}"), cueFS, lib, opts...)
|
|
}
|
|
|
|
var _ thema.LineageFactory = Lineage
|
|
var _ coremodel.Interface = &Coremodel{}
|
|
|
|
// Coremodel contains the foundational schema declaration for {{ .Name }}s.
|
|
// It implements coremodel.Interface.
|
|
type Coremodel struct {
|
|
lin thema.Lineage
|
|
}
|
|
|
|
// Lineage returns the canonical {{ .Name }} Lineage.
|
|
func (c *Coremodel) Lineage() thema.Lineage {
|
|
return c.lin
|
|
}
|
|
|
|
// CurrentSchema returns the current (latest) {{ .Name }} Thema schema.
|
|
func (c *Coremodel) CurrentSchema() thema.Schema {
|
|
return thema.SchemaP(c.lin, currentVersion)
|
|
}
|
|
|
|
// GoType returns a pointer to an empty Go struct that corresponds to
|
|
// the current Thema schema.
|
|
func (c *Coremodel) GoType() interface{} {
|
|
return &Model{}
|
|
}
|
|
|
|
// New returns a new instance of the {{ .Name }} coremodel.
|
|
//
|
|
// Note that this function does not cache, and initially loading a Thema lineage
|
|
// can be expensive. As such, the Grafana backend should prefer to access this
|
|
// coremodel through a registry (pkg/framework/coremodel/registry), which does cache.
|
|
func New(lib thema.Library) (*Coremodel, error) {
|
|
lin, err := Lineage(lib)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Coremodel{
|
|
lin: lin,
|
|
}, nil
|
|
}
|
|
`))
|
|
|
|
var tmplTypedef = `{{range .Types}}
|
|
{{ with .Schema.Description }}{{ . }}{{ else }}// {{.TypeName}} defines model for {{.JsonName}}.{{ end }}
|
|
//
|
|
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
|
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
|
type {{.TypeName}} {{if and (opts.AliasTypes) (.CanAlias)}}={{end}} {{.Schema.TypeDecl}}
|
|
{{end}}
|
|
`
|
|
|
|
var tmplRegistry = template.Must(template.New("registry").Parse(`
|
|
// This file is autogenerated. DO NOT EDIT.
|
|
//
|
|
// Generated by pkg/framework/coremodel/gen.go
|
|
// Run "make gen-cue" from repository root to regenerate.
|
|
|
|
package registry
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/google/wire"
|
|
{{range .Coremodels }}
|
|
"{{ .PkgPath }}"{{end}}
|
|
"github.com/grafana/grafana/pkg/cuectx"
|
|
"github.com/grafana/grafana/pkg/framework/coremodel"
|
|
"github.com/grafana/thema"
|
|
)
|
|
|
|
// Base is a registry of coremodel.Interface. It provides two modes for accessing
|
|
// coremodels: individually via literal named methods, or as a slice returned from All().
|
|
//
|
|
// Prefer the individual named methods for use cases where the particular coremodel(s) that
|
|
// are needed are known to the caller. For example, a dashboard linter can know that it
|
|
// specifically wants the dashboard coremodel.
|
|
//
|
|
// Prefer All() when performing operations generically across all coremodels. For example,
|
|
// a validation HTTP middleware for any coremodel-schematized object type.
|
|
type Base struct {
|
|
all []coremodel.Interface
|
|
{{- range .Coremodels }}
|
|
{{ .Name }} *{{ .Name }}.Coremodel{{end}}
|
|
}
|
|
|
|
// type guards
|
|
var (
|
|
{{- range .Coremodels }}
|
|
_ coremodel.Interface = &{{ .Name }}.Coremodel{}{{end}}
|
|
)
|
|
|
|
{{range .Coremodels }}
|
|
// {{ .TitleName }} returns the {{ .Name }} coremodel. The return value is guaranteed to
|
|
// implement coremodel.Interface.
|
|
func (s *Base) {{ .TitleName }}() *{{ .Name }}.Coremodel {
|
|
return s.{{ .Name }}
|
|
}
|
|
{{end}}
|
|
|
|
func doProvideBase(lib thema.Library) *Base {
|
|
var err error
|
|
reg := &Base{}
|
|
|
|
{{range .Coremodels }}
|
|
reg.{{ .Name }}, err = {{ .Name }}.New(lib)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("error while initializing {{ .Name }} coremodel: %s", err))
|
|
}
|
|
reg.all = append(reg.all, reg.{{ .Name }})
|
|
{{end}}
|
|
|
|
return reg
|
|
}
|
|
`))
|