Kindsys: Target k8s style resource definitions (#67008)
Co-authored-by: sam boyer <sdboyer@grafana.com>
This commit is contained in:
co-authored by
sam boyer
parent
b71b778d0d
commit
ca1f79b9ba
@@ -8,21 +8,25 @@ import (
|
||||
"github.com/grafana/kindsys"
|
||||
)
|
||||
|
||||
// LatestMajorsOrXJenny returns a jenny that repeats the input for the latest in each major version,
|
||||
func LatestMajorsOrXJenny(parentdir string, inner codejen.OneToOne[SchemaForGen]) OneToMany {
|
||||
// LatestMajorsOrXJenny returns a jenny that repeats the input for the latest in each major version.
|
||||
//
|
||||
// TODO remove forceGroup option, it's a temporary hack to accommodate core kinds
|
||||
func LatestMajorsOrXJenny(parentdir string, forceGroup bool, inner codejen.OneToOne[SchemaForGen]) OneToMany {
|
||||
if inner == nil {
|
||||
panic("inner jenny must not be nil")
|
||||
}
|
||||
|
||||
return &lmox{
|
||||
parentdir: parentdir,
|
||||
inner: inner,
|
||||
parentdir: parentdir,
|
||||
inner: inner,
|
||||
forceGroup: forceGroup,
|
||||
}
|
||||
}
|
||||
|
||||
type lmox struct {
|
||||
parentdir string
|
||||
inner codejen.OneToOne[SchemaForGen]
|
||||
parentdir string
|
||||
inner codejen.OneToOne[SchemaForGen]
|
||||
forceGroup bool
|
||||
}
|
||||
|
||||
func (j *lmox) JennyName() string {
|
||||
@@ -36,6 +40,10 @@ func (j *lmox) Generate(kind kindsys.Kind) (codejen.Files, error) {
|
||||
IsGroup: comm.LineageIsGroup,
|
||||
}
|
||||
|
||||
if j.forceGroup {
|
||||
sfg.IsGroup = true
|
||||
}
|
||||
|
||||
do := func(sfg SchemaForGen, infix string) (codejen.Files, error) {
|
||||
f, err := j.inner.Generate(sfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"cuelang.org/go/cue"
|
||||
"github.com/dave/dst/dstutil"
|
||||
"github.com/grafana/codejen"
|
||||
"github.com/grafana/kindsys"
|
||||
"github.com/grafana/thema/encoding/gocode"
|
||||
"github.com/grafana/thema/encoding/openapi"
|
||||
)
|
||||
|
||||
type ResourceGoTypesJenny struct {
|
||||
ApplyFuncs []dstutil.ApplyFunc
|
||||
ExpandReferences bool
|
||||
}
|
||||
|
||||
func (*ResourceGoTypesJenny) JennyName() string {
|
||||
return "GoTypesJenny"
|
||||
}
|
||||
|
||||
func (ag *ResourceGoTypesJenny) Generate(kind kindsys.Kind) (*codejen.File, error) {
|
||||
comm := kind.Props().Common()
|
||||
sfg := SchemaForGen{
|
||||
Name: comm.Name,
|
||||
Schema: kind.Lineage().Latest(),
|
||||
IsGroup: comm.LineageIsGroup,
|
||||
}
|
||||
sch := sfg.Schema
|
||||
|
||||
iter, err := sch.Underlying().Fields()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var subr []string
|
||||
for iter.Next() {
|
||||
subr = append(subr, typeNameFromKey(iter.Selector().String()))
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
mname := kind.Props().Common().MachineName
|
||||
if err := tmpls.Lookup("core_resource.tmpl").Execute(buf, tvars_resource{
|
||||
PackageName: mname,
|
||||
KindName: kind.Props().Common().Name,
|
||||
SubresourceNames: subr,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed executing core resource template: %w", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return codejen.NewFile(fmt.Sprintf("pkg/kinds/%s/%s_gen.go", mname, mname), buf.Bytes(), ag), nil
|
||||
}
|
||||
|
||||
type SubresourceGoTypesJenny struct {
|
||||
ApplyFuncs []dstutil.ApplyFunc
|
||||
ExpandReferences bool
|
||||
}
|
||||
|
||||
func (*SubresourceGoTypesJenny) JennyName() string {
|
||||
return "GoResourceTypes"
|
||||
}
|
||||
|
||||
func (g *SubresourceGoTypesJenny) Generate(kind kindsys.Kind) (codejen.Files, error) {
|
||||
comm := kind.Props().Common()
|
||||
sfg := SchemaForGen{
|
||||
Name: comm.Name,
|
||||
Schema: kind.Lineage().Latest(),
|
||||
IsGroup: comm.LineageIsGroup,
|
||||
}
|
||||
sch := sfg.Schema
|
||||
|
||||
// Iterate through all top-level fields and make go types for them
|
||||
// (this should consist of "spec" and arbitrary subresources)
|
||||
i, err := sch.Underlying().Fields()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := make(codejen.Files, 0)
|
||||
for i.Next() {
|
||||
str := i.Selector().String()
|
||||
|
||||
b, err := gocode.GenerateTypesOpenAPI(sch, &gocode.TypeConfigOpenAPI{
|
||||
// TODO will need to account for sanitizing e.g. dashes here at some point
|
||||
Config: &openapi.Config{
|
||||
Group: false, // TODO: better
|
||||
RootName: typeNameFromKey(str),
|
||||
Subpath: cue.MakePath(cue.Str(str)),
|
||||
},
|
||||
PackageName: sfg.Schema.Lineage().Name(),
|
||||
ApplyFuncs: append(g.ApplyFuncs, PrefixDropper(sfg.Name)),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := sfg.Schema.Lineage().Name()
|
||||
files = append(files, codejen.File{
|
||||
RelativePath: fmt.Sprintf("pkg/kinds/%s/%s_%s_gen.go", name, name, strings.ToLower(str)),
|
||||
Data: b,
|
||||
From: []codejen.NamedJenny{g},
|
||||
})
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func typeNameFromKey(key string) string {
|
||||
if len(key) > 0 {
|
||||
return strings.ToUpper(key[:1]) + key[1:]
|
||||
}
|
||||
return strings.ToUpper(key)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/grafana/codejen"
|
||||
"github.com/grafana/cuetsy/ts"
|
||||
"github.com/grafana/cuetsy/ts/ast"
|
||||
"github.com/grafana/thema/encoding/typescript"
|
||||
)
|
||||
|
||||
// TSResourceJenny is a [OneToOne] that produces TypeScript types and
|
||||
// defaults for a Thema schema.
|
||||
//
|
||||
// Thema's generic TS jenny will be able to replace this one once
|
||||
// https://github.com/grafana/thema/issues/89 is complete.
|
||||
type TSResourceJenny struct{}
|
||||
|
||||
var _ codejen.OneToOne[SchemaForGen] = &TSResourceJenny{}
|
||||
|
||||
func (j TSResourceJenny) JennyName() string {
|
||||
return "TSResourceJenny"
|
||||
}
|
||||
|
||||
func (j TSResourceJenny) Generate(sfg SchemaForGen) (*codejen.File, error) {
|
||||
// TODO allow using name instead of machine name in thema generator
|
||||
f, err := typescript.GenerateTypes(sfg.Schema, &typescript.TypeConfig{
|
||||
RootName: sfg.Name,
|
||||
Group: sfg.IsGroup,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
renameSpecNode(sfg.Name, f)
|
||||
|
||||
return codejen.NewFile(sfg.Schema.Lineage().Name()+"_types.gen.ts", []byte(f.String()), j), nil
|
||||
}
|
||||
|
||||
func renameSpecNode(name string, tf *ast.File) {
|
||||
specidx, specdefidx := -1, -1
|
||||
for idx, 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 == "spec" {
|
||||
specidx = idx
|
||||
x.Name.Name = name
|
||||
tf.Nodes[idx] = x
|
||||
}
|
||||
case ast.VarDecl:
|
||||
// Before:
|
||||
// export const defaultspec: Partial<spec> = {
|
||||
// After:
|
||||
/// export const defaultPlaylist: Partial<Playlist> = {
|
||||
if x.Names.Idents[0].Name == "defaultspec" {
|
||||
specdefidx = idx
|
||||
x.Names.Idents[0].Name = "default" + name
|
||||
tt := x.Type.(ast.TypeTransformExpr)
|
||||
tt.Expr = ts.Ident(name)
|
||||
x.Type = tt
|
||||
tf.Nodes[idx] = x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if specidx != -1 {
|
||||
decl := tf.Nodes[specidx]
|
||||
tf.Nodes = append(append(tf.Nodes[:specidx], tf.Nodes[specidx+1:]...), decl)
|
||||
}
|
||||
if specdefidx != -1 {
|
||||
if specdefidx > specidx {
|
||||
specdefidx--
|
||||
}
|
||||
decl := tf.Nodes[specdefidx]
|
||||
tf.Nodes = append(append(tf.Nodes[:specdefidx], tf.Nodes[specdefidx+1:]...), decl)
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ func (gen *genTSVeneerIndex) Generate(kinds ...kindsys.Kind) (*codejen.File, err
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", def.Props().Common().Name, err)
|
||||
}
|
||||
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)
|
||||
@@ -75,13 +76,13 @@ func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(def kindsys.Kind, tf *
|
||||
sels := p.Selectors()
|
||||
switch len(sels) {
|
||||
case 0:
|
||||
name = comm.Name
|
||||
fallthrough
|
||||
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() {
|
||||
if !(sels[0].IsDefinition() || sels[0].String() == "spec") {
|
||||
return false
|
||||
}
|
||||
// It might seem to make sense that we'd strip replaceout the leading # here for
|
||||
@@ -90,6 +91,10 @@ func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(def kindsys.Kind, tf *
|
||||
name = sels[0].String()
|
||||
}
|
||||
|
||||
if name == "spec" {
|
||||
name = comm.Name
|
||||
}
|
||||
|
||||
// Search the generated TS AST for the type and default def nodes
|
||||
pair := findDeclNode(name, tf)
|
||||
if pair.T == nil {
|
||||
|
||||
+8
-1
@@ -3,6 +3,7 @@ package codegen
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
@@ -15,7 +16,8 @@ var tmpls *template.Template
|
||||
|
||||
func init() {
|
||||
base := template.New("codegen").Funcs(template.FuncMap{
|
||||
"now": time.Now,
|
||||
"now": time.Now,
|
||||
"ToLower": strings.ToLower,
|
||||
})
|
||||
tmpls = template.Must(base.ParseFS(tmplFS, "tmpl/*.tmpl"))
|
||||
}
|
||||
@@ -46,6 +48,11 @@ type (
|
||||
tvars_coremodel_imports struct {
|
||||
PackageName string
|
||||
}
|
||||
tvars_resource struct {
|
||||
PackageName string
|
||||
KindName string
|
||||
SubresourceNames []string
|
||||
}
|
||||
)
|
||||
|
||||
type HeaderVars = tvars_autogen_header
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package {{ .PackageName }}
|
||||
|
||||
// Resource is the wire representation of {{ .KindName }}. (TODO be better)
|
||||
type Resource struct {
|
||||
{{- range .SubresourceNames }}
|
||||
{{ . }} {{ . }} `json:"{{ . | ToLower }}"`{{end}}
|
||||
}
|
||||
@@ -17,9 +17,9 @@ const rootrel string = "kinds/{{ .Props.MachineName }}"
|
||||
// TODO standard generated docs
|
||||
type Kind struct {
|
||||
kindsys.Core
|
||||
lin thema.ConvergentLineage[*{{ .Props.Name }}]
|
||||
lin thema.ConvergentLineage[*Resource]
|
||||
jcodec vmux.Codec
|
||||
valmux vmux.ValueMux[*{{ .Props.Name }}]
|
||||
valmux vmux.ValueMux[*Resource]
|
||||
}
|
||||
|
||||
// type guard - ensure generated Kind type satisfies the kindsys.Core interface
|
||||
@@ -40,7 +40,7 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
|
||||
// Get the thema.Schema that the meta says is in the current version (which
|
||||
// codegen ensures is always the latest)
|
||||
cursch := thema.SchemaP(k.Core.Lineage(), def.Properties.CurrentVersion)
|
||||
tsch, err := thema.BindType[*{{ .Props.Name }}](cursch, &{{ .Props.Name }}{})
|
||||
tsch, err := thema.BindType(cursch, &Resource{})
|
||||
if err != nil {
|
||||
// Should be unreachable, modulo bugs in the Thema->Go code generator
|
||||
return nil, err
|
||||
@@ -53,18 +53,18 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
|
||||
}
|
||||
|
||||
// ConvergentLineage returns the same [thema.Lineage] as Lineage, but bound (see [thema.BindType])
|
||||
// to the the {{ .Props.Name }} type generated from the current schema, v{{ .Props.CurrentVersion }}.
|
||||
func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*{{ .Props.Name }}] {
|
||||
// to the the {{ .Props.Name }} [Resource] type generated from the current schema, v{{ .Props.CurrentVersion }}.
|
||||
func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*Resource] {
|
||||
return k.lin
|
||||
}
|
||||
|
||||
// JSONValueMux is a version multiplexer that maps a []byte containing JSON data
|
||||
// at any schematized dashboard version to an instance of {{ .Props.Name }}.
|
||||
// at any schematized dashboard version to an instance of {{ .Props.Name }} [Resource].
|
||||
//
|
||||
// Validation and translation errors emitted from this func will identify the
|
||||
// input bytes as "dashboard.json".
|
||||
//
|
||||
// This is a thin wrapper around Thema's [vmux.ValueMux].
|
||||
func (k *Kind) JSONValueMux(b []byte) (*{{ .Props.Name }}, thema.TranslationLacunas, error) {
|
||||
func (k *Kind) JSONValueMux(b []byte) (*Resource, thema.TranslationLacunas, error) {
|
||||
return k.valmux(b)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user