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

199 lines
4.5 KiB

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