mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			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...)
 | 
						|
	}
 | 
						|
}
 |