Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace "sandbox" main files by a unified CLI #11

Merged
merged 1 commit into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
117 changes: 117 additions & 0 deletions cmd/cli/generate/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package generate

import (
"context"
"fmt"
"strings"

"github.com/grafana/codejen"
"github.com/grafana/cog/internal/ast"
"github.com/grafana/cog/internal/jennies"
"github.com/spf13/cobra"
)

type options struct {
outputDir string
entrypoints []string
schemasType string

// Cue-specific options
cueImports []string
}

func (opts options) cueIncludeImports() ([]cueIncludeImport, error) {
if len(opts.cueImports) == 0 {
return nil, nil
}

imports := make([]cueIncludeImport, len(opts.cueImports))
for i, importDefinition := range opts.cueImports {
parts := strings.Split(importDefinition, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("'%s' is not a valid import definition", importDefinition)
}

imports[i].fsPath = parts[0]
imports[i].importPath = parts[1]
}

return imports, nil
}

func Command() *cobra.Command {
opts := options{}

cmd := &cobra.Command{
Use: "generate",
Short: "Generates code from schemas.", // TODO: better descriptions
Long: `Generates code from schemas.`,
RunE: func(cmd *cobra.Command, args []string) error {
return doGenerate(opts)
},
}

cmd.Flags().StringVarP(&opts.schemasType, "loader", "l", "cue", "Schemas type.") // TODO: better usage text
cmd.Flags().StringVarP(&opts.outputDir, "output", "o", "generated", "Output directory.") // TODO: better usage text
cmd.Flags().StringArrayVarP(&opts.entrypoints, "input", "i", nil, "Schema.") // TODO: better usage text
cmd.Flags().StringArrayVarP(&opts.cueImports, "include-cue-import", "I", nil, "Specify an additional library import directory. Format: [path]:[import]. Example: '../grafana/common-library:github.com/grafana/grafana/packages/grafana-schema/src/common")

_ = cmd.MarkFlagRequired("input")
_ = cmd.MarkFlagDirname("input")
_ = cmd.MarkFlagDirname("output")

return cmd
}

func doGenerate(opts options) error {
loaders := map[string]func(opts options) ([]*ast.File, error){
"cue": cueLoader,
"kindsys-core": kindsysCoreLoader,
"kindsys-custom": kindsysCustomLoader,
"jsonschema": jsonschemaLoader,
}
loader, ok := loaders[opts.schemasType]
if !ok {
return fmt.Errorf("no loader found for '%s'", opts.schemasType)
}

schemas, err := loader(opts)
if err != nil {
return err
}

// Here begins the code generation setup
targetsByLanguage := jennies.All()
rootCodeJenFS := codejen.NewFS()

for language, target := range targetsByLanguage {
fmt.Printf("Running '%s' jennies...\n", language)

var err error
processedAsts := schemas

for _, compilerPass := range target.CompilerPasses {
processedAsts, err = compilerPass.Process(processedAsts)
if err != nil {
return err
}
}

fs, err := target.Jennies.GenerateFS(processedAsts)
if err != nil {
return err
}

err = rootCodeJenFS.Merge(fs)
if err != nil {
return err
}
}

err = rootCodeJenFS.Write(context.Background(), opts.outputDir)
if err != nil {
return err
}

return nil
}
97 changes: 26 additions & 71 deletions sandbox/codegen-cue/main.go → cmd/cli/generate/cue.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main
package generate

