Reconcile coremodels, entities, objects under new kind framework (#56492)

* Update thema to latest

* Deal with s/Library/*Runtime/

* Commit new, working results of codegen

* We like pointers now

* Always take runtime arg for NewBase()

* Sketchy handwavy pass at entity meta framework

* Little nibbles

* Update pkg/framework/coremodel/entityframework.cue

Co-authored-by: Artur Wierzbicki <wierzbicki.artur.94@gmail.com>

* Move file into new framework location

* Introduce loaders, Go code

* Complete rename to kind

* Flesh out framework, add svg/dashboard examples

* Cruft removal

* Remove generated kind go files from gitignore

* Refine maturity concept, add SlotKind

* Update embed and go deps

* Export PrefixWithGrafanaCUE

* Make the loader actually work, holy crap

* Many small tweaks to type.cue

* Add Apache 2 licensing exceptions for kinds

* Add new kinds dir, start of generator

* Roll back to earlier oapi-codegen

* Introduce new grafana-specific CUE loaders

* Introduce new tidy code generators framework

* Catch up kind framework with tinkering

* Add slices for the generators

* Add write/verify step to main generator

* Many renames

* Split up kind framework cue files

* Use kind.Decl within generated kinds

* Create kind.SomeDecl wrapper type to cache lineages

* Better names again

* Get one generated implemented, hopefully

* Copy dashboard schema into new kind.cue

* Small fixes to make the initial gen work

* Put svg kind in its new home

* Add generated Go dashboard type

* More renames and cleanups

* Add base kind registry and generator

* Stop blacklisting *_gen.go files

This is not the Go best practice, anyway. All we actually want to ignore
for enterprise is generated wire files.

* Change codegen output directories

pkg/kind -> pkg/kinds
pkg/registry/kindreg -> pkg/registry/corekind

* Rename pkg/framework/kind to pkg/kindsys

* Add core structured kind generator

* Add plural and machine names to kind spec

* Copy playlist over to kind system

* Consolidate kindsys files

* Add raw kind generator

* Update CODEOWNERS for kind framework

* Touch up comments a bit

* More docs tweaks

* Remove generated types to reduce noise for review

* Split each generator into its own file

* Rename Slot kind to Composable kind

* Add handwavy types for customkind loading

* Guard against init calls to framework loader

* First pass at doc on extending the kind system

* Improve attribute example in docs

* Fix wire imports

* Add basic TS types generator

* Fix composable kind category def

* No need for a separate file with generate directive

* Catch dashboard schema up

* Rename generator types to something saner and generic

* Make version configurable in ts/go generators

* Add CommonMeta to ease property access

* Add kindsys prop indicating whether lineage is group

* Put all kind categories back in a single file

* Finish with kindsys group props

* Refactor maturity progression per discussion

- Replace "committed" with "merged"
- All kindcats can use all maturity levels, at least for now

* Convert ts veneer index generator to modular system

* Move over to new jennywrites framework

* Strip down old coremodel generator

* Use public version of jennywrites

* Pull latest thema

* Commit generated Go types

* Add header injection postprocessor

* Move sdboyer/jennywrites to grafana/codejen

* Tweak header output

* Remove dashboard and playlist coremodels

* Fix up backend dashboards devenv test

* Fix TS import patterns to new gen filename

* Update internal imports, remove coremodel registry

* Fix compilation errors, wire generation

* Export and replace the prefix dropper

* More Go struct and field name changes

* Last name fixes, hopefully

* Fix lint errors

* Last lint error

Co-authored-by: Artur Wierzbicki <wierzbicki.artur.94@gmail.com>
This commit is contained in:
sam boyer 2022-11-10 15:36:40 -05:00 committed by GitHub
parent f92d978386
commit 07e5f8117f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 2703 additions and 1209 deletions

12
.github/CODEOWNERS vendored
View File

@ -218,7 +218,15 @@ lerna.json @grafana/frontend-ops
# Grafana Partnerships Team
/pkg/infra/httpclient/httpclientprovider/sigv4_middleware.go @grafana/grafana-partnerships-team
# Schema framework and code generation
# Kind system and code generation
embed.go @grafana/grafana-as-code
/kinds/ @grafana/grafana-as-code
/pkg/codegen @grafana/grafana-as-code
/pkg/framework/coremodel @grafana/grafana-as-code
/pkg/kindsys @grafana/grafana-as-code
/pkg/kinds/*/*_gen.go @grafana/grafana-as-code
/pkg/registry/corekind @grafana/grafana-as-code
/public/app/plugins/*gen.go @grafana/grafana-as-code
# Specific core kinds
/kinds/raw/ @grafana/grafana-edge-squad
/kinds/structured/dashboard @grafana/dashboards-squad

10
.gitignore vendored
View File

@ -170,14 +170,8 @@ compilation-stats.json
# auto generated frontend docs
/docs/sources/packages_api
# auto generated Go files
*_gen.go
!pkg/services/featuremgmt/toggles_gen.go
!pkg/coremodel/**/*_gen.go
!pkg/framework/**/*_gen.go
!pkg/plugins/pfs/**/*_gen.go
!public/app/plugins/**/*_gen.go
!pkg/services/publicdashboards/*_gen.go
# wire generated files
**/wire_gen.go
# Auto-generated internationalization files
public/locales/_build/

View File

@ -17,10 +17,11 @@ packages/grafana-toolkit/
packages/grafana-ui/
packages/jaeger-ui-components/
packaging/
pkg/coremodel/
pkg/framework/coremodel/
kinds/
pkg/kinds/
pkg/kindsys/
pkg/registry/corekind/
grafana-mixin/
cue/
public/app/plugins/datasource/tempo
public/img/icons/solid/
public/img/icons/unicons/

View File

@ -66,6 +66,7 @@ openapi3-gen: swagger-api-spec ## Generates OpenApi 3 specs from the Swagger 2 a
##@ Building
gen-cue: ## Do all CUE/Thema code generation
@echo "generate code from .cue files"
go generate ./kinds/gen.go
go generate ./pkg/framework/coremodel
go generate ./public/app/plugins

View File

@ -6,5 +6,5 @@ import (
// CueSchemaFS embeds all schema-related CUE files in the Grafana project.
//
//go:embed cue.mod/module.cue packages/grafana-schema/src/schema/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json pkg/framework/coremodel/*.cue
//go:embed cue.mod/module.cue kinds/*/*.cue kinds/*/*/*.cue packages/grafana-schema/src/schema/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json pkg/framework/coremodel/*.cue pkg/kindsys/*.cue
var CueSchemaFS embed.FS

10
go.mod
View File

@ -61,7 +61,7 @@ require (
github.com/grafana/grafana-aws-sdk v0.11.0
github.com/grafana/grafana-azure-sdk-go v1.3.1
github.com/grafana/grafana-plugin-sdk-go v0.142.0
github.com/grafana/thema v0.0.0-20220929145912-2c7c4a7bb20b
github.com/grafana/thema v0.0.0-20221107225215-00ad2949c7bc
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/hashicorp/go-hclog v1.0.0
github.com/hashicorp/go-plugin v1.4.3
@ -110,7 +110,7 @@ require (
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
golang.org/x/sync v0.1.0
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
golang.org/x/tools v0.1.12
gonum.org/v1/gonum v0.11.0
@ -250,11 +250,13 @@ require (
github.com/bufbuild/connect-go v1.0.0
github.com/dlmiddlecote/sqlstats v1.0.2
github.com/drone/drone-cli v1.6.1
github.com/getkin/kin-openapi v0.94.0
github.com/getkin/kin-openapi v0.103.0
github.com/golang-migrate/migrate/v4 v4.7.0
github.com/google/go-github/v45 v45.2.0
github.com/grafana/codejen v0.0.2
github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f
github.com/jmoiron/sqlx v1.3.5
github.com/kr/pretty v0.3.0
github.com/matryer/is v1.4.0
github.com/parca-dev/parca v0.12.1
github.com/urfave/cli v1.22.9
@ -285,11 +287,13 @@ require (
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/memberlist v0.4.0 // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-ieproxy v0.0.3 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/segmentio/asm v1.1.4 // indirect
go.starlark.net v0.0.0-20221020143700-22309ac47eac // indirect

15
go.sum
View File

@ -847,8 +847,9 @@ github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/getkin/kin-openapi v0.94.0 h1:bAxg2vxgnHHHoeefVdmGbR+oxtJlcv5HsJJa3qmAHuo=
github.com/getkin/kin-openapi v0.94.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q=
github.com/getkin/kin-openapi v0.103.0 h1:F5wAtaQvPWxKCAYZ69LgHAThgu16p4u41VQtbn1U8LA=
github.com/getkin/kin-openapi v0.103.0/go.mod h1:w4lRPHiyOdwGbOkLIyk+P0qCwlu7TXPCHD/64nSXzgE=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
@ -1348,6 +1349,8 @@ github.com/gosimple/slug v1.12.0 h1:xzuhj7G7cGtd34NXnW/yF0l+AGNfWqwgh/IXgFy7dnc=
github.com/gosimple/slug v1.12.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/grafana/codejen v0.0.2 h1:Ssp27X7SOnYxaPUTByW/6201tNV5Q60l1BSF+s3lRP8=
github.com/grafana/codejen v0.0.2/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cuetsy v0.1.1 h1:+1jaDDYCpvKlcOWJgBRbkc5+VZIClCEn5mbI+4PLZqM=
github.com/grafana/cuetsy v0.1.1/go.mod h1:4KWkUOslwvRTpEv7wdQG0jDFTuJmU+0L9x0h4kWxa2A=
github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f h1:FvvSVEbnGeM2bUivGmsiXTi8URJyBU7TcFEEoRe5wWI=
@ -1367,8 +1370,8 @@ github.com/grafana/prometheus-alertmanager v0.24.1-0.20221012142027-823cd9150293
github.com/grafana/prometheus-alertmanager v0.24.1-0.20221012142027-823cd9150293/go.mod h1:HVHqK+BVPa/tmL8EMhLCCrPt2a1GdJpEyxr5hgur2UI=
github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc h1:1PY8n+rXuBNr3r1JQhoytWDCpc+pq+BibxV0SZv+Cr4=
github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc/go.mod h1:9Zh6dWPtB3MSzTRt8fIFH60Z351QQ+s7hCU3J/tTlA4=
github.com/grafana/thema v0.0.0-20220929145912-2c7c4a7bb20b h1:OEGzlaj04LE6Eq7aGMOh0bCplGW5rXNeSSSwgamPBEY=
github.com/grafana/thema v0.0.0-20220929145912-2c7c4a7bb20b/go.mod h1:i3/NX50sNrwsPSAQAj56ckjQTb4biaYG/6y+zyKgpb0=
github.com/grafana/thema v0.0.0-20221107225215-00ad2949c7bc h1:Icv777/PBaqhLmbSBSDaajDl424cbmh5ee77Du2rUFE=
github.com/grafana/thema v0.0.0-20221107225215-00ad2949c7bc/go.mod h1:wnIJykzNiNVANl6g/Z4nkXxoMqaaH1LoG0IPNW++BEk=
github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6 h1:I9dh1MXGX0wGyxdV/Sl7+ugnki4Dfsy8lv2s5Yf887o=
github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
@ -1553,6 +1556,8 @@ github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bS
github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0=
github.com/influxdata/tdigest v0.0.2-0.20210216194612-fc98d27c9e8b/go.mod h1:Z0kXnxzbTC2qrx4NaIzYkE1k66+6oEDQTvL95hQFh5Y=
github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po=
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
@ -2891,8 +2896,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -3379,6 +3385,7 @@ gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=

157
kinds/gen.go Normal file
View File

@ -0,0 +1,157 @@
//go:build ignore
// +build ignore
//go:generate go run gen.go
package main
import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"cuelang.org/go/cue/errors"
"github.com/grafana/codejen"
"github.com/grafana/grafana/pkg/codegen"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/kindsys"
)
const sep = string(filepath.Separator)
func main() {
if len(os.Args) > 1 {
fmt.Fprintf(os.Stderr, "plugin thema code generator does not currently accept any arguments\n, got %q", os.Args)
os.Exit(1)
}
// Core kinds composite code generator. Produces all generated code in
// grafana/grafana that derives from raw and structured core kinds.
coreKindsGen := codejen.JennyListWithNamer[*codegen.DeclForGen](func(decl *codegen.DeclForGen) string {
return decl.Meta.Common().MachineName
})
// All the jennies that comprise the core kinds generator pipeline
coreKindsGen.Append(
codegen.GoTypesJenny(kindsys.GoCoreKindParentPath, nil),
codegen.CoreStructuredKindJenny(kindsys.GoCoreKindParentPath, nil),
codegen.RawKindJenny(kindsys.GoCoreKindParentPath, nil),
codegen.BaseCoreRegistryJenny(filepath.Join("pkg", "registry", "corekind"), kindsys.GoCoreKindParentPath),
codegen.TSTypesJenny(kindsys.TSCoreKindParentPath, &codegen.TSTypesGeneratorConfig{
GenDirName: func(decl *codegen.DeclForGen) string {
// FIXME this hardcodes always generating to experimental dir. OK for now, but need generator fanout
return filepath.Join(decl.Meta.Common().MachineName, "x")
},
}),
codegen.TSVeneerIndexJenny(filepath.Join("packages", "grafana-schema", "src")),
)
coreKindsGen.AddPostprocessors(codegen.SlashHeaderMapper("kinds/gen.go"))
cwd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "could not get working directory: %s", err)
os.Exit(1)
}
grootp := strings.Split(cwd, sep)
groot := filepath.Join(sep, filepath.Join(grootp[:len(grootp)-1]...))
rt := cuectx.GrafanaThemaRuntime()
var all []*codegen.DeclForGen
// structured kinddirs first
f := os.DirFS(filepath.Join(groot, kindsys.CoreStructuredDeclParentPath))
kinddirs := elsedie(fs.ReadDir(f, "."))("error reading structured fs root directory")
for _, ent := range kinddirs {
if !ent.IsDir() {
continue
}
rel := filepath.Join(kindsys.CoreStructuredDeclParentPath, ent.Name())
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rel, rt.Context(), nil)
if err != nil {
die(fmt.Errorf("%s is not a valid kind: %s", rel, errors.Details(err, nil)))
}
if decl.Meta.MachineName != ent.Name() {
die(fmt.Errorf("%s: kind's machine name (%s) must equal parent dir name (%s)", rel, decl.Meta.Name, ent.Name()))
}
all = append(all, elsedie(codegen.ForGen(rt, decl.Some()))(rel))
}
// now raw kinddirs
f = os.DirFS(filepath.Join(groot, kindsys.RawDeclParentPath))
kinddirs = elsedie(fs.ReadDir(f, "."))("error reading raw fs root directory")
for _, ent := range kinddirs {
if !ent.IsDir() {
continue
}
rel := filepath.Join(kindsys.RawDeclParentPath, ent.Name())
decl, err := kindsys.LoadCoreKind[kindsys.RawMeta](rel, rt.Context(), nil)
if err != nil {
die(fmt.Errorf("%s is not a valid kind: %s", rel, errors.Details(err, nil)))
}
if decl.Meta.MachineName != ent.Name() {
die(fmt.Errorf("%s: kind's machine name (%s) must equal parent dir name (%s)", rel, decl.Meta.Name, ent.Name()))
}
dfg, _ := codegen.ForGen(nil, decl.Some())
all = append(all, dfg)
}
sort.Slice(all, func(i, j int) bool {
return nameFor(all[i].Meta) < nameFor(all[j].Meta)
})
jfs, err := coreKindsGen.GenerateFS(all)
if err != nil {
die(fmt.Errorf("core kinddirs codegen failed: %w", err))
}
// for _, f := range jfs.AsFiles() {
// fmt.Println(filepath.Join(groot, f.RelativePath))
// }
if _, set := os.LookupEnv("CODEGEN_VERIFY"); set {
if err = jfs.Verify(context.Background(), groot); err != nil {
die(fmt.Errorf("generated code is out of sync with inputs:\n%s\nrun `make gen-cue` to regenerate", err))
}
} else if err = jfs.Write(context.Background(), groot); err != nil {
die(fmt.Errorf("error while writing generated code to disk:\n%s", err))
}
}
func nameFor(m kindsys.SomeKindMeta) string {
switch x := m.(type) {
case kindsys.RawMeta:
return x.Name
case kindsys.CoreStructuredMeta:
return x.Name
case kindsys.CustomStructuredMeta:
return x.Name
case kindsys.ComposableMeta:
return x.Name
default:
// unreachable so long as all the possibilities in KindMetas have switch branches
panic("unreachable")
}
}
func elsedie[T any](t T, err error) func(msg string) T {
if err != nil {
return func(msg string) T {
fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err)
os.Exit(1)
return t
}
}
return func(msg string) T {
return t
}
}
func die(err error) {
fmt.Fprint(os.Stderr, err, "\n")
os.Exit(1)
}

7
kinds/raw/constraint.cue Normal file
View File

@ -0,0 +1,7 @@
package kind
import "github.com/grafana/grafana/pkg/kindsys"
// In each child directory, the set of .cue files with 'package kind'
// must be an instance of kindsys.#Raw - a declaration of a raw kind.
kindsys.#Raw

View File

@ -0,0 +1,4 @@
package kind
name: "SVG"
extensions: ["svg"]

View File

@ -0,0 +1,8 @@
package kind
import "github.com/grafana/grafana/pkg/kindsys"
// In each child directory, the set of .cue files with 'package kind'
// must be an instance of kindsys.#CoreStructured - a declaration of a
// structured kind.
kindsys.#CoreStructured

View File

@ -1,18 +1,15 @@
package dashboard
package kind
import (
"strings"
import "strings"
"github.com/grafana/thema"
)
name: "Dashboard"
maturity: "merged"
thema.#Lineage
name: "dashboard"
seqs: [
lineage: seqs: [
{
schemas: [
{// 0.0
@grafana(TSVeneer="type")
@grafana(TSVeneer="type")
// Unique numeric identifier for the dashboard.
// TODO must isolate or remove identifiers local to a Grafana instance...?
@ -84,12 +81,13 @@ seqs: [
///////////////////////////////////////
// Definitions (referenced above) are declared below
// TODO docs
#AnnotationTarget: {
limit: int64
limit: int64
matchAny: bool
tags: [...string]
type: string
}
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
// TODO docs
// FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
@ -111,9 +109,9 @@ seqs: [
iconColor?: string @grafanamaturity(NeedsExpertReview)
type: string | *"dashboard" @grafanamaturity(NeedsExpertReview)
// Query for annotation data.
rawQuery?: string @grafanamaturity(NeedsExpertReview)
showIn: uint8 | *0 @grafanamaturity(NeedsExpertReview)
target?: #AnnotationTarget @grafanamaturity(NeedsExpertReview)
rawQuery?: string @grafanamaturity(NeedsExpertReview)
showIn: uint8 | *0 @grafanamaturity(NeedsExpertReview)
target?: #AnnotationTarget @grafanamaturity(NeedsExpertReview)
} @cuetsy(kind="interface")
// FROM: packages/grafana-data/src/types/templateVars.ts
@ -237,7 +235,7 @@ seqs: [
#SpecialValueMap: {
type: #MappingType & "special"
options: {
match: "true" | "false"
match: "true" | "false"
pattern: string
result: #ValueMappingResult
}
@ -376,7 +374,7 @@ seqs: [
// Human readable field metadata
description?: string @grafanamaturity(NeedsExpertReview)
// An explict path to the field in the datasource. When the frame meta includes a path,
// An explicit path to the field in the datasource. When the frame meta includes a path,
// This will default to `${frame.meta.path}/${field.name}
//
// When defined, this value can be used as an identifier within the datasource scope, and

View File

@ -1,12 +1,9 @@
package playlist
package kind
import (
"github.com/grafana/thema"
)
name: "Playlist"
maturity: "merged"
thema.#Lineage
name: "playlist"
seqs: [
lineage: seqs: [
{
schemas: [
{//0.0

View File

@ -1,11 +1,15 @@
// This file is autogenerated. DO NOT EDIT.
// THIS FILE IS GENERATED. EDITING IS FUTILE.
//
// Generated by pkg/framework/coremodel/gen.go
// Generated by:
// kinds/gen.go
// Using jennies:
// TSVeneerIndexJenny
//
// Run `make gen-cue` from repository root to regenerate.
// Run 'make gen-cue' from repository root to regenerate.
// Raw generated types from dashboard entity type.
// Raw generated types from Dashboard kind.
export type {
AnnotationTarget,
AnnotationQuery,
VariableModel,
DashboardLink,
@ -30,10 +34,11 @@ export type {
DashboardCursorSync,
MatcherConfig,
RowPanel
} from './raw/dashboard/x/dashboard.gen';
} from './raw/dashboard/x/dashboard_types.gen';
// Raw generated default consts from dashboard entity type.
// Raw generated default consts from dashboard kind.
export {
defaultAnnotationTarget,
defaultAnnotationQuery,
defaultDashboardLink,
defaultGridPos,
@ -41,10 +46,10 @@ export {
defaultDashboardCursorSync,
defaultMatcherConfig,
defaultRowPanel
} from './raw/dashboard/x/dashboard.gen';
} from './raw/dashboard/x/dashboard_types.gen';
// The following exported declarations correspond to types in the dashboard@0.0 schema with
// attribute @grafana(TSVeneer="type"). (lineage declared in file: pkg/coremodel/dashboard/coremodel.cue)
// The following exported declarations correspond to types in the dashboard@0.0 kind's
// schema with attribute @grafana(TSVeneer="type").
//
// The handwritten file for these type and default veneers is expected to be at
// packages/grafana-schema/src/veneer/dashboard.types.ts.
@ -59,8 +64,8 @@ export type {
FieldConfig
} from './veneer/dashboard.types';
// The following exported declarations correspond to types in the dashboard@0.0 schema with
// attribute @grafana(TSVeneer="type"). (lineage declared in file: pkg/coremodel/dashboard/coremodel.cue)
// The following exported declarations correspond to types in the dashboard@0.0 kind's
// schema with attribute @grafana(TSVeneer="type").
//
// The handwritten file for these type and default veneers is expected to be at
// packages/grafana-schema/src/veneer/dashboard.types.ts.
@ -75,11 +80,11 @@ export {
defaultFieldConfig
} from './veneer/dashboard.types';
// Raw generated types from playlist entity type.
// Raw generated types from Playlist kind.
export type {
Playlist,
PlaylistItem
} from './raw/playlist/x/playlist.gen';
} from './raw/playlist/x/playlist_types.gen';
// Raw generated default consts from playlist entity type.
export { defaultPlaylist } from './raw/playlist/x/playlist.gen';
// Raw generated default consts from playlist kind.
export { defaultPlaylist } from './raw/playlist/x/playlist_types.gen';

View File

@ -1,10 +1,25 @@
// This file is autogenerated. DO NOT EDIT.
// THIS FILE IS GENERATED. EDITING IS FUTILE.
//
// Generated by pkg/framework/coremodel/gen.go
// Generated by:
// kinds/gen.go
// Using jennies:
// TSTypesJenny
//
// Derived from the Thema lineage declared in pkg/coremodel/dashboard/coremodel.cue
//
// Run `make gen-cue` from repository root to regenerate.
// Run 'make gen-cue' from repository root to regenerate.
/**
* TODO docs
*/
export interface AnnotationTarget {
limit: number;
matchAny: boolean;
tags: Array<string>;
type: string;
}
export const defaultAnnotationTarget: Partial<AnnotationTarget> = {
tags: [],
};
/**
* TODO docs
@ -40,12 +55,7 @@ export interface AnnotationQuery {
*/
rawQuery?: string;
showIn: number;
target?: {
limit: number;
matchAny: boolean;
tags: Array<string>;
type: string;
};
target?: AnnotationTarget;
type: string;
}
@ -493,7 +503,7 @@ export interface FieldConfig {
*/
noValue?: string;
/**
* An explict path to the field in the datasource. When the frame meta includes a path,
* An explicit path to the field in the datasource. When the frame meta includes a path,
* This will default to `${frame.meta.path}/${field.name}
*
* When defined, this value can be used as an identifier within the datasource scope, and

View File

@ -1,10 +1,11 @@
// This file is autogenerated. DO NOT EDIT.
// THIS FILE IS GENERATED. EDITING IS FUTILE.
//
// Generated by pkg/framework/coremodel/gen.go
// Generated by:
// kinds/gen.go
// Using jennies:
// TSTypesJenny
//
// Derived from the Thema lineage declared in pkg/coremodel/playlist/coremodel.cue
//
// Run `make gen-cue` from repository root to regenerate.
// Run 'make gen-cue' from repository root to regenerate.
export interface PlaylistItem {
/**

View File

@ -1,4 +1,4 @@
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
// This file is autogenerated. DO NOT EDIT.
//
// To regenerate, run "make gen-cue" from the repository root.

View File

@ -1,4 +1,4 @@
import * as raw from '../raw/dashboard/x/dashboard.gen';
import * as raw from '../raw/dashboard/x/dashboard_types.gen';
export interface Dashboard extends raw.Dashboard {
panels?: Array<

View File

@ -16,9 +16,8 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/dashdiffs"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/coremodel/dashboard"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/kinds/dashboard"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/alerting"
@ -365,21 +364,22 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext) response.Response {
}
if hs.Features.IsEnabled(featuremgmt.FlagValidateDashboardsOnSave) {
cm := hs.Coremodels.Dashboard()
kind := hs.Kinds.Dashboard()
dashbytes, err := cmd.Dashboard.Bytes()
if err != nil {
return response.Error(http.StatusBadRequest, "unable to parse dashboard", err)
}
// Ideally, coremodel validation calls would be integrated into the web
// framework. But this does the job for now.
schv, err := cmd.Dashboard.Get("schemaVersion").Int()
// Only try to validate if the schemaVersion is at least the handoff version
// (the minimum schemaVersion against which the dashboard schema is known to
// work), or if schemaVersion is absent (which will happen once the Thema
// schema becomes canonical).
// work), or if schemaVersion is absent (which will happen once the kind schema
// becomes canonical).
if err != nil || schv >= dashboard.HandoffSchemaVersion {
// Can't fail, web.Bind() already ensured it's valid JSON
b, _ := cmd.Dashboard.Bytes()
v, _ := cuectx.JSONtoCUE("dashboard.json", b)
if _, err := cm.CurrentSchema().Validate(v); err != nil {
if _, _, err := kind.JSONValueMux(dashbytes); err != nil {
return response.Error(http.StatusBadRequest, "invalid dashboard json", err)
}
}
@ -772,7 +772,7 @@ func (hs *HTTPServer) ValidateDashboard(c *models.ReqContext) response.Response
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cm := hs.Coremodels.Dashboard()
dk := hs.Kinds.Dashboard()
dashboardBytes := []byte(cmd.Dashboard)
// POST api receives dashboard as a string of json (so line numbers for errors stay consistent),
@ -793,8 +793,7 @@ func (hs *HTTPServer) ValidateDashboard(c *models.ReqContext) response.Response
// work), or if schemaVersion is absent (which will happen once the Thema
// schema becomes canonical).
if err != nil || schemaVersion >= dashboard.HandoffSchemaVersion {
v, _ := cuectx.JSONtoCUE("dashboard.json", dashboardBytes)
_, validationErr := cm.CurrentSchema().Validate(v)
_, _, validationErr := dk.JSONValueMux(dashboardBytes)
if validationErr == nil {
isValid = true

View File

@ -17,11 +17,11 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/framework/coremodel/registry"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry/corekind"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
@ -65,7 +65,7 @@ func TestGetHomeDashboard(t *testing.T) {
SQLStore: mockstore.NewSQLStoreMock(),
preferenceService: prefService,
dashboardVersionService: dashboardVersionService,
Coremodels: registry.NewBase(nil),
Kinds: corekind.NewBase(nil),
}
tests := []struct {
@ -149,7 +149,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
Features: featuremgmt.WithFeatures(),
DashboardService: dashboardService,
dashboardVersionService: fakeDashboardVersionService,
Coremodels: registry.NewBase(nil),
Kinds: corekind.NewBase(nil),
}
setUp := func() {
@ -271,7 +271,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
DashboardService: dashboardService,
dashboardVersionService: fakeDashboardVersionService,
Features: featuremgmt.WithFeatures(),
Coremodels: registry.NewBase(nil),
Kinds: corekind.NewBase(nil),
}
setUp := func() {
@ -968,7 +968,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
AccessControl: accesscontrolmock.New(),
DashboardService: dashboardService,
Features: featuremgmt.WithFeatures(),
Coremodels: registry.NewBase(nil),
Kinds: corekind.NewBase(nil),
}
hs.callGetDashboard(sc)
@ -1023,7 +1023,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
),
DashboardService: dashboardService,
Features: featuremgmt.WithFeatures(),
Coremodels: registry.NewBase(nil),
Kinds: corekind.NewBase(nil),
}
hs.callGetDashboard(sc)
@ -1089,7 +1089,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
DashboardService: dashboardService,
folderService: folderService,
Features: featuremgmt.WithFeatures(),
Coremodels: registry.NewBase(nil),
Kinds: corekind.NewBase(nil),
}
sc := setupScenarioContext(t, url)
@ -1121,7 +1121,7 @@ func postValidateScenario(t *testing.T, desc string, url string, routePattern st
LibraryElementService: &mockLibraryElementService{},
SQLStore: sqlmock,
Features: featuremgmt.WithFeatures(),
Coremodels: registry.NewBase(nil),
Kinds: corekind.NewBase(nil),
}
sc := setupScenarioContext(t, url)
@ -1158,7 +1158,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string
SQLStore: sqlmock,
dashboardVersionService: fakeDashboardVersionService,
Features: featuremgmt.WithFeatures(),
Coremodels: registry.NewBase(nil),
Kinds: corekind.NewBase(nil),
}
sc := setupScenarioContext(t, url)
@ -1197,7 +1197,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
SQLStore: sqlStore,
Features: featuremgmt.WithFeatures(),
dashboardVersionService: fakeDashboardVersionService,
Coremodels: registry.NewBase(nil),
Kinds: corekind.NewBase(nil),
}
sc := setupScenarioContext(t, url)

View File

@ -29,7 +29,6 @@ import (
"github.com/grafana/grafana/pkg/api/routing"
httpstatic "github.com/grafana/grafana/pkg/api/static"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/framework/coremodel/registry"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
@ -41,6 +40,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/plugincontext"
"github.com/grafana/grafana/pkg/registry/corekind"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/annotations"
@ -192,7 +192,7 @@ type HTTPServer struct {
dashboardVersionService dashver.Service
PublicDashboardsApi *publicdashboardsApi.Api
starService star.Service
Coremodels *registry.Base
Kinds *corekind.Base
playlistService playlist.Service
apiKeyService apikey.Service
kvStore kvstore.KVStore
@ -241,7 +241,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service,
teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
starService star.Service, csrfService csrf.Service, coremodels *registry.Base,
starService star.Service, csrfService csrf.Service, basekinds *corekind.Base,
playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore,
secretsMigrator secrets.Migrator, secretsPluginManager plugins.SecretsPluginManager, secretsService secrets.Service,
secretsPluginMigrator spm.SecretMigrationProvider, secretsStore secretsKV.SecretsKVStore,
@ -337,7 +337,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
dashboardPermissionsService: dashboardPermissionsService,
dashboardVersionService: dashboardVersionService,
starService: starService,
Coremodels: coremodels,
Kinds: basekinds,
playlistService: playlistService,
apiKeyService: apiKeyService,
kvStore: kvStore,

View File

@ -8,7 +8,7 @@ import (
"github.com/google/wire"
"github.com/grafana/grafana/pkg/tsdb/parca"
phlare "github.com/grafana/grafana/pkg/tsdb/phlare"
"github.com/grafana/grafana/pkg/tsdb/phlare"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/api"
@ -17,7 +17,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/expr"
cmreg "github.com/grafana/grafana/pkg/framework/coremodel/registry"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
"github.com/grafana/grafana/pkg/infra/httpclient"
@ -47,6 +46,7 @@ import (
managerStore "github.com/grafana/grafana/pkg/plugins/manager/store"
"github.com/grafana/grafana/pkg/plugins/plugincontext"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/registry/corekind"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
@ -305,7 +305,7 @@ var wireSet = wire.NewSet(
avatar.ProvideAvatarCacheServer,
authproxy.ProvideAuthProxy,
statscollector.ProvideService,
cmreg.CoremodelSet,
corekind.KindSet,
cuectx.GrafanaCUEContext,
cuectx.GrafanaThemaRuntime,
csrf.ProvideCSRFFilter,

View File

@ -29,7 +29,7 @@ type FooThing struct {
}`,
out: `package foo
type Model struct {
type Foo struct {
Id int64
Ref Thing
}
@ -52,7 +52,7 @@ type FooThing struct {
}`,
out: `package foo
type Model struct {
type Foo struct {
Id int64
Ref *Thing
}
@ -77,7 +77,7 @@ type FooThing struct {
}`,
out: `package foo
type Model struct {
type Foo struct {
Id int64
Ref []Thing
PRef []*Thing
@ -104,7 +104,7 @@ type FooThing struct {
}`,
out: `package foo
type Model struct {
type Foo struct {
Id int64
KeyRef map[Thing]string
ValRef map[string]Thing
@ -132,7 +132,7 @@ type FooThing struct {
}`,
out: `package foo
type Model struct {
type Foo struct {
Id int64
KeyRef map[*Thing]string
ValRef map[string]*Thing
@ -154,7 +154,7 @@ type Foo struct {
}`,
out: `package foo
type Model struct {
type Foo struct {
Id int64
FooRef []string
}
@ -235,6 +235,37 @@ type Thing string
// of objects, only types, so we shouldn't encounter this case.
skip: true,
},
"comments": {
in: `package foo
// Foo is a thing. It should be Foo still.
type Foo struct {
Id int64
Ref FooThing
}
// FooThing is also a thing. We want [FooThing] to be known properly.
// Even if FooThing
// were not a FooThing, in our minds, forever shall it be FooThing.
type FooThing struct {
Id int64
}`,
out: `package foo
// Foo is a thing. It should be Foo still.
type Foo struct {
Id int64
Ref Thing
}
// Thing is also a thing. We want [Thing] to be known properly.
// Even if Thing
// were not a Thing, in our minds, forever shall it be Thing.
type Thing struct {
Id int64
}
`,
},
}
for name, it := range tt {
@ -250,7 +281,7 @@ type Thing string
t.Fatal(err)
}
drop := makePrefixDropper("Foo", "Model")
drop := PrefixDropper("Foo")
astutil.Apply(inf, drop, nil)
buf := new(bytes.Buffer)
err = format.Node(buf, fset, inf)

View File

@ -4,11 +4,9 @@ import (
"bytes"
"errors"
"fmt"
"go/ast"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"testing/fstest"
@ -21,7 +19,6 @@ import (
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/thema"
"github.com/grafana/thema/encoding/openapi"
"golang.org/x/tools/go/ast/astutil"
)
// CoremodelDeclaration contains the results of statically analyzing a Grafana
@ -218,7 +215,7 @@ func (cd *CoremodelDeclaration) GenerateGoCoremodel(path string) (WriteDiffer, e
fullp := filepath.Join(path, fmt.Sprintf("%s_gen.go", lin.Name()))
byt, err := postprocessGoFile(genGoFile{
path: fullp,
walker: makePrefixDropper(strings.Title(lin.Name()), "Model"),
walker: PrefixDropper(strings.Title(lin.Name())),
in: buf.Bytes(),
})
if err != nil {
@ -273,117 +270,6 @@ func (cd *CoremodelDeclaration) GenerateTypescriptCoremodel() (*tsast.File, erro
return tf, nil
}
type prefixDropper struct {
str string
base string
rxp *regexp.Regexp
rxpsuff *regexp.Regexp
}
func makePrefixDropper(str, base string) astutil.ApplyFunc {
return (&prefixDropper{
str: str,
base: base,
rxpsuff: regexp.MustCompile(fmt.Sprintf(`%s([a-zA-Z_]*)`, str)),
rxp: regexp.MustCompile(fmt.Sprintf(`%s([\s.,;-])`, str)),
}).applyfunc
}
func depoint(e ast.Expr) ast.Expr {
if star, is := e.(*ast.StarExpr); is {
return star.X
}
return e
}
func (d prefixDropper) applyfunc(c *astutil.Cursor) bool {
n := c.Node()
// fmt.Printf("%T %s\n", c.Node(), ast.Print(nil, c.Node()))
switch x := n.(type) {
case *ast.ValueSpec:
// fmt.Printf("%T %s\n", c.Node(), ast.Print(nil, c.Node()))
d.handleExpr(x.Type)
for _, id := range x.Names {
d.do(id)
}
case *ast.TypeSpec:
// Always do typespecs
d.do(x.Name)
case *ast.Field:
// Don't rename struct fields. We just want to rename type declarations, and
// field value specifications that reference those types.
d.handleExpr(x.Type)
// return false
case *ast.CommentGroup:
for _, c := range x.List {
c.Text = d.rxp.ReplaceAllString(c.Text, d.base+"$1")
c.Text = d.rxpsuff.ReplaceAllString(c.Text, "$1")
}
}
return true
}
func (d prefixDropper) handleExpr(e ast.Expr) {
// Deref a StarExpr, if there is one
expr := depoint(e)
switch x := expr.(type) {
case *ast.Ident:
d.do(x)
case *ast.ArrayType:
if id, is := depoint(x.Elt).(*ast.Ident); is {
d.do(id)
}
case *ast.MapType:
if id, is := depoint(x.Key).(*ast.Ident); is {
d.do(id)
}
if id, is := depoint(x.Value).(*ast.Ident); is {
d.do(id)
}
}
}
func (d prefixDropper) do(n *ast.Ident) {
if n.Name != d.str {
n.Name = strings.TrimPrefix(n.Name, d.str)
} else {
n.Name = d.base
}
}
// 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 []*CoremodelDeclaration) (WriteDiffer, error) {
var cml []tplVars
for _, ec := range ecl {
cml = append(cml, ec.toTemplateObj())
}
buf := new(bytes.Buffer)
if err := tmpls.Lookup("coremodel_registry.tmpl").Execute(buf, tvars_coremodel_registry{
Header: tvars_autogen_header{
GeneratorPath: "pkg/framework/coremodel/gen.go", // FIXME hardcoding is not OK
},
Coremodels: cml,
}); err != nil {
return nil, fmt.Errorf("failed executing coremodel registry template: %w", err)
}
byt, err := postprocessGoFile(genGoFile{
path: path,
in: buf.Bytes(),
})
if err != nil {
return nil, err
}
wd := NewWriteDiffer()
wd[path] = byt
return wd, nil
}
var tmplTypedef = `{{range .Types}}
{{ with .Schema.Description }}{{ . }}{{ else }}// {{.TypeName}} is the Go representation of a {{.JsonName}}.{{ end }}
//

61
pkg/codegen/generators.go Normal file
View File

@ -0,0 +1,61 @@
package codegen
import (
"bytes"
"fmt"
"github.com/grafana/codejen"
"github.com/grafana/grafana/pkg/kindsys"
"github.com/grafana/thema"
)
type OneToOne codejen.OneToOne[*DeclForGen]
type OneToMany codejen.OneToMany[*DeclForGen]
type ManyToOne codejen.ManyToOne[*DeclForGen]
type ManyToMany codejen.ManyToMany[*DeclForGen]
// ForGen is a codejen input transformer that converts a pure kindsys.SomeDecl into
// a DeclForGen by binding its contained lineage.
func ForGen(rt *thema.Runtime, decl *kindsys.SomeDecl) (*DeclForGen, error) {
lin, err := decl.BindKindLineage(rt)
if err != nil {
return nil, err
}
return &DeclForGen{
SomeDecl: decl,
lin: lin,
}, nil
}
// DeclForGen wraps [kindsys.SomeDecl] to provide trivial caching of
// the lineage declared by the kind (nil for raw kinds).
type DeclForGen struct {
*kindsys.SomeDecl
lin thema.Lineage
}
func (decl *DeclForGen) Lineage() thema.Lineage {
return decl.lin
}
func SlashHeaderMapper(maingen string) codejen.FileMapper {
return func(f codejen.File) (codejen.File, error) {
b := new(bytes.Buffer)
fmt.Fprintf(b, headerTmpl, maingen, f.FromString())
fmt.Fprint(b, string(f.Data))
f.Data = b.Bytes()
return f, nil
}
}
var headerTmpl = `// THIS FILE IS GENERATED. EDITING IS FUTILE.
//
// Generated by:
// %s
// Using jennies:
// %s
//
// Run 'make gen-cue' from repository root to regenerate.
`

View File

@ -0,0 +1,62 @@
package codegen
import (
"bytes"
"fmt"
"path/filepath"
"github.com/grafana/codejen"
)
// BaseCoreRegistryJenny generates a static registry for core kinds that
// only initializes their [kindsys.Interface]. No slot kinds are composed.
//
// Path should be the relative path to the directory that will contain the
// generated registry. kindrelroot should be the repo-root-relative path to the
// parent directory to all directories that contain generated kind bindings
// (e.g. pkg/kind).
func BaseCoreRegistryJenny(path, kindrelroot string) ManyToOne {
return &genBaseRegistry{
path: path,
kindrelroot: kindrelroot,
}
}
type genBaseRegistry struct {
path string
kindrelroot string
}
func (gen *genBaseRegistry) JennyName() string {
return "BaseCoreRegistryJenny"
}
func (gen *genBaseRegistry) Generate(decls []*DeclForGen) (*codejen.File, error) {
var numRaw int
for _, k := range decls {
if k.IsRaw() {
numRaw++
}
}
buf := new(bytes.Buffer)
if err := tmpls.Lookup("kind_registry.tmpl").Execute(buf, tvars_kind_registry{
NumRaw: numRaw,
NumStructured: len(decls) - numRaw,
PackageName: filepath.Base(gen.path),
KindPackagePrefix: filepath.ToSlash(filepath.Join("github.com/grafana/grafana", gen.kindrelroot)),
Kinds: decls,
}); err != nil {
return nil, fmt.Errorf("failed executing kind registry template: %w", err)
}
b, err := postprocessGoFile(genGoFile{
path: gen.path,
in: buf.Bytes(),
})
if err != nil {
return nil, err
}
return codejen.NewFile(filepath.Join(gen.path, "base_gen.go"), b, gen), nil
}

View File

@ -0,0 +1,71 @@
package codegen
import (
"bytes"
"fmt"
"path/filepath"
"github.com/grafana/codejen"
)
// CoreStructuredKindJenny generates the implementation of
// [kindsys.Structured] for the provided kind declaration.
//
// gokindsdir should be the relative path to the parent directory that contains
// all generated kinds.
//
// This generator only has output for core structured kinds.
func CoreStructuredKindJenny(gokindsdir string, cfg *CoreStructuredKindGeneratorConfig) OneToOne {
if cfg == nil {
cfg = new(CoreStructuredKindGeneratorConfig)
}
if cfg.GenDirName == nil {
cfg.GenDirName = func(decl *DeclForGen) string {
return decl.Meta.Common().MachineName
}
}
return &genCoreStructuredKind{
gokindsdir: gokindsdir,
cfg: cfg,
}
}
// CoreStructuredKindGeneratorConfig holds configuration options for [CoreStructuredKindJenny].
type CoreStructuredKindGeneratorConfig struct {
// GenDirName returns the name of the directory in which the file should be
// generated. Defaults to DeclForGen.Lineage().Name() if nil.
GenDirName func(*DeclForGen) string
}
type genCoreStructuredKind struct {
gokindsdir string
cfg *CoreStructuredKindGeneratorConfig
}
var _ OneToOne = &genCoreStructuredKind{}
func (gen *genCoreStructuredKind) JennyName() string {
return "CoreStructuredKindJenny"
}
func (gen *genCoreStructuredKind) Generate(decl *DeclForGen) (*codejen.File, error) {
if !decl.IsCoreStructured() {
return nil, nil
}
path := filepath.Join(gen.gokindsdir, gen.cfg.GenDirName(decl), decl.Meta.Common().MachineName+"_kind_gen.go")
buf := new(bytes.Buffer)
if err := tmpls.Lookup("kind_corestructured.tmpl").Execute(buf, decl); err != nil {
return nil, fmt.Errorf("failed executing kind_corestructured template for %s: %w", path, err)
}
b, err := postprocessGoFile(genGoFile{
path: path,
in: buf.Bytes(),
})
if err != nil {
return nil, err
}
return codejen.NewFile(path, b, gen), nil
}

View File

@ -0,0 +1,98 @@
package codegen
import (
"fmt"
"path/filepath"
"github.com/grafana/codejen"
"github.com/grafana/thema"
"github.com/grafana/thema/encoding/gocode"
"golang.org/x/tools/go/ast/astutil"
)
// GoTypesJenny creates a [OneToOne] that produces Go types for the latest
// Thema schema in a structured kind's lineage.
//
// At minimum, a gokindsdir must be provided. This should be the path to the parent
// directory of the directory in which the types should be generated, relative
// to the project root. For example, if the types for a kind named "foo"
// should live at pkg/kind/foo/foo_gen.go, relpath should be "pkg/kind".
//
// This generator is a no-op for raw kinds.
func GoTypesJenny(gokindsdir string, cfg *GoTypesGeneratorConfig) OneToOne {
if cfg == nil {
cfg = new(GoTypesGeneratorConfig)
}
if cfg.GenDirName == nil {
cfg.GenDirName = func(decl *DeclForGen) string {
return decl.Meta.Common().MachineName
}
}
return &genGoTypes{
gokindsdir: gokindsdir,
cfg: cfg,
}
}
// GoTypesGeneratorConfig holds configuration options for [GoTypesJenny].
type GoTypesGeneratorConfig struct {
// Apply is an optional AST manipulation func that, if provided, will be run
// against the generated Go file prior to running it through goimports.
Apply astutil.ApplyFunc
// GenDirName returns the name of the parent directory in which the type file
// should be generated. If nil, the DeclForGen.Lineage().Name() will be used.
GenDirName func(*DeclForGen) string
// Version of the schema to generate. If nil, latest is generated.
Version *thema.SyntacticVersion
}
type genGoTypes struct {
gokindsdir string
cfg *GoTypesGeneratorConfig
}
func (gen *genGoTypes) JennyName() string {
return "GoTypesJenny"
}
func (gen *genGoTypes) Generate(decl *DeclForGen) (*codejen.File, error) {
if decl.IsRaw() {
return nil, nil
}
var sch thema.Schema
var err error
lin := decl.Lineage()
if gen.cfg.Version == nil {
sch = lin.Latest()
} else {
sch, err = lin.Schema(*gen.cfg.Version)
if err != nil {
return nil, fmt.Errorf("error in configured version for %s generator: %w", *gen.cfg.Version, err)
}
}
// always drop prefixes.
var appf []astutil.ApplyFunc
if gen.cfg.Apply != nil {
appf = append(appf, gen.cfg.Apply)
}
appf = append(appf, PrefixDropper(decl.Meta.Common().Name))
pdir := gen.cfg.GenDirName(decl)
fpath := filepath.Join(gen.gokindsdir, pdir, lin.Name()+"_types_gen.go")
// TODO allow using name instead of machine name in thema generator
b, err := gocode.GenerateTypesOpenAPI(sch, &gocode.TypeConfigOpenAPI{
PackageName: filepath.Base(pdir),
ApplyFuncs: appf,
})
if err != nil {
return nil, err
}
return codejen.NewFile(fpath, b, gen), nil
}

View File

@ -0,0 +1,68 @@
package codegen
import (
"bytes"
"fmt"
"path/filepath"
"github.com/grafana/codejen"
)
// RawKindJenny generates the implementation of [kindsys.Raw] for the
// provided kind declaration.
//
// gokindsdir should be the relative path to the parent directory that contains
// all generated kinds.
//
// This generator only has output for raw kinds.
func RawKindJenny(gokindsdir string, cfg *RawKindGeneratorConfig) OneToOne {
if cfg == nil {
cfg = new(RawKindGeneratorConfig)
}
if cfg.GenDirName == nil {
cfg.GenDirName = func(decl *DeclForGen) string {
return decl.Meta.Common().MachineName
}
}
return &genRawKind{
gokindsdir: gokindsdir,
cfg: cfg,
}
}
type genRawKind struct {
gokindsdir string
cfg *RawKindGeneratorConfig
}
type RawKindGeneratorConfig struct {
// GenDirName returns the name of the directory in which the file should be
// generated. Defaults to DeclForGen.Lineage().Name() if nil.
GenDirName func(*DeclForGen) string
}
func (gen *genRawKind) JennyName() string {
return "RawKindJenny"
}
func (gen *genRawKind) Generate(decl *DeclForGen) (*codejen.File, error) {
if !decl.IsRaw() {
return nil, nil
}
path := filepath.Join(gen.gokindsdir, gen.cfg.GenDirName(decl), decl.Meta.Common().MachineName+"_kind_gen.go")
buf := new(bytes.Buffer)
if err := tmpls.Lookup("kind_raw.tmpl").Execute(buf, decl); err != nil {
return nil, fmt.Errorf("failed executing kind_raw template for %s: %w", path, err)
}
b, err := postprocessGoFile(genGoFile{
path: path,
in: buf.Bytes(),
})
if err != nil {
return nil, err
}
return codejen.NewFile(path, b, gen), nil
}

View File

@ -0,0 +1,85 @@
package codegen
import (
"fmt"
"path/filepath"
"github.com/grafana/codejen"
"github.com/grafana/thema"
"github.com/grafana/thema/encoding/typescript"
)
// TSTypesJenny creates a [OneToOne] that produces TypeScript types and
// defaults for the latest Thema schema in a structured kind's lineage.
//
// At minimum, a tskindsdir must be provided. This should be the path to the parent
// directory of the directory in which the types should be generated, relative
// to the project root. For example, if the types for a kind named "foo"
// should live at packages/grafana-schema/src/raw/foo, relpath should be "pkg/kind".
//
// This generator is a no-op for raw kinds.
func TSTypesJenny(tskindsdir string, cfg *TSTypesGeneratorConfig) OneToOne {
if cfg == nil {
cfg = new(TSTypesGeneratorConfig)
}
if cfg.GenDirName == nil {
cfg.GenDirName = func(decl *DeclForGen) string {
return decl.Meta.Common().MachineName
}
}
return &genTSTypes{
tskindsdir: tskindsdir,
cfg: cfg,
}
}
// TSTypesGeneratorConfig holds configuration options for [TSTypesJenny].
type TSTypesGeneratorConfig struct {
// GenDirName returns the name of the parent directory in which the type file
// should be generated. If nil, the DeclForGen.Lineage().Name() will be used.
GenDirName func(*DeclForGen) string
// Version of the schema to generate. If nil, latest is generated.
Version *thema.SyntacticVersion
}
type genTSTypes struct {
tskindsdir string
cfg *TSTypesGeneratorConfig
}
func (gen *genTSTypes) JennyName() string {
return "TSTypesJenny"
}
func (gen *genTSTypes) Generate(decl *DeclForGen) (*codejen.File, error) {
if decl.IsRaw() {
return nil, nil
}
var sch thema.Schema
var err error
lin := decl.Lineage()
if gen.cfg.Version == nil {
sch = lin.Latest()
} else {
sch, err = lin.Schema(*gen.cfg.Version)
if err != nil {
return nil, fmt.Errorf("error in configured version for %s generator: %w", *gen.cfg.Version, err)
}
}
// TODO allow using name instead of machine name in thema generator
f, err := typescript.GenerateTypes(sch, &typescript.TypeConfig{
RootName: decl.Meta.Common().Name,
Group: decl.Meta.Common().LineageIsGroup,
})
if err != nil {
return nil, err
}
return codejen.NewFile(
filepath.Join(gen.tskindsdir, gen.cfg.GenDirName(decl), lin.Name()+"_types.gen.ts"),
[]byte(f.String()),
gen), nil
}

View File

@ -0,0 +1,325 @@
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/ts"
"github.com/grafana/cuetsy/ts/ast"
"github.com/grafana/grafana/pkg/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(decls []*DeclForGen) (*codejen.File, error) {
tsf := new(ast.File)
for _, decl := range decls {
if decl.IsRaw() {
continue
}
sch := decl.Lineage().Latest()
f, err := typescript.GenerateTypes(sch, &typescript.TypeConfig{
RootName: decl.Meta.Common().Name,
Group: decl.Meta.Common().LineageIsGroup,
})
if err != nil {
return nil, fmt.Errorf("%s: %w", decl.Meta.Common().Name, err)
}
elems, err := gen.extractTSIndexVeneerElements(decl, f)
if err != nil {
return nil, fmt.Errorf("%s: %w", decl.Meta.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(decl *DeclForGen, tf *ast.File) ([]ast.Decl, error) {
lin := decl.Lineage()
sch := thema.SchemaP(lin, thema.LatestVersion(lin))
comm := decl.Meta.Common()
// Check the root, then walk the tree
rootv := sch.UnwrapCUE()
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:
name = strings.Title(lin.Name())
fallthrough
case 1:
// Only deal with subpaths that are definitions, for now
// TODO incorporate smarts about grouped lineages here
if name == "" {
if !sels[0].IsDefinition() {
return false
}
// It might seem to make sense that we'd strip replaceout the leading # here for
// definitions. However, cuetsy's tsast actually has the # still present in its
// Ident types, stripping it replaceout on the fly when stringifying.
name = sels[0].String()
}
// Search the generated TS AST for the type and default decl nodes
pair := findDeclNode(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 {
custom = append(custom, *pair.T)
if pair.D != nil {
customD = append(customD, *pair.D)
}
} 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.LatestVersion(lin)[0])
if decl.Meta.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 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.LatestVersion(lin), 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 decl 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
}
type tsVeneerAttr struct {
target string
}
func findDeclNode(name string, tf *ast.File) declPair {
var p declPair
for _, decl := range tf.Nodes {
// Peer through export keywords
if ex, is := decl.(ast.ExportKeyword); is {
decl = ex.Decl
}
switch x := decl.(type) {
case ast.TypeDecl:
if x.Name.Name == name {
p.T = &x.Name
}
case ast.VarDecl:
if x.Names.Idents[0].Name == "default"+name {
p.D = &x.Names.Idents[0]
}
}
}
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 {
var list []string
for tgt := range allowedTSVeneers {
list = append(list, tgt)
}
sort.Strings(list)
return strings.Join(list, "|")
}
func valError(v cue.Value, format string, args ...interface{}) error {
s := v.Source()
if s == nil {
return fmt.Errorf(format, args...)
}
return errors.Newf(s.Pos(), format, args...)
}

View File

@ -209,7 +209,7 @@ func (pt *PluginTree) GenerateGo(path string, cfg GoGenConfig) (WriteDiffer, err
for subpath, plug := range all {
fullp := filepath.Join(path, subpath)
if cfg.Types {
gwd, err := genGoTypes(plug, path, subpath, cfg.DocPathPrefix)
gwd, err := pgenGoTypes(plug, path, subpath, cfg.DocPathPrefix)
if err != nil {
return nil, fmt.Errorf("error generating go types for %s: %w", fullp, err)
}
@ -218,7 +218,7 @@ func (pt *PluginTree) GenerateGo(path string, cfg GoGenConfig) (WriteDiffer, err
}
}
if cfg.ThemaBindings {
twd, err := genThemaBindings(plug, path, subpath, cfg.DocPathPrefix)
twd, err := pgenThemaBindings(plug, path, subpath, cfg.DocPathPrefix)
if err != nil {
return nil, fmt.Errorf("error generating thema bindings for %s: %w", fullp, err)
}
@ -231,7 +231,7 @@ func (pt *PluginTree) GenerateGo(path string, cfg GoGenConfig) (WriteDiffer, err
return wd, nil
}
func genGoTypes(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer, error) {
func pgenGoTypes(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer, error) {
wd := NewWriteDiffer()
for slotname, lin := range plug.SlotImplementations() {
lowslot := strings.ToLower(slotname)
@ -287,7 +287,7 @@ func genGoTypes(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer,
finalpath := filepath.Join(path, subpath, fmt.Sprintf("types_%s_gen.go", lowslot))
byt, err := postprocessGoFile(genGoFile{
path: finalpath,
walker: makePrefixDropper(strings.Title(lin.Name()), slotname),
walker: PrefixDropper(strings.Title(lin.Name())),
in: buf.Bytes(),
})
if err != nil {
@ -300,7 +300,7 @@ func genGoTypes(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer,
return wd, nil
}
func genThemaBindings(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer, error) {
func pgenThemaBindings(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer, error) {
wd := NewWriteDiffer()
bindings := make([]tvars_plugin_lineage_binding, 0)
for slotname, lin := range plug.SlotImplementations() {

View File

@ -29,9 +29,12 @@ type (
LineageCUEPath string
GenLicense bool
}
tvars_coremodel_registry struct {
Header tvars_autogen_header
Coremodels []tplVars
tvars_kind_registry struct {
// Header tvars_autogen_header
NumRaw, NumStructured int
PackageName string
KindPackagePrefix string
Kinds []*DeclForGen
}
tvars_coremodel_imports struct {
PackageName string

View File

@ -1,58 +0,0 @@
{{ template "autogen_header.tmpl" .Header }}
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 (b *Base) {{ .TitleName }}() *{{ .Name }}.Coremodel {
return b.{{ .Name }}
}
{{end}}
func doProvideBase(rt *thema.Runtime) *Base {
var err error
reg := &Base{}
{{range .Coremodels }}
reg.{{ .Name }}, err = {{ .Name }}.New(rt)
if err != nil {
panic(fmt.Sprintf("error while initializing {{ .Name }} coremodel: %s", err))
}
reg.all = append(reg.all, reg.{{ .Name }})
{{end}}
return reg
}

View File

@ -0,0 +1,95 @@
package {{ .Meta.MachineName }}
import (
"github.com/grafana/grafana/pkg/kindsys"
"github.com/grafana/thema"
"github.com/grafana/thema/vmux"
)
// rootrel is the relative path from the grafana repository root to the
// directory containing the .cue files in which this kind is declared. Necessary
// for runtime errors related to the declaration and/or lineage to provide
// a real path to the correct .cue file.
const rootrel string = "kinds/structured/{{ .Meta.MachineName }}"
// TODO standard generated docs
type Kind struct {
lin thema.ConvergentLineage[*{{ .Meta.Name }}]
jendec vmux.Endec
valmux vmux.ValueMux[*{{ .Meta.Name }}]
decl kindsys.Decl[kindsys.CoreStructuredMeta]
}
// type guard
var _ kindsys.Structured = &Kind{}
// TODO standard generated docs
func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil)
if err != nil {
return nil, err
}
k := &Kind{
decl: *decl,
}
lin, err := decl.Some().BindKindLineage(rt, opts...)
if err != nil {
return nil, err
}
// Get the thema.Schema that the meta says is in the current version (which
// codegen ensures is always the latest)
cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion)
tsch, err := thema.BindType[*{{ .Meta.Name }}](cursch, &{{ .Meta.Name }}{})
if err != nil {
// Should be unreachable, modulo bugs in the Thema->Go code generator
return nil, err
}
k.jendec = vmux.NewJSONEndec("{{ .Meta.MachineName }}.json")
k.lin = tsch.ConvergentLineage()
k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jendec)
return k, nil
}
// TODO standard generated docs
func (k *Kind) Name() string {
return "{{ .Meta.MachineName }}"
}
// TODO standard generated docs
func (k *Kind) MachineName() string {
return "{{ .Meta.MachineName }}"
}
// TODO standard generated docs
func (k *Kind) Lineage() thema.Lineage {
return k.lin
}
// TODO standard generated docs
func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*{{ .Meta.Name }}] {
return k.lin
}
// JSONValueMux is a version multiplexer that maps a []byte containing JSON data
// at any schematized dashboard version to an instance of {{ .Meta.Name }}.
//
// 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) (*{{ .Meta.Name }}, thema.TranslationLacunas, error) {
return k.valmux(b)
}
// TODO standard generated docs
func (k *Kind) Maturity() kindsys.Maturity {
return k.decl.Meta.Maturity
}
// TODO standard generated docs
func (k *Kind) Meta() kindsys.CoreStructuredMeta {
return k.decl.Meta
}

View File

@ -0,0 +1,47 @@
package {{ .Meta.MachineName }}
import (
"github.com/grafana/grafana/pkg/kindsys"
"github.com/grafana/thema"
"github.com/grafana/thema/vmux"
)
// TODO standard generated docs
type Kind struct {
decl kindsys.Decl[kindsys.RawMeta]
}
// type guard
var _ kindsys.Raw = &Kind{}
// TODO standard generated docs
func NewKind() (*Kind, error) {
decl, err := kindsys.LoadCoreKind[kindsys.RawMeta]("kinds/raw/{{ .Meta.MachineName }}", nil, nil)
if err != nil {
return nil, err
}
return &Kind{
decl: *decl,
}, nil
}
// TODO standard generated docs
func (k *Kind) Name() string {
return "{{ .Meta.Name }}"
}
// TODO standard generated docs
func (k *Kind) MachineName() string {
return "{{ .Meta.MachineName }}"
}
// TODO standard generated docs
func (k *Kind) Maturity() kindsys.Maturity {
return k.decl.Meta.Maturity
}
// TODO standard generated docs
func (k *Kind) Meta() kindsys.RawMeta {
return k.decl.Meta
}

View File

@ -0,0 +1,60 @@
package {{ .PackageName }}
import (
"fmt"
"sync"
{{range .Kinds }}
"{{ $.KindPackagePrefix }}/{{ .Meta.MachineName }}"{{end}}
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/kindsys"
"github.com/grafana/thema"
)
// Base is a registry of kindsys.Interface. It provides two modes for accessing
// kinds: individually via literal named methods, or as a slice returned from
// an All*() method.
//
// Prefer the individual named methods for use cases where the particular kind(s) that
// are needed are known to the caller. For example, a dashboard linter can know that it
// specifically wants the dashboard kind.
//
// Prefer All*() methods when performing operations generically across all kinds.
// For example, a validation HTTP middleware for any kind-schematized object type.
type Base struct {
all []kindsys.Interface
numRaw, numStructured int
{{- range .Kinds }}
{{ .Meta.MachineName }} *{{ .Meta.MachineName }}.Kind{{end}}
}
// type guards
var (
{{- range .Kinds }}
_ kindsys.{{ if .IsRaw }}Raw{{ else }}Structured{{ end }} = &{{ .Meta.MachineName }}.Kind{}{{end}}
)
{{range .Kinds }}
// {{ .Meta.Name }} returns the [kindsys.Interface] implementation for the {{ .Meta.MachineName }} kind.
func (b *Base) {{ .Meta.Name }}() *{{ .Meta.MachineName }}.Kind {
return b.{{ .Meta.MachineName }}
}
{{end}}
func doNewBase(rt *thema.Runtime) *Base {
var err error
reg := &Base{
numRaw: {{ .NumRaw }},
numStructured: {{ .NumStructured }},
}
{{range .Kinds }}
reg.{{ .Meta.MachineName }}, err = {{ .Meta.MachineName }}.NewKind({{ if .IsCoreStructured }}rt{{ end }})
if err != nil {
panic(fmt.Sprintf("error while initializing the {{ .Meta.MachineName }} Kind: %s", err))
}
reg.all = append(reg.all, reg.{{ .Meta.MachineName }})
{{end}}
return reg
}

View File

@ -3,11 +3,13 @@ package codegen
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"strings"
"golang.org/x/tools/go/ast/astutil"
@ -65,3 +67,84 @@ func postprocessGoFile(cfg genGoFile) ([]byte, error) {
return byt, nil
}
type prefixmod struct {
str string
base string
rxp *regexp.Regexp
rxpsuff *regexp.Regexp
}
// PrefixDropper returns an astutil.ApplyFunc that removes the provided prefix
// string when it appears as a leading sequence in type names, var names, and
// comments in a generated Go file.
func PrefixDropper(prefix string) astutil.ApplyFunc {
return (&prefixmod{
str: prefix,
rxpsuff: regexp.MustCompile(fmt.Sprintf(`%s([a-zA-Z_]+)`, prefix)),
rxp: regexp.MustCompile(fmt.Sprintf(`%s([\s.,;-])`, prefix)),
}).applyfunc
}
func depoint(e ast.Expr) ast.Expr {
if star, is := e.(*ast.StarExpr); is {
return star.X
}
return e
}
func (d prefixmod) applyfunc(c *astutil.Cursor) bool {
n := c.Node()
switch x := n.(type) {
case *ast.ValueSpec:
d.handleExpr(x.Type)
for _, id := range x.Names {
d.do(id)
}
case *ast.TypeSpec:
// Always do typespecs
d.do(x.Name)
case *ast.Field:
// Don't rename struct fields. We just want to rename type declarations, and
// field value specifications that reference those types.
d.handleExpr(x.Type)
case *ast.CommentGroup:
for _, c := range x.List {
c.Text = d.rxpsuff.ReplaceAllString(c.Text, "$1")
if d.base != "" {
c.Text = d.rxp.ReplaceAllString(c.Text, d.base+"$1")
}
}
}
return true
}
func (d prefixmod) handleExpr(e ast.Expr) {
// Deref a StarExpr, if there is one
expr := depoint(e)
switch x := expr.(type) {
case *ast.Ident:
d.do(x)
case *ast.ArrayType:
if id, is := depoint(x.Elt).(*ast.Ident); is {
d.do(id)
}
case *ast.MapType:
if id, is := depoint(x.Key).(*ast.Ident); is {
d.do(id)
}
if id, is := depoint(x.Value).(*ast.Ident); is {
d.do(id)
}
}
}
func (d prefixmod) do(n *ast.Ident) {
if n.Name != d.str {
n.Name = strings.TrimPrefix(n.Name, d.str)
} else if d.base != "" {
n.Name = d.base
}
}

View File

@ -1,136 +0,0 @@
// This file is autogenerated. DO NOT EDIT.
//
// Generated by pkg/framework/coremodel/gen.go
//
// Derived from the Thema lineage declared in pkg/coremodel/playlist/coremodel.cue
//
// Run `make gen-cue` from repository root to regenerate.
package playlist
import (
"embed"
"path/filepath"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/framework/coremodel"
"github.com/grafana/thema"
)
// Defines values for PlaylistItemType.
const (
PlaylistItemTypeDashboardById PlaylistItemType = "dashboard_by_id"
PlaylistItemTypeDashboardByTag PlaylistItemType = "dashboard_by_tag"
PlaylistItemTypeDashboardByUid PlaylistItemType = "dashboard_by_uid"
)
// Model is the Go representation of a playlist.
//
// 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 Model struct {
// Interval sets the time between switching views in a playlist.
// FIXME: Is this based on a standardized format or what options are available? Can datemath be used?
Interval string `json:"interval"`
// The ordered list of items that the playlist will iterate over.
// FIXME! This should not be optional, but changing it makes the godegen awkward
Items *[]PlaylistItem `json:"items,omitempty"`
// Name of the playlist.
Name string `json:"name"`
// Unique playlist identifier. Generated on creation, either by the
// creator of the playlist of by the application.
Uid string `json:"uid"`
}
// PlaylistItem is the Go representation of a playlist.Item.
//
// 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 PlaylistItem struct {
// Title is an unused property -- it will be removed in the future
Title *string `json:"title,omitempty"`
// Type of the item.
Type PlaylistItemType `json:"type"`
// Value depends on type and describes the playlist item.
//
// - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
// is not portable as the numerical identifier is non-deterministic between different instances.
// Will be replaced by dashboard_by_uid in the future. (deprecated)
// - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
// dashboards behind the tag will be added to the playlist.
// - dashboard_by_uid: The value is the dashboard UID
Value string `json:"value"`
}
// Type of the item.
//
// 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 PlaylistItemType string
//go:embed coremodel.cue
var cueFS embed.FS
// The current version of the coremodel schema, as declared in coremodel.cue.
// This version determines what schema version is returned from [Coremodel.CurrentSchema],
// and which schema version is used for code generation within the grafana/grafana repository.
//
// The code generator ensures that this is always the latest Thema schema version.
var currentVersion = thema.SV(0, 0)
// Lineage returns the Thema lineage representing a Grafana playlist.
//
// The lineage is the canonical specification of the current playlist schema,
// all prior schema versions, and the mappings that allow migration between
// schema versions.
func Lineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("pkg", "coremodel", "playlist"), cueFS, rt, opts...)
}
var _ thema.LineageFactory = Lineage
var _ coremodel.Interface = &Coremodel{}
// Coremodel contains the foundational schema declaration for playlists.
// It implements coremodel.Interface.
type Coremodel struct {
lin thema.Lineage
}
// Lineage returns the canonical playlist Lineage.
func (c *Coremodel) Lineage() thema.Lineage {
return c.lin
}
// CurrentSchema returns the current (latest) playlist 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 playlist 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(rt *thema.Runtime) (*Coremodel, error) {
lin, err := Lineage(rt)
if err != nil {
return nil, err
}
return &Coremodel{
lin: lin,
}, nil
}

View File

@ -5,15 +5,19 @@
package cuectx
import (
"fmt"
"io/fs"
"path/filepath"
"testing/fstest"
"cuelang.org/go/cue"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/cuecontext"
"github.com/grafana/grafana"
"github.com/grafana/thema"
"github.com/grafana/thema/load"
"github.com/grafana/thema/vmux"
"github.com/yalue/merged_fs"
)
var ctx = cuecontext.New()
@ -84,16 +88,16 @@ func LoadGrafanaInstancesWithThema(path string, cueFS fs.FS, rt *thema.Runtime,
return lin, nil
}
// prefixWithGrafanaCUE constructs an fs.FS that merges the provided fs.FS with one
// containing grafana's cue.mod at the root. The provided prefix should be the
// prefixWithGrafanaCUE constructs an fs.FS that merges the provided fs.FS with
// the embedded FS containing Grafana's core CUE files, [grafana.CueSchemaFS].
// The provided prefix should be the relative path from the grafana repository
// root to the directory root of the provided inputfs.
//
// The returned fs.FS is suitable for passing to a CUE loader, such as
// cuelang.org/cue/load.Instances or
// github.com/grafana/thema/load.InstancesWithThema.
// The returned fs.FS is suitable for passing to a CUE loader, such as [load.InstancesWithThema].
func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) {
m := fstest.MapFS{
// fstest can recognize only forward slashes.
filepath.ToSlash(filepath.Join("cue.mod", "module.cue")): &fstest.MapFile{Data: []byte(`module: "github.com/grafana/grafana"`)},
// filepath.ToSlash(filepath.Join("cue.mod", "module.cue")): &fstest.MapFile{Data: []byte(`module: "github.com/grafana/grafana"`)},
}
prefix = filepath.FromSlash(prefix)
@ -114,6 +118,81 @@ func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) {
m[filepath.ToSlash(filepath.Join(prefix, path))] = &fstest.MapFile{Data: b}
return nil
})
return m, err
if err != nil {
return nil, err
}
return merged_fs.NewMergedFS(m, grafana.CueSchemaFS), nil
}
// BuildGrafanaInstance wraps [load.InstancesWithThema] to load a
// [*build.Instance] corresponding to a particular path within the
// github.com/grafana/grafana CUE module, then builds that into a [cue.Value],
// checks it for errors and returns.
//
// This allows resolution of imports within the grafana or thema CUE modules to
// work correctly and consistently by relying on the embedded FS at
// [grafana.CueSchemaFS] and [thema.CueFS].
//
// relpath should be a relative path path within [grafana.CueSchemaFS] to be
// loaded. Optionally, the caller may provide an additional fs.FS via the
// overlay parameter, which will be merged with [grafana.CueSchemaFS] at
// relpath, and loaded.
//
// pkg, if non-empty, is set as the value of
// ["cuelang.org/go/cue/load".Config.Package]. If the CUE package to be loaded
// is the same as the parent directory name, it should be omitted.
//
// NOTE this function will be removed in favor of a more generic loader
func BuildGrafanaInstance(relpath string, pkg string, ctx *cue.Context, overlay fs.FS) (cue.Value, error) {
// notes about how this crap needs to work
//
// Within grafana/grafana, need:
// - pass in an fs.FS that, in its root, contains the .cue files to load
// - has no cue.mod
// - gets prefixed with the appropriate path within grafana/grafana
// - and merged with all the other .cue files from grafana/grafana
if ctx == nil {
ctx = GrafanaCUEContext()
}
relpath = filepath.ToSlash(relpath)
var v cue.Value
var f fs.FS = grafana.CueSchemaFS
var err error
if overlay != nil {
f, err = prefixWithGrafanaCUE(relpath, overlay)
if err != nil {
return v, err
}
}
var bi *build.Instance
if pkg != "" {
bi, err = load.InstancesWithThema(f, relpath, load.Package(pkg))
} else {
bi, err = load.InstancesWithThema(f, relpath)
}
if err != nil {
return v, err
}
v = ctx.BuildInstance(bi)
if v.Err() != nil {
return v, fmt.Errorf("%s not a valid CUE instance: %w", relpath, v.Err())
}
return v, nil
}
// TODO docs
// NOTE this function will be removed in favor of a more generic loader
func LoadInstanceWithGrafana(ifs fs.FS, prefix string) (*build.Instance, error) {
// notes about how this crap needs to work
//
// Need a prefixing instance loader that:
// - can take multiple fs.FS, each one representing a CUE module (nesting?)
// - reconcile at most one of the provided fs with cwd
// - behavior must differ depending on whether cwd is in a cue module
// - behavior should(?) be controllable depending on
panic("TODO")
}

View File

@ -7,21 +7,12 @@ package main
import (
"fmt"
"os"
"path"
"path/filepath"
"sort"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/load"
"github.com/grafana/cuetsy"
"github.com/grafana/cuetsy/ts"
"github.com/grafana/cuetsy/ts/ast"
gcgen "github.com/grafana/grafana/pkg/codegen"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/thema"
)
const sep = string(filepath.Separator)
@ -45,74 +36,11 @@ func init() {
// Generate Go and Typescript implementations for all coremodels, and populate the
// coremodel static registry.
func main() {
rt := cuectx.GrafanaThemaRuntime()
if len(os.Args) > 1 {
fmt.Fprintf(os.Stderr, "coremodel code generator does not currently accept any arguments\n, got %q", os.Args)
os.Exit(1)
}
items, err := os.ReadDir(cmroot)
if err != nil {
fmt.Fprintf(os.Stderr, "could not read coremodels parent dir %s: %s\n", cmroot, err)
os.Exit(1)
}
var lins []*gcgen.CoremodelDeclaration
for _, item := range items {
if item.IsDir() {
lin, err := gcgen.ExtractLineage(filepath.Join(cmroot, item.Name(), "coremodel.cue"), rt)
if err != nil {
fmt.Fprintf(os.Stderr, "could not process coremodel dir %s: %s\n", filepath.Join(cmroot, item.Name()), err)
os.Exit(1)
}
lins = append(lins, lin)
}
}
sort.Slice(lins, func(i, j int) bool {
return lins[i].Lineage.Name() < lins[j].Lineage.Name()
})
// The typescript veneer index.gen.ts file, which we'll build up over time
// from the exported types.
tsvidx := new(ast.File)
wd := gcgen.NewWriteDiffer()
for _, ls := range lins {
gofiles, err := ls.GenerateGoCoremodel(filepath.Join(cmroot, ls.Lineage.Name()))
if err != nil {
fmt.Fprintf(os.Stderr, "failed to generate Go for %s: %s\n", ls.Lineage.Name(), err)
os.Exit(1)
}
wd.Merge(gofiles)
// Only generate TS for API types
if ls.IsAPIType {
tsf, err := ls.GenerateTypescriptCoremodel()
if err != nil {
fmt.Fprintf(os.Stderr, "error generating TypeScript for %s: %s\n", ls.Lineage.Name(), err)
os.Exit(1)
}
tsf.Doc = mkTSHeader(ls)
wd[filepath.FromSlash(filepath.Join(tsroot, rawTSGenPath(ls)))] = []byte(tsf.String())
decls, err := extractTSIndexVeneerElements(ls, tsf)
if err != nil {
fmt.Fprintf(os.Stderr, "error generating TypeScript veneer for %s: %s\n", ls.Lineage.Name(), errors.Details(err, nil))
os.Exit(1)
}
tsvidx.Nodes = append(tsvidx.Nodes, decls...)
}
}
tsvidx.Doc = mkTSHeader(nil)
wd[filepath.Join(tsroot, "index.gen.ts")] = []byte(tsvidx.String())
regfiles, err := gcgen.GenerateCoremodelRegistry(filepath.Join(groot, "pkg", "framework", "coremodel", "registry", "registry_gen.go"), lins)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to generate coremodel registry: %s\n", err)
os.Exit(1)
}
wd.Merge(regfiles)
// TODO generating these is here temporarily until we make a more permanent home
wdsh, err := genSharedSchemas(groot)
@ -137,25 +65,6 @@ func main() {
}
}
// generates the path relative to packages/grafana-schema/src at which the raw
// type definitions should be exported for the latest schema of this type
func rawTSGenPath(cm *gcgen.CoremodelDeclaration) string {
return fmt.Sprintf("raw/%s/%s/%s.gen.ts", cm.Lineage.Name(), cm.PathVersion(), cm.Lineage.Name())
}
func mkTSHeader(cm *gcgen.CoremodelDeclaration) *ast.Comment {
v := gcgen.HeaderVars{
GeneratorPath: "pkg/framework/coremodel/gen.go",
}
if cm != nil {
v.LineagePath = cm.RelativePath
}
v.GeneratorPath = "pkg/framework/coremodel/gen.go"
return &ast.Comment{
Text: strings.TrimSpace(gcgen.GenGrafanaHeader(v)),
}
}
func genSharedSchemas(groot string) (gcgen.WriteDiffer, error) {
abspath := filepath.Join(groot, "packages", "grafana-schema", "src", "schema")
cfg := &load.Config{
@ -183,7 +92,7 @@ func genSharedSchemas(groot string) (gcgen.WriteDiffer, error) {
}
wd := gcgen.NewWriteDiffer()
wd[filepath.Join(abspath, "mudball.gen.ts")] = append([]byte(`//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
wd[filepath.Join(abspath, "mudball.gen.ts")] = append([]byte(`//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
// This file is autogenerated. DO NOT EDIT.
//
// To regenerate, run "make gen-cue" from the repository root.
@ -191,259 +100,3 @@ func genSharedSchemas(groot string) (gcgen.WriteDiffer, error) {
`), b...)
return wd, nil
}
// TODO make this more generic and reusable
func extractTSIndexVeneerElements(cm *gcgen.CoremodelDeclaration, tf *ast.File) ([]ast.Decl, error) {
lin := cm.Lineage
sch := thema.SchemaP(lin, thema.LatestVersion(lin))
// Check the root, then walk the tree
rootv := sch.UnwrapCUE()
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:
name = strings.Title(cm.Lineage.Name())
fallthrough
case 1:
// Only deal with subpaths that are definitions, for now
// TODO incorporate smarts about grouped lineages here
if name == "" {
if !sels[0].IsDefinition() {
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 decl nodes
pair := findDeclNode(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 {
custom = append(custom, *pair.T)
if pair.D != nil {
customD = append(customD, *pair.D)
}
} 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
}
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 entity type.", cm.Lineage.Name()), 80, false)},
TypeOnly: true,
Exports: raw,
From: ast.Str{Value: fmt.Sprintf("./raw/%s/%s/%s.gen", cm.Lineage.Name(), cm.PathVersion(), cm.Lineage.Name())},
})
}
if len(rawD) > 0 {
ret = append(ret, ast.ExportSet{
CommentList: []ast.Comment{ts.CommentFromString(fmt.Sprintf("Raw generated default consts from %s entity type.", cm.Lineage.Name()), 80, false)},
TypeOnly: false,
Exports: rawD,
From: ast.Str{Value: fmt.Sprintf("./raw/%s/%s/%s.gen", cm.Lineage.Name(), cm.PathVersion(), cm.Lineage.Name())},
})
}
vtfile := fmt.Sprintf("./veneer/%s.types", cm.Lineage.Name())
customstr := fmt.Sprintf(`// The following exported declarations correspond to types in the %s@%s schema with
// attribute @grafana(TSVeneer="type"). (lineage declared in file: %s)
//
// 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`,
cm.Lineage.Name(), thema.LatestVersion(cm.Lineage), cm.RelativePath, filepath.Clean(path.Join("packages", "grafana-schema", "src", 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 decl 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
}
func findDeclNode(name string, tf *ast.File) declPair {
var p declPair
for _, decl := range tf.Nodes {
// Peer through export keywords
if ex, is := decl.(ast.ExportKeyword); is {
decl = ex.Decl
}
switch x := decl.(type) {
case ast.TypeDecl:
if x.Name.Name == name {
p.T = &x.Name
}
case ast.VarDecl:
if x.Names.Idents[0].Name == "default"+name {
p.D = &x.Names.Idents[0]
}
}
}
return p
}
type tsVeneerAttr struct {
target string
}
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.IncompleteKind() {
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)...)
}
var allowedTSVeneers = map[string]bool{
"type": true,
}
func allowedTSVeneersString() string {
var list []string
for tgt := range allowedTSVeneers {
list = append(list, tgt)
}
sort.Strings(list)
return strings.Join(list, "|")
}
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
}
func valError(v cue.Value, format string, args ...interface{}) error {
s := v.Source()
if s == nil {
return fmt.Errorf(format, args...)
}
return errors.Newf(s.Pos(), format, args...)
}

View File

@ -1,22 +0,0 @@
package registry_test
import (
"testing"
"github.com/grafana/grafana/pkg/framework/coremodel/registry"
"github.com/grafana/thema"
)
func TestSchemaAssignability(t *testing.T) {
reg := registry.NewBase(nil)
for _, cm := range reg.All() {
tcm := cm
t.Run(tcm.Lineage().Name(), func(t *testing.T) {
err := thema.AssignableTo(tcm.CurrentSchema(), tcm.GoType())
if err != nil {
t.Fatal(err)
}
})
}
}

View File

@ -1,48 +0,0 @@
package registry
import (
"sync"
"github.com/google/wire"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/framework/coremodel"
"github.com/grafana/thema"
)
// CoremodelSet contains all of the wire-style providers related to coremodels.
var CoremodelSet = wire.NewSet(
NewBase,
)
var (
baseOnce sync.Once
defaultBase *Base
)
// NewBase provides a registry of all coremodels, without any composition of
// plugin-defined schemas.
//
// All calling code within grafana/grafana is expected to use Grafana's
// singleton [thema.Runtime], returned from [cuectx.GrafanaThemaRuntime]. If nil
// is passed, the singleton will be used.
func NewBase(rt *thema.Runtime) *Base {
allrt := cuectx.GrafanaThemaRuntime()
if rt == nil || rt == allrt {
baseOnce.Do(func() {
defaultBase = doProvideBase(allrt)
})
return defaultBase
}
return doProvideBase(rt)
}
// All returns a slice of all registered coremodels.
//
// Prefer this method when operating generically across all coremodels.
//
// The returned slice is sorted lexicographically by coremodel name. It should
// not be modified.
func (b *Base) All() []coremodel.Interface {
return b.all
}

View File

@ -1,83 +0,0 @@
// 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"
"github.com/grafana/grafana/pkg/coremodel/dashboard"
"github.com/grafana/grafana/pkg/coremodel/playlist"
"github.com/grafana/grafana/pkg/coremodel/pluginmeta"
"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
dashboard *dashboard.Coremodel
playlist *playlist.Coremodel
pluginmeta *pluginmeta.Coremodel
}
// type guards
var (
_ coremodel.Interface = &dashboard.Coremodel{}
_ coremodel.Interface = &playlist.Coremodel{}
_ coremodel.Interface = &pluginmeta.Coremodel{}
)
// Dashboard returns the dashboard coremodel. The return value is guaranteed to
// implement coremodel.Interface.
func (b *Base) Dashboard() *dashboard.Coremodel {
return b.dashboard
}
// Playlist returns the playlist coremodel. The return value is guaranteed to
// implement coremodel.Interface.
func (b *Base) Playlist() *playlist.Coremodel {
return b.playlist
}
// Pluginmeta returns the pluginmeta coremodel. The return value is guaranteed to
// implement coremodel.Interface.
func (b *Base) Pluginmeta() *pluginmeta.Coremodel {
return b.pluginmeta
}
func doProvideBase(rt *thema.Runtime) *Base {
var err error
reg := &Base{}
reg.dashboard, err = dashboard.New(rt)
if err != nil {
panic(fmt.Sprintf("error while initializing dashboard coremodel: %s", err))
}
reg.all = append(reg.all, reg.dashboard)
reg.playlist, err = playlist.New(rt)
if err != nil {
panic(fmt.Sprintf("error while initializing playlist coremodel: %s", err))
}
reg.all = append(reg.all, reg.playlist)
reg.pluginmeta, err = pluginmeta.New(rt)
if err != nil {
panic(fmt.Sprintf("error while initializing pluginmeta coremodel: %s", err))
}
reg.all = append(reg.all, reg.pluginmeta)
return reg
}

View File

@ -1,2 +0,0 @@
// Package Slot exposes Grafana's coremodel composition Slot definitions for use in Go.
package slot

View File

@ -0,0 +1,104 @@
// THIS FILE IS GENERATED. EDITING IS FUTILE.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// CoreStructuredKindJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package dashboard
import (
"github.com/grafana/grafana/pkg/kindsys"
"github.com/grafana/thema"
"github.com/grafana/thema/vmux"
)
// rootrel is the relative path from the grafana repository root to the
// directory containing the .cue files in which this kind is declared. Necessary
// for runtime errors related to the declaration and/or lineage to provide
// a real path to the correct .cue file.
const rootrel string = "kinds/structured/dashboard"
// TODO standard generated docs
type Kind struct {
lin thema.ConvergentLineage[*Dashboard]
jendec vmux.Endec
valmux vmux.ValueMux[*Dashboard]
decl kindsys.Decl[kindsys.CoreStructuredMeta]
}
// type guard
var _ kindsys.Structured = &Kind{}
// TODO standard generated docs
func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil)
if err != nil {
return nil, err
}
k := &Kind{
decl: *decl,
}
lin, err := decl.Some().BindKindLineage(rt, opts...)
if err != nil {
return nil, err
}
// Get the thema.Schema that the meta says is in the current version (which
// codegen ensures is always the latest)
cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion)
tsch, err := thema.BindType[*Dashboard](cursch, &Dashboard{})
if err != nil {
// Should be unreachable, modulo bugs in the Thema->Go code generator
return nil, err
}
k.jendec = vmux.NewJSONEndec("dashboard.json")
k.lin = tsch.ConvergentLineage()
k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jendec)
return k, nil
}
// TODO standard generated docs
func (k *Kind) Name() string {
return "dashboard"
}
// TODO standard generated docs
func (k *Kind) MachineName() string {
return "dashboard"
}
// TODO standard generated docs
func (k *Kind) Lineage() thema.Lineage {
return k.lin
}
// TODO standard generated docs
func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*Dashboard] {
return k.lin
}
// JSONValueMux is a version multiplexer that maps a []byte containing JSON data
// at any schematized dashboard version to an instance of Dashboard.
//
// 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) (*Dashboard, thema.TranslationLacunas, error) {
return k.valmux(b)
}
// TODO standard generated docs
func (k *Kind) Maturity() kindsys.Maturity {
return k.decl.Meta.Maturity
}
// TODO standard generated docs
func (k *Kind) Meta() kindsys.CoreStructuredMeta {
return k.decl.Meta
}

View File

@ -1,22 +1,14 @@
// This file is autogenerated. DO NOT EDIT.
// THIS FILE IS GENERATED. EDITING IS FUTILE.
//
// Generated by pkg/framework/coremodel/gen.go
// Generated by:
// kinds/gen.go
// Using jennies:
// GoTypesJenny
//
// Derived from the Thema lineage declared in pkg/coremodel/dashboard/coremodel.cue
//
// Run `make gen-cue` from repository root to regenerate.
// Run 'make gen-cue' from repository root to regenerate.
package dashboard
import (
"embed"
"path/filepath"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/framework/coremodel"
"github.com/grafana/thema"
)
// Defines values for GraphTooltip.
const (
GraphTooltipN0 GraphTooltip = 0
@ -207,11 +199,8 @@ const (
VariableTypeTextbox VariableType = "textbox"
)
// Model is the Go representation of a dashboard.
//
// 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 Model struct {
// Dashboard defines model for dashboard.
type Dashboard struct {
Annotations *struct {
// TODO docs
List []AnnotationQuery `json:"list"`
@ -298,29 +287,17 @@ type Model struct {
WeekStart *string `json:"weekStart,omitempty"`
}
// GraphTooltip is the Go representation of a Model.GraphTooltip.
//
// 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.
// GraphTooltip defines model for Dashboard.GraphTooltip.
type GraphTooltip int
// Theme of dashboard.
//
// 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 Style string
// Timezone of dashboard,
//
// 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 Timezone string
// TODO docs
// FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
//
// 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 AnnotationQuery struct {
BuiltIn int `json:"builtIn"`
@ -343,16 +320,15 @@ type AnnotationQuery struct {
Name *string `json:"name,omitempty"`
// Query for annotation data.
RawQuery *string `json:"rawQuery,omitempty"`
ShowIn int `json:"showIn"`
Target *AnnotationTarget `json:"target,omitempty"`
Type string `json:"type"`
RawQuery *string `json:"rawQuery,omitempty"`
ShowIn int `json:"showIn"`
// TODO docs
Target *AnnotationTarget `json:"target,omitempty"`
Type string `json:"type"`
}
// AnnotationTarget is the Go representation of a dashboard.AnnotationTarget.
//
// 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.
// TODO docs
type AnnotationTarget struct {
Limit int64 `json:"limit"`
MatchAny bool `json:"matchAny"`
@ -363,16 +339,10 @@ type AnnotationTarget struct {
// 0 for no shared crosshair or tooltip (default).
// 1 for shared crosshair.
// 2 for shared crosshair AND shared tooltip.
//
// 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 DashboardCursorSync int
// FROM public/app/features/dashboard/state/Models.ts - ish
// TODO docs
//
// 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 DashboardLink struct {
AsDropdown bool `json:"asDropdown"`
Icon *string `json:"icon,omitempty"`
@ -386,25 +356,16 @@ type DashboardLink struct {
Url *string `json:"url,omitempty"`
}
// DashboardLinkType is the Go representation of a DashboardLink.Type.
//
// 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.
// DashboardLinkType defines model for DashboardLink.Type.
type DashboardLinkType string
// DynamicConfigValue is the Go representation of a dashboard.DynamicConfigValue.
//
// 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.
// DynamicConfigValue defines model for dashboard.DynamicConfigValue.
type DynamicConfigValue struct {
Id string `json:"id"`
Value *interface{} `json:"value,omitempty"`
}
// TODO docs
//
// 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 FieldColor struct {
// Stores the fixed color value if mode is fixed
FixedColor *string `json:"fixedColor,omitempty"`
@ -417,21 +378,12 @@ type FieldColor struct {
}
// TODO docs
//
// 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 FieldColorModeId string
// TODO docs
//
// 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 FieldColorSeriesByMode string
// FieldConfig is the Go representation of a dashboard.FieldConfig.
//
// 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.
// FieldConfig defines model for dashboard.FieldConfig.
type FieldConfig struct {
// TODO docs
Color *FieldColor `json:"color,omitempty"`
@ -467,7 +419,7 @@ type FieldConfig struct {
// Alternative to empty string
NoValue *string `json:"noValue,omitempty"`
// An explict path to the field in the datasource. When the frame meta includes a path,
// An explicit path to the field in the datasource. When the frame meta includes a path,
// This will default to `${frame.meta.path}/${field.name}
//
// When defined, this value can be used as an identifier within the datasource scope, and
@ -482,10 +434,7 @@ type FieldConfig struct {
Writeable *bool `json:"writeable,omitempty"`
}
// FieldConfigSource is the Go representation of a dashboard.FieldConfigSource.
//
// 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.
// FieldConfigSource defines model for dashboard.FieldConfigSource.
type FieldConfigSource struct {
Defaults struct {
// TODO docs
@ -522,7 +471,7 @@ type FieldConfigSource struct {
// Alternative to empty string
NoValue *string `json:"noValue,omitempty"`
// An explict path to the field in the datasource. When the frame meta includes a path,
// An explicit path to the field in the datasource. When the frame meta includes a path,
// This will default to `${frame.meta.path}/${field.name}
//
// When defined, this value can be used as an identifier within the datasource scope, and
@ -548,25 +497,16 @@ type FieldConfigSource struct {
} `json:"overrides"`
}
// GraphPanel is the Go representation of a dashboard.GraphPanel.
//
// 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.
// GraphPanel defines model for dashboard.GraphPanel.
type GraphPanel struct {
// Support for legacy graph and heatmap panels.
Type GraphPanelType `json:"type"`
}
// Support for legacy graph and heatmap panels.
//
// 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 GraphPanelType string
// GridPos is the Go representation of a dashboard.GridPos.
//
// 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.
// GridPos defines model for dashboard.GridPos.
type GridPos struct {
// Panel
H int `json:"h"`
@ -584,41 +524,26 @@ type GridPos struct {
Y int `json:"y"`
}
// HeatmapPanel is the Go representation of a dashboard.HeatmapPanel.
//
// 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.
// HeatmapPanel defines model for dashboard.HeatmapPanel.
type HeatmapPanel struct {
Type HeatmapPanelType `json:"type"`
}
// HeatmapPanelType is the Go representation of a HeatmapPanel.Type.
//
// 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.
// HeatmapPanelType defines model for HeatmapPanel.Type.
type HeatmapPanelType string
// TODO docs
//
// 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 MappingType string
// MatcherConfig is the Go representation of a dashboard.MatcherConfig.
//
// 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.
// MatcherConfig defines model for dashboard.MatcherConfig.
type MatcherConfig struct {
Id string `json:"id"`
Options *interface{} `json:"options,omitempty"`
}
// Model panels. Panels are canonically defined inline
// Dashboard panels. Panels are canonically defined inline
// because they share a version timeline with the dashboard
// schema; they do not evolve independently.
//
// 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 Panel struct {
// The datasource used in all targets.
Datasource *struct {
@ -664,7 +589,7 @@ type Panel struct {
// Alternative to empty string
NoValue *string `json:"noValue,omitempty"`
// An explict path to the field in the datasource. When the frame meta includes a path,
// An explicit path to the field in the datasource. When the frame meta includes a path,
// This will default to `${frame.meta.path}/${field.name}
//
// When defined, this value can be used as an identifier within the datasource scope, and
@ -755,15 +680,9 @@ type Panel struct {
// Direction to repeat in if 'repeat' is set.
// "h" for horizontal, "v" for vertical.
//
// 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 PanelRepeatDirection string
// TODO docs
//
// 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 RangeMap struct {
Options struct {
// to and from are `number | null` in current ts, really not sure what to do
@ -779,16 +698,10 @@ type RangeMap struct {
Type RangeMapType `json:"type"`
}
// RangeMapType is the Go representation of a RangeMap.Type.
//
// 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.
// RangeMapType defines model for RangeMap.Type.
type RangeMapType string
// TODO docs
//
// 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 RegexMap struct {
Options struct {
Pattern string `json:"pattern"`
@ -802,16 +715,10 @@ type RegexMap struct {
Type RegexMapType `json:"type"`
}
// RegexMapType is the Go representation of a RegexMap.Type.
//
// 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.
// RegexMapType defines model for RegexMap.Type.
type RegexMapType string
// Row panel
//
// 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 RowPanel struct {
Collapsed bool `json:"collapsed"`
@ -830,16 +737,10 @@ type RowPanel struct {
Type RowPanelType `json:"type"`
}
// RowPanelType is the Go representation of a RowPanel.Type.
//
// 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.
// RowPanelType defines model for RowPanel.Type.
type RowPanelType string
// TODO docs
//
// 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 SpecialValueMap struct {
Options struct {
Match SpecialValueMapOptionsMatch `json:"match"`
@ -854,40 +755,25 @@ type SpecialValueMap struct {
Type SpecialValueMapType `json:"type"`
}
// SpecialValueMapOptionsMatch is the Go representation of a SpecialValueMap.Options.Match.
//
// 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.
// SpecialValueMapOptionsMatch defines model for SpecialValueMap.Options.Match.
type SpecialValueMapOptionsMatch string
// SpecialValueMapType is the Go representation of a SpecialValueMap.Type.
//
// 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.
// SpecialValueMapType defines model for SpecialValueMap.Type.
type SpecialValueMapType string
// TODO docs
//
// 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 SpecialValueMatch string
// Schema for panel targets is specified by datasource
// plugins. We use a placeholder definition, which the Go
// schema loader either left open/as-is with the Base
// variant of the Model and Panel families, or filled
// variant of the Dashboard and Panel families, or filled
// with types derived from plugins in the Instance variant.
// When working directly from CUE, importers can extend this
// type directly to achieve the same effect.
//
// 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 Target map[string]interface{}
// TODO docs
//
// 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 Threshold struct {
// TODO docs
Color string `json:"color"`
@ -902,10 +788,7 @@ type Threshold struct {
Value *float32 `json:"value,omitempty"`
}
// ThresholdsConfig is the Go representation of a dashboard.ThresholdsConfig.
//
// 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.
// ThresholdsConfig defines model for dashboard.ThresholdsConfig.
type ThresholdsConfig struct {
Mode ThresholdsConfigMode `json:"mode"`
@ -925,53 +808,32 @@ type ThresholdsConfig struct {
} `json:"steps"`
}
// ThresholdsConfigMode is the Go representation of a ThresholdsConfig.Mode.
//
// 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.
// ThresholdsConfigMode defines model for ThresholdsConfig.Mode.
type ThresholdsConfigMode string
// ThresholdsMode is the Go representation of a dashboard.ThresholdsMode.
//
// 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.
// ThresholdsMode defines model for dashboard.ThresholdsMode.
type ThresholdsMode string
// TODO docs
// FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it
//
// 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 Transformation struct {
Id string `json:"id"`
Options map[string]interface{} `json:"options"`
}
// TODO docs
//
// 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 ValueMap struct {
Options map[string]interface{} `json:"options"`
Type ValueMapType `json:"type"`
}
// ValueMapType is the Go representation of a ValueMap.Type.
//
// 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.
// ValueMapType defines model for ValueMap.Type.
type ValueMapType string
// TODO docs
//
// 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 ValueMapping interface{}
// TODO docs
//
// 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 ValueMappingResult struct {
Color *string `json:"color,omitempty"`
Icon *string `json:"icon,omitempty"`
@ -983,85 +845,16 @@ type ValueMappingResult struct {
// TODO docs
// TODO what about what's in public/app/features/types.ts?
// TODO there appear to be a lot of different kinds of [template] vars here? if so need a disjunction
//
// 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 VariableModel struct {
Label *string `json:"label,omitempty"`
Name string `json:"name"`
Type VariableModelType `json:"type"`
}
// VariableModelType is the Go representation of a VariableModel.Type.
//
// 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.
// VariableModelType defines model for VariableModel.Type.
type VariableModelType string
// FROM: packages/grafana-data/src/types/templateVars.ts
// TODO docs
// TODO this implies some wider pattern/discriminated union, probably?
//
// 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 VariableType string
//go:embed coremodel.cue
var cueFS embed.FS
// The current version of the coremodel schema, as declared in coremodel.cue.
// This version determines what schema version is returned from [Coremodel.CurrentSchema],
// and which schema version is used for code generation within the grafana/grafana repository.
//
// The code generator ensures that this is always the latest Thema schema version.
var currentVersion = thema.SV(0, 0)
// Lineage returns the Thema lineage representing a Grafana dashboard.
//
// The lineage is the canonical specification of the current dashboard schema,
// all prior schema versions, and the mappings that allow migration between
// schema versions.
func Lineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("pkg", "coremodel", "dashboard"), cueFS, rt, opts...)
}
var _ thema.LineageFactory = Lineage
var _ coremodel.Interface = &Coremodel{}
// Coremodel contains the foundational schema declaration for dashboards.
// It implements coremodel.Interface.
type Coremodel struct {
lin thema.Lineage
}
// Lineage returns the canonical dashboard Lineage.
func (c *Coremodel) Lineage() thema.Lineage {
return c.lin
}
// CurrentSchema returns the current (latest) dashboard 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 dashboard 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(rt *thema.Runtime) (*Coremodel, error) {
lin, err := Lineage(rt)
if err != nil {
return nil, err
}
return &Coremodel{
lin: lin,
}, nil
}

View File

@ -10,10 +10,9 @@ import (
"testing"
"cuelang.org/go/cue/errors"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/coremodel/dashboard"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/kinds/dashboard"
"github.com/stretchr/testify/require"
)
func TestDevenvDashboardValidity(t *testing.T) {
@ -22,7 +21,7 @@ func TestDevenvDashboardValidity(t *testing.T) {
m, err := themaTestableDashboards(os.DirFS(path))
require.NoError(t, err)
cm, err := dashboard.New(cuectx.GrafanaThemaRuntime())
dk, err := dashboard.NewKind(cuectx.GrafanaThemaRuntime())
require.NoError(t, err)
for path, b := range m {
@ -31,7 +30,7 @@ func TestDevenvDashboardValidity(t *testing.T) {
cv, err := cuectx.JSONtoCUE(path, b)
require.NoError(t, err, "error while decoding dashboard JSON into a CUE value")
_, err = cm.CurrentSchema().Validate(cv)
_, err = dk.ConvergentLineage().TypedSchema().Validate(cv)
if err != nil {
// Testify trims errors to short length. We want the full text
errstr := errors.Details(err, nil)

View File

@ -0,0 +1,104 @@
// THIS FILE IS GENERATED. EDITING IS FUTILE.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// CoreStructuredKindJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package playlist
import (
"github.com/grafana/grafana/pkg/kindsys"
"github.com/grafana/thema"
"github.com/grafana/thema/vmux"
)
// rootrel is the relative path from the grafana repository root to the
// directory containing the .cue files in which this kind is declared. Necessary
// for runtime errors related to the declaration and/or lineage to provide
// a real path to the correct .cue file.
const rootrel string = "kinds/structured/playlist"
// TODO standard generated docs
type Kind struct {
lin thema.ConvergentLineage[*Playlist]
jendec vmux.Endec
valmux vmux.ValueMux[*Playlist]
decl kindsys.Decl[kindsys.CoreStructuredMeta]
}
// type guard
var _ kindsys.Structured = &Kind{}
// TODO standard generated docs
func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil)
if err != nil {
return nil, err
}
k := &Kind{
decl: *decl,
}
lin, err := decl.Some().BindKindLineage(rt, opts...)
if err != nil {
return nil, err
}
// Get the thema.Schema that the meta says is in the current version (which
// codegen ensures is always the latest)
cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion)
tsch, err := thema.BindType[*Playlist](cursch, &Playlist{})
if err != nil {
// Should be unreachable, modulo bugs in the Thema->Go code generator
return nil, err
}
k.jendec = vmux.NewJSONEndec("playlist.json")
k.lin = tsch.ConvergentLineage()
k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jendec)
return k, nil
}
// TODO standard generated docs
func (k *Kind) Name() string {
return "playlist"
}
// TODO standard generated docs
func (k *Kind) MachineName() string {
return "playlist"
}
// TODO standard generated docs
func (k *Kind) Lineage() thema.Lineage {
return k.lin
}
// TODO standard generated docs
func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*Playlist] {
return k.lin
}
// JSONValueMux is a version multiplexer that maps a []byte containing JSON data
// at any schematized dashboard version to an instance of Playlist.
//
// 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) (*Playlist, thema.TranslationLacunas, error) {
return k.valmux(b)
}
// TODO standard generated docs
func (k *Kind) Maturity() kindsys.Maturity {
return k.decl.Meta.Maturity
}
// TODO standard generated docs
func (k *Kind) Meta() kindsys.CoreStructuredMeta {
return k.decl.Meta
}

View File

@ -0,0 +1,59 @@
// THIS FILE IS GENERATED. EDITING IS FUTILE.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// GoTypesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package playlist
// Defines values for PlaylistItemType.
const (
PlaylistItemTypeDashboardById PlaylistItemType = "dashboard_by_id"
PlaylistItemTypeDashboardByTag PlaylistItemType = "dashboard_by_tag"
PlaylistItemTypeDashboardByUid PlaylistItemType = "dashboard_by_uid"
)
// Playlist defines model for playlist.
type Playlist struct {
// Interval sets the time between switching views in a playlist.
// FIXME: Is this based on a standardized format or what options are available? Can datemath be used?
Interval string `json:"interval"`
// The ordered list of items that the playlist will iterate over.
// FIXME! This should not be optional, but changing it makes the godegen awkward
Items *[]PlaylistItem `json:"items,omitempty"`
// Name of the playlist.
Name string `json:"name"`
// Unique playlist identifier. Generated on creation, either by the
// creator of the playlist of by the application.
Uid string `json:"uid"`
}
// PlaylistItem defines model for playlist.Item.
type PlaylistItem struct {
// Title is an unused property -- it will be removed in the future
Title *string `json:"title,omitempty"`
// Type of the item.
Type PlaylistItemType `json:"type"`
// Value depends on type and describes the playlist item.
//
// - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
// is not portable as the numerical identifier is non-deterministic between different instances.
// Will be replaced by dashboard_by_uid in the future. (deprecated)
// - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
// dashboards behind the tag will be added to the playlist.
// - dashboard_by_uid: The value is the dashboard UID
Value string `json:"value"`
}
// Type of the item.
type PlaylistItemType string

View File

@ -0,0 +1,54 @@
// THIS FILE IS GENERATED. EDITING IS FUTILE.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// RawKindJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package svg
import (
"github.com/grafana/grafana/pkg/kindsys"
)
// TODO standard generated docs
type Kind struct {
decl kindsys.Decl[kindsys.RawMeta]
}
// type guard
var _ kindsys.Raw = &Kind{}
// TODO standard generated docs
func NewKind() (*Kind, error) {
decl, err := kindsys.LoadCoreKind[kindsys.RawMeta]("kinds/raw/svg", nil, nil)
if err != nil {
return nil, err
}
return &Kind{
decl: *decl,
}, nil
}
// TODO standard generated docs
func (k *Kind) Name() string {
return "SVG"
}
// TODO standard generated docs
func (k *Kind) MachineName() string {
return "svg"
}
// TODO standard generated docs
func (k *Kind) Maturity() kindsys.Maturity {
return k.decl.Meta.Maturity
}
// TODO standard generated docs
func (k *Kind) Meta() kindsys.RawMeta {
return k.decl.Meta
}

60
pkg/kindsys/EXTENDING.md Normal file
View File

@ -0,0 +1,60 @@
# Kind System
This package contains Grafana's kind system, which defines the rules that govern all Grafana kind declarations, including both core and plugin kinds. It contains many contracts on which public promises of backwards compatibility are made. All changes must be considered with care.
While this package is maintained by @grafana/grafana-as-code, contributions from others are a main goal! Any time you have the thought, "I wish this part of Grafana's codebase was consistent," rather than writing docs (that people will inevitably miss), it's worth seeing if you can express that consistency as a kindsys extension instead.
This document is the guide to extending kindsys. But first, we have to identify kindsys's key components.
## Elements of kindsys
* **CUE framework** - the collection of .cue files in this directory, `pkg/kindsys`. These are schemas that define how Kinds are declared.
* **Go framework** - the Go package in this directory containing utilities for loading individual kind declarations, validating them against the CUE framework, and representing them consistently in Go.
* **Code generators** - `pkg/codegen` contains the codegen framework. Individual generators (which take one or many `pkg/kindsys.Decl`, and produce a single file) each have a `pkg/codegen/generator_*.go` file.
* **Registries** - generated lists of all or a well-defined subset of kinds that can be used in code. `pkg/registries/corekind` is a registry of all core `pkg/kindsys.Interface` implementations; `packages/grafana-schema/src/index.gen.ts` is a registry of all the TypeScript types generated from the current versions of each kind's schema.
* **Kind declarations** - the declarations of individual kinds. By kind category:
* **Core Structured** - each child directory of `kinds/structured`.
* **Raw** - each child directory of `kinds/raw`.
* **Composable** - In Grafana core, `public/app/plugins/*/*/models.cue` files.
* **Custom** - No examples in Grafana core. See [operator-app-sdk](https://github.com/grafana/operator-app-sdk) (TODO that repo is private; make it public, or point to public examples).
The above are treated as similarly to stateless libraries - a layer beneath the main Grafana frontend and backend without dependencies on it (no storage, no API, no wire, etc.). This lack of dependencies, and their Apache v2 licensing, allow their use as libraries for external tools.
## Extending kindsys
Extending the kind system generally involves:
* Introducing one or more new fields into the CUE framework
* Updating the Go framework to accommodate the new fields
* Updating the kind authoring and maturity planning docs to reflect the new extension
* (possibly) Writing one or more new code generators
* (possibly) Writing/refactoring some frontend code that depends on new codegen output
* (possibly) Writing/refactoring some backend code that depends on codegen output and/or the Go kind framework
* (possibly) Tweaking all existing kinds as-needed to accommodate the new extension
_TODO detailed guide to the above steps_
The above steps certainly aren't trivial. But they all come only after figuring out a way to solve the problem you want to solve in terms of the kind system and code generation in the first place.
_TODO brief guide on how to think in codegen_
## Extensions not involving kind metadata
While the main path for extending kindsys is through adding metadata, there are some other ways of extending kindsys.
### CUE attributes
[CUE attributes](https://cuelang.org/docs/references/spec/#attributes) provide additional information to kind tooling. They are suitable when it is necessary for a schema author to express additional information about a particular field or definition within a schema, without actually modifying the meaning of the schema. Two such known patterns are:
* Controlling nuanced behavior of code generators for some field or type. Example: [@cuetsy](https://github.com/grafana/cuetsy#usage) attributes, which govern TS output
* Expressing some kind of structured TODO or WIP information on a field or type that can be easily analyzed and fed into other systems. Example: a kind marked at least `stable` maturity may not have any `@grafanamaturity` attributes
In both of these cases, attributes are a tool _for the individual kind author_ to convey something to downstream consumers of kind declarations. It is essential. While attributes allow consistency in _how_ a particular task is accomplished, they leave _when_ to apply the rule up to the judgment of the kind author.
Attributes occupy an awkward middle ground. They are more challenging to implement than standard kind framework properties, and less consistent than general codegen transformers while still imposing a cognitive burden on kind authors. They should be the last tool you reach for - but may be the only tool available when field-level schema metadata is required.
TODO create a general pattern for self-contained attribute parser/validators to follow
### Codegen transformers
TODO actually write this - use `Uid`->`UID` as example

35
pkg/kindsys/errors.go Normal file
View File

@ -0,0 +1,35 @@
package kindsys
import "errors"
// TODO consider rewriting with https://github.com/cockroachdb/errors
var (
// ErrValueNotExist indicates that a necessary CUE value did not exist.
ErrValueNotExist = errors.New("cue value does not exist")
// ErrValueNotAKind indicates that a provided CUE value is not any variety of
// Interface. This is almost always an end-user error - they oops'd and provided the
// wrong path, file, etc.
ErrValueNotAKind = errors.New("not a kind")
)
func ewrap(actual, is error) error {
return &errPassthrough{
actual: actual,
is: is,
}
}
type errPassthrough struct {
actual error
is error
}
func (e *errPassthrough) Is(err error) bool {
return errors.Is(err, e.actual) || errors.Is(err, e.is)
}
func (e *errPassthrough) Error() string {
return e.actual.Error()
}

82
pkg/kindsys/kind.go Normal file
View File

@ -0,0 +1,82 @@
package kindsys
import (
"fmt"
"github.com/grafana/thema"
)
// TODO docs
type Maturity string
const (
MaturityMerged Maturity = "merged"
MaturityExperimental Maturity = "experimental"
MaturityStable Maturity = "stable"
MaturityMature Maturity = "mature"
)
func maturityIdx(m Maturity) int {
// icky to do this globally, this is effectively setting a default
if string(m) == "" {
m = MaturityMerged
}
for i, ms := range maturityOrder {
if m == ms {
return i
}
}
panic(fmt.Sprintf("unknown maturity milestone %s", m))
}
var maturityOrder = []Maturity{
MaturityMerged,
MaturityExperimental,
MaturityStable,
MaturityMature,
}
func (m Maturity) Less(om Maturity) bool {
return maturityIdx(m) < maturityIdx(om)
}
// TODO docs
type Interface interface {
// TODO docs
Name() string
// TODO docs
MachineName() string
// TODO docs
Maturity() Maturity // TODO unclear if we want maturity for raw kinds
}
// TODO docs
type Raw interface {
Interface
// TODO docs
Meta() RawMeta
}
type Structured interface {
Interface
// TODO docs
Lineage() thema.Lineage
// TODO docs
Meta() CoreStructuredMeta // TODO figure out how to reconcile this interface with CustomStructuredMeta
}
// type Composable interface {
// Interface
//
// // TODO docs
// Lineage() thema.Lineage
//
// // TODO docs
// Meta() CoreStructuredMeta // TODO figure out how to reconcile this interface with CustomStructuredMeta
// }

166
pkg/kindsys/kindcats.cue Normal file
View File

@ -0,0 +1,166 @@
package kindsys
import (
"strings"
"github.com/grafana/thema"
)
// A Kind specifies a type of Grafana resource.
//
// An instance of a Kind is called an entity. An entity is a sequence of bytes -
// for example, a JSON file or HTTP request body - that conforms to the
// constraints defined in a Kind, and enforced by Grafana's entity system.
//
// Once Grafana has determined a given byte sequence to be an
// instance of a known Kind, kind-specific behaviors can be applied,
// requests can be routed, events can be triggered, etc.
//
// Classes and objects in most programming languages are analogous:
// - #Kind is like a `class` keyword
// - Each declaration of #Kind is like a class declaration
// - Byte sequences are like arguments to the class constructor
// - Entities are like objects - what's returned from the constructor
//
// There are four categories of kinds: Raw, Composable, CoreStructured,
// and CustomStructured.
#Kind: #Raw | #Composable | #CoreStructured | #CustomStructured
// properties shared between all kind categories.
_sharedKind: {
// name is the canonical name of a Kind, as expressed in PascalCase.
//
// To ensure names are generally portable and amenable for consumption
// in various mechanical tasks, name largely follows the relatively
// strict DNS label naming standard as defined in RFC 1123:
// - Contain at most 63 characters
// - Contain only lowercase alphanumeric characters or '-'
// - Start with an uppercase alphabetic character
// - End with an alphanumeric character
name: =~"^([A-Z][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])$"
// machineName is the case-normalized (lowercase) version of [name]. This
// version of the name is preferred for use in most mechanical contexts,
// as case normalization ensures that case-insensitive and case-sensitive
// checks will never disagree on uniqueness.
//
// In addition to lowercase normalization, dashes are transformed to underscores.
machineName: strings.ToLower(strings.Replace(name, "-", "_", -1))
// pluralName is the pluralized form of name. Defaults to name + "s".
pluralName: =~"^([A-Z][a-zA-Z0-9-]{0,61}[a-zA-Z])$" | *(name + "s")
// pluralMachineName is the pluralized form of [machineName]. The same case
// normalization and dash transformation is applied to [pluralName] as [machineName]
// applies to [name].
pluralMachineName: strings.ToLower(strings.Replace(pluralName, "-", "_", -1))
// lineageIsGroup indicates whether the lineage in this kind is "grouped". In a
// grouped lineage, each top-level field in the schema specifies a discrete
// object that is expected to exist in the wild
//
// This field is set at the framework level, and cannot be in the declaration of
// any individual kind.
//
// This is likely to eventually become a first-class property in Thema:
// https://github.com/grafana/thema/issues/62
lineageIsGroup: bool
maturity: #Maturity
// The kind system itself is not mature enough yet for any single
// kind to advance beyond "experimental"
// TODO allow more maturity stages once system is ready https://github.com/orgs/grafana/projects/133/views/8
maturity: *"merged" | "experimental"
// form indicates whether the kind has a schema ("structured") or not ("raw")
form: "structured" | "raw"
}
// Maturity indicates the how far a given kind declaration is in its initial
// journey. Mature kinds still evolve, but with guarantees about compatibility.
#Maturity: "merged" | "experimental" | "stable" | "mature"
// Structured encompasses all three of the structured kind categories, in which
// a schema specifies validity rules for the byte sequence. These represent all
// the conventional types and functional resources in Grafana, such as
// dashboards and datasources.
//
// Structured kinds may be defined either by Grafana itself (#CoreStructured),
// or by plugins (#CustomStructured). Plugin-defined kinds have a slightly
// reduced set of capabilities, due to the constraints imposed by them being run
// in separate processes, and the risks arising from executing code from
// potentially untrusted third parties.
#Structured: S={
_sharedKind
form: "structured"
// lineage is the Thema lineage containing all the schemas that have existed for this kind.
// It is required that lineage.name is the same as the [machineName].
lineage: thema.#Lineage & { name: S.machineName }
currentVersion: thema.#SyntacticVersion & (thema.#LatestVersion & {lin: lineage}).out
}
// Raw is a category of Kind that specifies handling for a raw file,
// like an image, or an svg or parquet file. Grafana mostly acts as asset storage for raw
// kinds: the byte sequence is a black box to Grafana, and type is determined
// through metadata such as file extension.
#Raw: {
_sharedKind
form: "raw"
// TODO docs
extensions?: [...string]
lineageIsGroup: false
// known TODOs
// - sanitize function
// - get summary
}
// TODO
#CustomStructured: {
#Structured
lineageIsGroup: false
...
}
// TODO
#CoreStructured: {
#Structured
lineageIsGroup: false
}
// Composable is a category of structured kind that provides schema elements for
// composition into CoreStructured and CustomStructured kinds. Grafana plugins
// provide composable kinds; for example, a datasource plugin provides one to
// describe the structure of its queries, which is then composed into dashboards
// and alerting rules.
//
// Each Composable is an implementation of exactly one Slot, a shared meta-schema
// defined by Grafana itself that constrains the shape of schemas declared in
// that ComposableKind.
#Composable: S={
_sharedKind
form: "structured"
// TODO docs
// TODO unify this with the existing slots decls in pkg/framework/coremodel
slot: "Panel" | "Query" | "DSConfig"
// TODO unify this with the existing slots decls in pkg/framework/coremodel
lineageIsGroup: bool & [
if slot == "Panel" { true },
if slot == "DSConfig" { true },
if slot == "Query" { false },
][0]
// lineage is the Thema lineage containing all the schemas that have existed for this kind.
// It is required that lineage.name is the same as the [machineName].
lineage: thema.#Lineage & { name: S.machineName }
}

74
pkg/kindsys/kindmetas.go Normal file
View File

@ -0,0 +1,74 @@
package kindsys
import "github.com/grafana/thema"
// CommonMeta contains the kind metadata common to all categories of kinds.
type CommonMeta struct {
Name string `json:"name"`
PluralName string `json:"pluralName"`
MachineName string `json:"machineName"`
PluralMachineName string `json:"pluralMachineName"`
LineageIsGroup bool `json:"lineageIsGroup"`
Maturity Maturity `json:"maturity"`
}
// TODO generate from type.cue
type RawMeta struct {
CommonMeta
Extensions []string `json:"extensions"`
}
func (m RawMeta) _private() {}
func (m RawMeta) Common() CommonMeta {
return m.CommonMeta
}
// TODO
type CoreStructuredMeta struct {
CommonMeta
CurrentVersion thema.SyntacticVersion `json:"currentVersion"`
}
func (m CoreStructuredMeta) _private() {}
func (m CoreStructuredMeta) Common() CommonMeta {
return m.CommonMeta
}
// TODO
type CustomStructuredMeta struct {
CommonMeta
CurrentVersion thema.SyntacticVersion `json:"currentVersion"`
}
func (m CustomStructuredMeta) _private() {}
func (m CustomStructuredMeta) Common() CommonMeta {
return m.CommonMeta
}
// TODO
type ComposableMeta struct {
CommonMeta
CurrentVersion thema.SyntacticVersion `json:"currentVersion"`
}
func (m ComposableMeta) _private() {}
func (m ComposableMeta) Common() CommonMeta {
return m.CommonMeta
}
// SomeKindMeta is an interface type to abstract over the different kind
// metadata struct types: [RawMeta], [CoreStructuredMeta],
// [CustomStructuredMeta].
//
// It is the traditional interface counterpart to the generic type constraint
// KindMetas.
type SomeKindMeta interface {
_private()
Common() CommonMeta
}
// KindMetas is a type parameter that comprises the base possible set of
// kind metadata configurations.
type KindMetas interface {
RawMeta | CoreStructuredMeta | CustomStructuredMeta | ComposableMeta
}

242
pkg/kindsys/load.go Normal file
View File

@ -0,0 +1,242 @@
package kindsys
import (
"fmt"
"io/fs"
"path/filepath"
"sync"
"cuelang.org/go/cue"
"cuelang.org/go/cue/errors"
"github.com/grafana/grafana"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/thema"
tload "github.com/grafana/thema/load"
)
// CoreStructuredDeclParentPath is the path, relative to the repository root, where
// each child directory is expected to contain .cue files declaring one
// CoreStructured kind.
var CoreStructuredDeclParentPath = filepath.Join("kinds", "structured")
// RawDeclParentPath is the path, relative to the repository root, where each child
// directory is expected to contain .cue files declaring one Raw kind.
var RawDeclParentPath = filepath.Join("kinds", "raw")
// GoCoreKindParentPath is the path, relative to the repository root, to the directory
// containing one directory per kind, full of generated Go kind output: types and bindings.
var GoCoreKindParentPath = filepath.Join("pkg", "kinds")
// TSCoreKindParentPath is the path, relative to the repository root, to the directory that
// contains one directory per kind, full of generated TS kind output: types and default consts.
var TSCoreKindParentPath = filepath.Join("packages", "grafana-schema", "src", "raw")
var defaultFramework cue.Value
var fwOnce sync.Once
func init() {
loadpFrameworkOnce()
}
func loadpFrameworkOnce() {
fwOnce.Do(func() {
var err error
defaultFramework, err = doLoadFrameworkCUE(cuectx.GrafanaCUEContext())
if err != nil {
panic(err)
}
})
}
var prefix = filepath.Join("/pkg", "kindsys")
func doLoadFrameworkCUE(ctx *cue.Context) (cue.Value, error) {
var v cue.Value
var err error
absolutePath := prefix
if !filepath.IsAbs(absolutePath) {
absolutePath, err = filepath.Abs(absolutePath)
if err != nil {
return v, err
}
}
bi, err := tload.InstancesWithThema(grafana.CueSchemaFS, absolutePath)
if err != nil {
return v, err
}
v = ctx.BuildInstance(bi)
if err = v.Validate(cue.Concrete(false), cue.All()); err != nil {
return cue.Value{}, fmt.Errorf("coremodel framework loaded cue.Value has err: %w", err)
}
return v, nil
}
// CUEFramework returns a cue.Value representing all the kind framework
// raw CUE files.
//
// For low-level use in constructing other types and APIs, while still letting
// us declare all the frameworky CUE bits in a single package. Other Go types
// make the constructs in this value easy to use.
//
// All calling code within grafana/grafana is expected to use Grafana's
// singleton [cue.Context], returned from [cuectx.GrafanaCUEContext]. If nil
// is passed, the singleton will be used.
func CUEFramework(ctx *cue.Context) cue.Value {
if ctx == nil || ctx == cuectx.GrafanaCUEContext() {
// Ensure framework is loaded, even if this func is called
// from an init() somewhere.
loadpFrameworkOnce()
return defaultFramework
}
// Error guaranteed to be nil here because erroring would have caused init() to panic
v, _ := doLoadFrameworkCUE(ctx) // nolint:errcheck
return v
}
// ToKindMeta takes a cue.Value expected to represent a kind of the category
// specified by the type parameter and populates the Go type from the cue.Value.
func ToKindMeta[T KindMetas](v cue.Value) (T, error) {
meta := new(T)
if !v.Exists() {
return *meta, ErrValueNotExist
}
fw := CUEFramework(v.Context())
var kdef cue.Value
anymeta := any(*meta).(SomeKindMeta)
switch anymeta.(type) {
case RawMeta:
kdef = fw.LookupPath(cue.MakePath(cue.Def("Raw")))
case CoreStructuredMeta:
kdef = fw.LookupPath(cue.MakePath(cue.Def("CoreStructured")))
case CustomStructuredMeta:
kdef = fw.LookupPath(cue.MakePath(cue.Def("CustomStructured")))
case ComposableMeta:
kdef = fw.LookupPath(cue.MakePath(cue.Def("Composable")))
default:
// unreachable so long as all the possibilities in KindMetas have switch branches
panic("unreachable")
}
item := v.Unify(kdef)
if err := item.Validate(cue.Concrete(false), cue.All()); err != nil {
return *meta, ewrap(item.Err(), ErrValueNotAKind)
}
if err := item.Decode(meta); err != nil {
// Should only be reachable if CUE and Go framework types have diverged
panic(errors.Details(err, nil))
}
return *meta, nil
}
// SomeDecl represents a single kind declaration, having been loaded
// and validated by a func such as [LoadCoreKind].
//
// The underlying type of the Meta field indicates the category of
// kind.
type SomeDecl struct {
// V is the cue.Value containing the entire Kind declaration.
V cue.Value
// Meta contains the kind's metadata settings.
Meta SomeKindMeta
}
// BindKindLineage binds the lineage for the kind declaration. nil, nil is returned
// for raw kinds.
//
// For kinds with a corresponding Go type, it is left to the caller to associate
// that Go type with the lineage returned from this function by a call to [thema.BindType].
func (decl *SomeDecl) BindKindLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
if rt == nil {
rt = cuectx.GrafanaThemaRuntime()
}
switch decl.Meta.(type) {
case RawMeta:
return nil, nil
case CoreStructuredMeta, CustomStructuredMeta, ComposableMeta:
return thema.BindLineage(decl.V.LookupPath(cue.MakePath(cue.Str("lineage"))), rt, opts...)
default:
panic("unreachable")
}
}
// IsRaw indicates whether the represented kind is a raw kind.
func (decl *SomeDecl) IsRaw() bool {
_, is := decl.Meta.(RawMeta)
return is
}
// IsCoreStructured indicates whether the represented kind is a core structured kind.
func (decl *SomeDecl) IsCoreStructured() bool {
_, is := decl.Meta.(CoreStructuredMeta)
return is
}
// IsCustomStructured indicates whether the represented kind is a custom structured kind.
func (decl *SomeDecl) IsCustomStructured() bool {
_, is := decl.Meta.(CustomStructuredMeta)
return is
}
// IsComposable indicates whether the represented kind is a composable kind.
func (decl *SomeDecl) IsComposable() bool {
_, is := decl.Meta.(ComposableMeta)
return is
}
// Decl represents a single kind declaration, having been loaded
// and validated by a func such as [LoadCoreKind].
//
// Its type parameter indicates the category of kind.
type Decl[T KindMetas] struct {
// V is the cue.Value containing the entire Kind declaration.
V cue.Value
// Meta contains the kind's metadata settings.
Meta T
}
// Some converts the typed Decl to the equivalent typeless SomeDecl.
func (decl *Decl[T]) Some() *SomeDecl {
return &SomeDecl{
V: decl.V,
Meta: any(decl.Meta).(SomeKindMeta),
}
}
// LoadCoreKind loads and validates a core kind declaration of the kind category
// indicated by the type parameter. On success, it returns a [Decl] which
// contains the entire contents of the kind declaration.
//
// declpath is the path to the directory containing the core kind declaration,
// relative to the grafana/grafana root. For example, dashboards are in
// "kinds/structured/dashboard".
//
// The .cue file bytes containing the core kind declaration will be retrieved
// from the central embedded FS, [grafana.CueSchemaFS]. If desired (e.g. for
// testing), an optional fs.FS may be provided via the overlay parameter, which
// will be merged over [grafana.CueSchemaFS]. But in typical circumstances,
// overlay can and should be nil.
//
// This is a low-level function, primarily intended for use in code generation.
// For representations of core kinds that are useful in Go programs at runtime,
// see ["github.com/grafana/grafana/pkg/registry/corekind"].
func LoadCoreKind[T RawMeta | CoreStructuredMeta](declpath string, ctx *cue.Context, overlay fs.FS) (*Decl[T], error) {
vk, err := cuectx.BuildGrafanaInstance(declpath, "kind", ctx, overlay)
if err != nil {
return nil, err
}
decl := &Decl[T]{
V: vk,
}
decl.Meta, err = ToKindMeta[T](vk)
if err != nil {
return nil, err
}
return decl, nil
}

View File

@ -0,0 +1,76 @@
package corekind
import (
"sync"
"github.com/google/wire"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/kindsys"
"github.com/grafana/thema"
)
// KindSet contains all of the wire-style providers related to kinds.
var KindSet = wire.NewSet(
NewBase,
)
var (
baseOnce sync.Once
defaultBase *Base
)
// NewBase provides a registry of all core raw and structured kinds, without any
// composition of slot kinds.
//
// All calling code within grafana/grafana is expected to use Grafana's
// singleton [thema.Runtime], returned from [cuectx.GrafanaThemaRuntime]. If nil
// is passed, the singleton will be used.
func NewBase(rt *thema.Runtime) *Base {
allrt := cuectx.GrafanaThemaRuntime()
if rt == nil || rt == allrt {
baseOnce.Do(func() {
defaultBase = doNewBase(allrt)
})
return defaultBase
}
return doNewBase(rt)
}
// All returns a slice of the [kindsys.Interface] instances corresponding to all
// core raw and structured kinds.
//
// The returned slice is sorted lexicographically by kind machine name.
func (b *Base) All() []kindsys.Interface {
ret := make([]kindsys.Interface, len(b.all))
copy(ret, b.all)
return ret
}
// AllRaw returns a slice of the [kindsys.Raw] instances for all raw kinds.
//
// The returned slice is sorted lexicographically by kind machine name.
func (b *Base) AllRaw() []kindsys.Raw {
ret := make([]kindsys.Raw, 0, b.numRaw)
for _, k := range b.all {
if rk, is := k.(kindsys.Raw); is {
ret = append(ret, rk)
}
}
return ret
}
// AllStructured returns a slice of the [kindsys.Structured] instances for
// all core structured kinds.
//
// The returned slice is sorted lexicographically by kind machine name.
func (b *Base) AllStructured() []kindsys.Structured {
ret := make([]kindsys.Structured, 0, b.numStructured)
for _, k := range b.all {
if rk, is := k.(kindsys.Structured); is {
ret = append(ret, rk)
}
}
return ret
}

View File

@ -0,0 +1,88 @@
// THIS FILE IS GENERATED. EDITING IS FUTILE.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// BaseCoreRegistryJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package corekind
import (
"fmt"
"github.com/grafana/grafana/pkg/kinds/dashboard"
"github.com/grafana/grafana/pkg/kinds/playlist"
"github.com/grafana/grafana/pkg/kinds/svg"
"github.com/grafana/grafana/pkg/kindsys"
"github.com/grafana/thema"
)
// Base is a registry of kindsys.Interface. It provides two modes for accessing
// kinds: individually via literal named methods, or as a slice returned from
// an All*() method.
//
// Prefer the individual named methods for use cases where the particular kind(s) that
// are needed are known to the caller. For example, a dashboard linter can know that it
// specifically wants the dashboard kind.
//
// Prefer All*() methods when performing operations generically across all kinds.
// For example, a validation HTTP middleware for any kind-schematized object type.
type Base struct {
all []kindsys.Interface
numRaw, numStructured int
dashboard *dashboard.Kind
playlist *playlist.Kind
svg *svg.Kind
}
// type guards
var (
_ kindsys.Structured = &dashboard.Kind{}
_ kindsys.Structured = &playlist.Kind{}
_ kindsys.Raw = &svg.Kind{}
)
// Dashboard returns the [kindsys.Interface] implementation for the dashboard kind.
func (b *Base) Dashboard() *dashboard.Kind {
return b.dashboard
}
// Playlist returns the [kindsys.Interface] implementation for the playlist kind.
func (b *Base) Playlist() *playlist.Kind {
return b.playlist
}
// SVG returns the [kindsys.Interface] implementation for the svg kind.
func (b *Base) SVG() *svg.Kind {
return b.svg
}
func doNewBase(rt *thema.Runtime) *Base {
var err error
reg := &Base{
numRaw: 1,
numStructured: 2,
}
reg.dashboard, err = dashboard.NewKind(rt)
if err != nil {
panic(fmt.Sprintf("error while initializing the dashboard Kind: %s", err))
}
reg.all = append(reg.all, reg.dashboard)
reg.playlist, err = playlist.NewKind(rt)
if err != nil {
panic(fmt.Sprintf("error while initializing the playlist Kind: %s", err))
}
reg.all = append(reg.all, reg.playlist)
reg.svg, err = svg.NewKind()
if err != nil {
panic(fmt.Sprintf("error while initializing the svg Kind: %s", err))
}
reg.all = append(reg.all, reg.svg)
return reg
}

View File

@ -12,7 +12,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/expr"
cmreg "github.com/grafana/grafana/pkg/framework/coremodel/registry"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider"
@ -41,6 +40,7 @@ import (
managerStore "github.com/grafana/grafana/pkg/plugins/manager/store"
"github.com/grafana/grafana/pkg/plugins/plugincontext"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/registry/corekind"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
@ -334,7 +334,7 @@ var wireBasicSet = wire.NewSet(
avatar.ProvideAvatarCacheServer,
authproxy.ProvideAuthProxy,
statscollector.ProvideService,
cmreg.CoremodelSet,
corekind.KindSet,
cuectx.GrafanaCUEContext,
cuectx.GrafanaThemaRuntime,
csrf.ProvideCSRFFilter,

View File

@ -3,7 +3,7 @@ package playlist
import (
"errors"
"github.com/grafana/grafana/pkg/coremodel/playlist"
"github.com/grafana/grafana/pkg/kinds/playlist"
)
// Typed errors
@ -22,7 +22,7 @@ type Playlist struct {
OrgId int64 `json:"-" db:"org_id"`
}
type PlaylistDTO = playlist.Model
type PlaylistDTO = playlist.Playlist
type PlaylistItemDTO = playlist.PlaylistItem
type PlaylistItemType = playlist.PlaylistItemType

View File

@ -5,7 +5,7 @@ import (
"strconv"
"time"
"github.com/grafana/grafana/pkg/coremodel/dashboard"
"github.com/grafana/grafana/pkg/kinds/dashboard"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
)

View File

@ -8,9 +8,9 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/components/simplejson"
dashboard2 "github.com/grafana/grafana/pkg/coremodel/dashboard"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
dashboard2 "github.com/grafana/grafana/pkg/kinds/dashboard"
grafanamodels "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/annotations/annotationsimpl"
@ -1024,11 +1024,11 @@ func TestBuildAnonymousUser(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
//publicdashboardStore := database.ProvideStore(sqlStore)
//service := &PublicDashboardServiceImpl{
// publicdashboardStore := database.ProvideStore(sqlStore)
// service := &PublicDashboardServiceImpl{
// log: log.New("test.logger"),
// store: publicdashboardStore,
//}
// }
t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) {
user := buildAnonymousUser(context.Background(), dashboard)

View File

@ -5,7 +5,7 @@ import (
"encoding/json"
"fmt"
"github.com/grafana/grafana/pkg/coremodel/playlist"
"github.com/grafana/grafana/pkg/kinds/playlist"
"github.com/grafana/grafana/pkg/models"
)
@ -22,7 +22,7 @@ func GetObjectSummaryBuilder() models.ObjectSummaryBuilder {
}
func summaryBuilder(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
obj := &playlist.Model{}
obj := &playlist.Playlist{}
err := json.Unmarshal(body, obj)
if err != nil {
return nil, nil, err // unable to read object

View File

@ -5,7 +5,7 @@ import (
"encoding/json"
"testing"
"github.com/grafana/grafana/pkg/coremodel/playlist"
"github.com/grafana/grafana/pkg/kinds/playlist"
"github.com/stretchr/testify/require"
)
@ -16,7 +16,7 @@ func TestPlaylistSummary(t *testing.T) {
_, _, err := builder(context.Background(), "abc", []byte("{invalid json"))
require.Error(t, err)
playlist := playlist.Model{
playlist := playlist.Playlist{
Interval: "30s",
Name: "test",
Items: &[]playlist.PlaylistItem{

View File

@ -1,4 +1,4 @@
import { PlaylistItem as PlaylistItemFromSchema } from '@grafana/schema/src/raw/playlist/x/playlist.gen';
import { PlaylistItem as PlaylistItemFromSchema } from '@grafana/schema';
import { DashboardQueryResult } from '../search/service';

View File

@ -32,7 +32,6 @@ var skipPlugins = map[string]bool{
const sep = string(filepath.Separator)
// Generate TypeScript for all plugin models.cue
func main() {
if len(os.Args) > 1 {
fmt.Fprintf(os.Stderr, "plugin thema code generator does not currently accept any arguments\n, got %q", os.Args)