mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
200 lines
4.5 KiB
Go
200 lines
4.5 KiB
Go
|
package generators
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
|
||
|
"cuelang.org/go/cue"
|
||
|
"cuelang.org/go/cue/ast"
|
||
|
"cuelang.org/go/encoding/openapi"
|
||
|
)
|
||
|
|
||
|
type OpenApiConfig struct {
|
||
|
Config *openapi.Config
|
||
|
IsGroup bool
|
||
|
RootName string
|
||
|
SubPath cue.Path
|
||
|
}
|
||
|
|
||
|
func generateOpenAPI(v cue.Value, cfg *OpenApiConfig) (*ast.File, error) {
|
||
|
if cfg == nil {
|
||
|
return nil, fmt.Errorf("missing openapi configuration")
|
||
|
}
|
||
|
|
||
|
if cfg.Config == nil {
|
||
|
cfg.Config = &openapi.Config{}
|
||
|
}
|
||
|
|
||
|
name, err := getSchemaName(v)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
gen := &oapiGen{
|
||
|
cfg: cfg,
|
||
|
name: name,
|
||
|
val: v.LookupPath(cue.ParsePath("lineage.schemas[0].schema")),
|
||
|
subpath: cfg.SubPath,
|
||
|
bpath: v.LookupPath(cue.ParsePath("lineage.schemas[0]")).Path(),
|
||
|
}
|
||
|
|
||
|
declFunc := genSchema
|
||
|
if cfg.IsGroup {
|
||
|
declFunc = genGroup
|
||
|
}
|
||
|
|
||
|
decls, err := declFunc(gen)
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// TODO recursively sort output to improve stability of output
|
||
|
return &ast.File{
|
||
|
Decls: []ast.Decl{
|
||
|
ast.NewStruct(
|
||
|
"openapi", ast.NewString("3.0.0"),
|
||
|
"paths", ast.NewStruct(),
|
||
|
"components", ast.NewStruct(
|
||
|
"schemas", &ast.StructLit{Elts: decls},
|
||
|
),
|
||
|
),
|
||
|
},
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
type oapiGen struct {
|
||
|
cfg *OpenApiConfig
|
||
|
val cue.Value
|
||
|
subpath cue.Path
|
||
|
|
||
|
// overall name for the generated oapi doc
|
||
|
name string
|
||
|
|
||
|
// original NameFunc
|
||
|
onf func(cue.Value, cue.Path) string
|
||
|
|
||
|
// full prefix path that leads up to the #SchemaDef, e.g. lin._sortedSchemas[0]
|
||
|
bpath cue.Path
|
||
|
}
|
||
|
|
||
|
func genGroup(gen *oapiGen) ([]ast.Decl, error) {
|
||
|
ctx := gen.val.Context()
|
||
|
iter, err := gen.val.Fields(cue.Definitions(true), cue.Optional(true))
|
||
|
if err != nil {
|
||
|
panic(fmt.Errorf("unreachable - should always be able to get iter for struct kinds: %w", err))
|
||
|
}
|
||
|
|
||
|
var decls []ast.Decl
|
||
|
for iter.Next() {
|
||
|
val, sel := iter.Value(), iter.Selector()
|
||
|
name := strings.Trim(sel.String(), "?#")
|
||
|
|
||
|
v := ctx.CompileString(fmt.Sprintf("#%s: _", name))
|
||
|
defpath := cue.MakePath(cue.Def(name))
|
||
|
defsch := v.FillPath(defpath, val)
|
||
|
|
||
|
cfgi := *gen.cfg.Config
|
||
|
cfgi.NameFunc = func(val cue.Value, path cue.Path) string {
|
||
|
return gen.nfSingle(val, path, defpath, name)
|
||
|
}
|
||
|
|
||
|
part, err := openapi.Generate(defsch, &cfgi)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("failed generation for grouped field %s: %w", sel, err)
|
||
|
}
|
||
|
|
||
|
decls = append(decls, getSchemas(part)...)
|
||
|
}
|
||
|
|
||
|
return decls, nil
|
||
|
}
|
||
|
|
||
|
func genSchema(gen *oapiGen) ([]ast.Decl, error) {
|
||
|
hasSubpath := len(gen.cfg.SubPath.Selectors()) > 0
|
||
|
name := sanitizeLabelString(gen.name)
|
||
|
if gen.cfg.RootName != "" {
|
||
|
name = gen.cfg.RootName
|
||
|
} else if hasSubpath {
|
||
|
sel := gen.cfg.SubPath.Selectors()
|
||
|
name = sel[len(sel)-1].String()
|
||
|
}
|
||
|
|
||
|
val := gen.val
|
||
|
if hasSubpath {
|
||
|
for i, sel := range gen.cfg.SubPath.Selectors() {
|
||
|
if !gen.val.Allows(sel) {
|
||
|
return nil, fmt.Errorf("subpath %q not present in schema", cue.MakePath(gen.cfg.SubPath.Selectors()[:i+1]...))
|
||
|
}
|
||
|
}
|
||
|
val = val.LookupPath(gen.cfg.SubPath)
|
||
|
}
|
||
|
|
||
|
v := gen.val.Context().CompileString(fmt.Sprintf("#%s: _", name))
|
||
|
defpath := cue.MakePath(cue.Def(name))
|
||
|
defsch := v.FillPath(defpath, val)
|
||
|
|
||
|
gen.cfg.Config.NameFunc = func(val cue.Value, path cue.Path) string {
|
||
|
return gen.nfSingle(val, path, defpath, name)
|
||
|
}
|
||
|
|
||
|
f, err := openapi.Generate(defsch.Eval(), gen.cfg.Config)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return getSchemas(f), nil
|
||
|
}
|
||
|
|
||
|
// For generating a single, our NameFunc must:
|
||
|
// - Eliminate any path prefixes on the element, both internal lineage and wrapping
|
||
|
// - Replace the name "_#schema" with the desired name
|
||
|
// - Call the user-provided NameFunc, if any
|
||
|
// - Remove CUE markers like #, !, ?
|
||
|
func (gen *oapiGen) nfSingle(val cue.Value, path, defpath cue.Path, name string) string {
|
||
|
tpath := trimPathPrefix(trimThemaPathPrefix(path, gen.bpath), defpath)
|
||
|
|
||
|
if path.String() == "" || tpath.String() == defpath.String() {
|
||
|
return name
|
||
|
}
|
||
|
|
||
|
if val == gen.val {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
if gen.onf != nil {
|
||
|
return gen.onf(val, tpath)
|
||
|
}
|
||
|
return strings.Trim(tpath.String(), "?#")
|
||
|
}
|
||
|
|
||
|
func getSchemas(f *ast.File) []ast.Decl {
|
||
|
compos := orp(getFieldByLabel(f, "components"))
|
||
|
schemas := orp(getFieldByLabel(compos.Value, "schemas"))
|
||
|
return schemas.Value.(*ast.StructLit).Elts
|
||
|
}
|
||
|
|
||
|
func orp[T any](t T, err error) T {
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
return t
|
||
|
}
|
||
|
|
||
|
func trimThemaPathPrefix(p, base cue.Path) cue.Path {
|
||
|
if !pathHasPrefix(p, base) {
|
||
|
return p
|
||
|
}
|
||
|
|
||
|
rest := p.Selectors()[len(base.Selectors()):]
|
||
|
if len(rest) == 0 {
|
||
|
return cue.Path{}
|
||
|
}
|
||
|
switch rest[0].String() {
|
||
|
case "schema", "_#schema", "_join", "joinSchema":
|
||
|
return cue.MakePath(rest[1:]...)
|
||
|
default:
|
||
|
return cue.MakePath(rest...)
|
||
|
}
|
||
|
}
|