import (
"context"
"fmt"
"io"
"io/fs"
Expand All @@ -11,30 +10,24 @@ import (

"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/load"
"github.com/grafana/codejen"
"github.com/grafana/cog/internal/ast"
"github.com/grafana/cog/internal/jennies"
"github.com/grafana/cog/internal/simplecue"
"github.com/yalue/merged_fs"
)

func main() {
entrypoints := []string{
"./schemas/cue/core/dashboard/",
// "./schemas/cue/core/playlist/",

"./schemas/cue/composable/timeseries/",

"github.com/grafana/grafana/packages/grafana-schema/src/common",
}
type cueIncludeImport struct {
fsPath string // path of the library on the filesystem
importPath string // path used in CUE files to import that library
}

cueFsOverlay, err := buildCueOverlay()
func cueLoader(opts options) ([]*ast.File, error) {
cueFsOverlay, err := buildCueOverlay(opts)
if err != nil {
panic(err)
return nil, err
}

allSchemas := make([]*ast.File, 0, len(entrypoints))
for _, entrypoint := range entrypoints {
allSchemas := make([]*ast.File, 0, len(opts.entrypoints))
for _, entrypoint := range opts.entrypoints {
pkg := filepath.Base(entrypoint)

// Load Cue files into Cue build.Instances slice
Expand All @@ -47,90 +40,52 @@ func main() {

values, err := cuecontext.New().BuildInstances(bis)
if err != nil {
panic(err)
return nil, err
}

schemaAst, err := simplecue.GenerateAST(values[0], simplecue.Config{
Package: pkg, // TODO: extract from input schema/?
})
if err != nil {
panic(err)
return nil, err
}

allSchemas = append(allSchemas, schemaAst)
}

// Here begins the code generation setup
targetsByLanguage := jennies.All()
rootCodeJenFS := codejen.NewFS()

for language, target := range targetsByLanguage {
fmt.Printf("Running '%s' jennies...\n", language)

var err error
processedAsts := allSchemas

for _, compilerPass := range target.CompilerPasses {
processedAsts, err = compilerPass.Process(processedAsts)
if err != nil {
panic(err)
}
}

fs, err := target.Jennies.GenerateFS(processedAsts)
if err != nil {
panic(err)
}

err = rootCodeJenFS.Merge(fs)
if err != nil {
panic(err)
}
}

err = rootCodeJenFS.Write(context.Background(), "generated")
if err != nil {
panic(err)
}
return allSchemas, nil
}

func buildCueOverlay() (map[string]load.Source, error) {
libFs, err := buildBaseFSWithLibraries()
func buildCueOverlay(opts options) (map[string]load.Source, error) {
libFs, err := buildBaseFSWithLibraries(opts)
if err != nil {
return nil, err
}

overlay := make(map[string]load.Source)
if err := ToCueOverlay("/", libFs, overlay); err != nil {
if err := toCueOverlay("/", libFs, overlay); err != nil {
return nil, err
}

return overlay, nil
}

func buildBaseFSWithLibraries() (fs.FS, error) {
// TODO: these should be received as inputs/arguments/parameters
importDefinitions := [][2]string{
{
"github.com/grafana/grafana/packages/grafana-schema/src/common",
"../kind-registry/grafana/next/common",
},
{
"github.com/grafana/cog",
".",
},
func buildBaseFSWithLibraries(opts options) (fs.FS, error) {
importDefinitions, err := opts.cueIncludeImports()
if err != nil {
return nil, err
}

var librariesFS []fs.FS
for _, importDefinition := range importDefinitions {
absPath, err := filepath.Abs(importDefinition[1])
absPath, err := filepath.Abs(importDefinition.fsPath)
if err != nil {
return nil, err
}

fmt.Printf("Loading '%s' module from '%s'\n", importDefinition[0], absPath)
fmt.Printf("Loading '%s' module from '%s'\n", importDefinition.importPath, absPath)

libraryFS, err := dirToPrefixedFS(absPath, "cue.mod/pkg/"+importDefinition[0])
libraryFS, err := dirToPrefixedFS(absPath, "cue.mod/pkg/"+importDefinition.importPath)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -164,8 +119,8 @@ func dirToPrefixedFS(directory string, prefix string) (fs.FS, error) {
return commonFS, nil
}

// ToOverlay converts an fs.FS into a CUE loader overlay.
func ToCueOverlay(prefix string, vfs fs.FS, overlay map[string]load.Source) error {
// ToOverlay converts a fs.FS into a CUE loader overlay.
func toCueOverlay(prefix string, vfs fs.FS, overlay map[string]load.Source) error {
// TODO why not just stick the prefix on automatically...?
if !filepath.IsAbs(prefix) {
return fmt.Errorf("must provide absolute path prefix when generating cue overlay, got %q", prefix)
Expand All @@ -183,7 +138,7 @@ func ToCueOverlay(prefix string, vfs fs.FS, overlay map[string]load.Source) erro
if err != nil {
return err
}
defer f.Close() // nolint: errcheck
defer func() { _ = f.Close() }()

b, err := io.ReadAll(f)
if err != nil {
Expand Down
32 changes: 32 additions & 0 deletions cmd/cli/generate/jsonschema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package generate

import (
"os"
"path/filepath"

"github.com/grafana/cog/internal/ast"
"github.com/grafana/cog/internal/jsonschema"
)

func jsonschemaLoader(opts options) ([]*ast.File, error) {
allSchemas := make([]*ast.File, 0, len(opts.entrypoints))
for _, entrypoint := range opts.entrypoints {
pkg := filepath.Base(filepath.Dir(entrypoint))

reader, err := os.Open(entrypoint)
if err != nil {
return nil, err
}

schemaAst, err := jsonschema.GenerateAST(reader, jsonschema.Config{
Package: pkg, // TODO: extract from input schema/folder?
})
if err != nil {
return nil, err
}

allSchemas = append(allSchemas, schemaAst)
}

return allSchemas, nil
}
61 changes: 61 additions & 0 deletions cmd/cli/generate/kindsyscore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package generate

import (
"fmt"
"path/filepath"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"github.com/grafana/cog/internal/ast"
"github.com/grafana/cog/internal/simplecue"
"github.com/grafana/kindsys"
"github.com/grafana/thema"
)

func kindsysCoreLoader(opts options) ([]*ast.File, error) {
themaRuntime := thema.NewRuntime(cuecontext.New())

allSchemas := make([]*ast.File, 0, len(opts.entrypoints))
for _, entrypoint := range opts.entrypoints {
pkg := filepath.Base(entrypoint)

overlayFS, err := dirToPrefixedFS(entrypoint, "")
if err != nil {
return nil, err
}

cueInstance, err := kindsys.BuildInstance(themaRuntime.Context(), ".", "kind", overlayFS)
if err != nil {
return nil, fmt.Errorf("could not load kindsys instance: %w", err)
}

props, err := kindsys.ToKindProps[kindsys.CoreProperties](cueInstance)
if err != nil {
return nil, fmt.Errorf("could not convert cue value to kindsys props: %w", err)
}

kindDefinition := kindsys.Def[kindsys.CoreProperties]{
V: cueInstance,
Properties: props,
}

boundKind, err := kindsys.BindCore(themaRuntime, kindDefinition)
if err != nil {
return nil, fmt.Errorf("could not bind kind definition to kind: %w", err)
}

rawLatestSchemaAsCue := boundKind.Lineage().Latest().Underlying()
latestSchemaAsCue := rawLatestSchemaAsCue.LookupPath(cue.MakePath(cue.Hid("_#schema", "github.com/grafana/thema")))

schemaAst, err := simplecue.GenerateAST(latestSchemaAsCue, simplecue.Config{
Package: pkg, // TODO: extract from input schema/folder?
})
if err != nil {
return nil, err
}

allSchemas = append(allSchemas, schemaAst)
}

return allSchemas, nil
}
Loading