-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Daniel Martí <[email protected]> Change-Id: Ifd21c87196643fdb44900f9258493b67d614e11a Dispatch-Trailer: {"type":"trybot","CL":1206583,"patchset":1,"ref":"refs/changes/83/1206583/1","targetBranch":"master"}
- Loading branch information
Showing
3 changed files
with
507 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
// Copyright 2024 CUE Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
"go/format" | ||
"os" | ||
"path/filepath" | ||
"sort" | ||
"strings" | ||
|
||
"cuelang.org/go/cue" | ||
"cuelang.org/go/cue/ast" | ||
"cuelang.org/go/cue/build" | ||
"cuelang.org/go/cue/load" | ||
"cuelang.org/go/mod/module" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
func newExpCmd(c *Command) *cobra.Command { | ||
cmd := &cobra.Command{ | ||
// Experimental commands are hidden by design. | ||
Hidden: true, | ||
|
||
Use: "exp <cmd> [arguments]", | ||
Short: "experimental commands", | ||
Long: `exp groups commands which are still in an experimental stage. | ||
Experimental commands may be changed or removed at any time, | ||
as the objective tends to be to gain experience and then move the feature elsewhere. | ||
`, | ||
RunE: mkRunE(c, func(cmd *Command, args []string) error { | ||
stderr := cmd.Stderr() | ||
if len(args) == 0 { | ||
fmt.Fprintln(stderr, "exp must be run as one of its subcommands") | ||
} else { | ||
fmt.Fprintf(stderr, "exp must be run as one of its subcommands: unknown subcommand %q\n", args[0]) | ||
} | ||
fmt.Fprintln(stderr, "Run 'cue help exp' for known subcommands.") | ||
return ErrPrintedError | ||
}), | ||
} | ||
|
||
cmd.AddCommand(newExpGenGoTypesCmd(c)) | ||
return cmd | ||
} | ||
|
||
func newExpGenGoTypesCmd(c *Command) *cobra.Command { | ||
cmd := &cobra.Command{ | ||
Use: "gengotypes", | ||
Short: "generate Go types from CUE definitions", | ||
Long: `XXX write a summary once the feature set is finished | ||
`, | ||
RunE: mkRunE(c, runExpGenGoTypes), | ||
} | ||
|
||
return cmd | ||
} | ||
|
||
func runExpGenGoTypes(cmd *Command, args []string) error { | ||
insts := load.Instances(args, &load.Config{}) | ||
done := make(map[*build.Instance]bool) | ||
for len(insts) > 0 { // we append imports to this list | ||
inst := insts[0] | ||
insts = insts[1:] | ||
if err := inst.Err; err != nil { | ||
return err | ||
} | ||
if done[inst] { | ||
continue | ||
} | ||
println(inst.ImportPath) | ||
done[inst] = true | ||
|
||
val := cmd.ctx.BuildInstance(inst) | ||
if err := val.Validate(); err != nil { | ||
return err | ||
} | ||
iter, err := val.Fields(cue.Definitions(true)) | ||
if err != nil { | ||
return err | ||
} | ||
p := printer{ | ||
pkgRoot: val, | ||
importedAs: make(map[string]string), | ||
} | ||
for iter.Next() { | ||
sel := iter.Selector() | ||
if !sel.IsDefinition() { | ||
continue | ||
} | ||
// TODO(mvdan): surely there is a better way to handle definition hashes. | ||
name := strings.TrimPrefix(sel.String(), "#") | ||
val := iter.Value() | ||
p.emitDocs(name, val.Doc()) | ||
p.appendf("type %s ", name) | ||
if err := p.emitType(val); err != nil { | ||
return err | ||
} | ||
p.appendf("\n\n") | ||
} | ||
|
||
typesBuf := p.dst | ||
p.dst = nil | ||
|
||
// TODO(mvdan): we should refuse to generate for packages which are not | ||
// part of the main module, as they may be inside the read-only module cache. | ||
for _, imp := range inst.Imports { | ||
if !done[imp] && p.importedAs[imp.ImportPath] != "" { | ||
insts = append(insts, imp) | ||
} | ||
} | ||
|
||
p.appendf("// Code generated by \"cue exp gengotypes\"; DO NOT EDIT.\n\n") | ||
p.appendf("package %s\n\n", inst.PkgName) | ||
// TODO(mvdan): maps.Keys etc | ||
var imported []string | ||
for _, path := range p.importedAs { | ||
imported = append(imported, path) | ||
} | ||
// TODO: need to remove dupes here too | ||
sort.Strings(imported) | ||
if len(imported) > 0 { | ||
p.appendf("import (\n") | ||
for _, path := range imported { | ||
p.appendf("\t%q\n", path) | ||
} | ||
p.appendf(")\n") | ||
} | ||
p.appendf("%s", typesBuf) | ||
formatted, err := format.Source(p.dst) | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "-- %s/cue_gen.go --\n%s\n--\n", inst.Dir, p.dst) | ||
return err | ||
} | ||
outfile := filepath.Join(inst.Dir, "cue_gen.go") | ||
if err := os.WriteFile(outfile, formatted, 0o666); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
type printer struct { | ||
dst []byte | ||
|
||
// importedAs records which packages need to be importedAs in the generated Go package. | ||
// This is collected as we emit types, given that some CUE fields and types are omitted | ||
// and we don't want to end up with unused Go imports. | ||
// | ||
// The keys are the full CUE import paths; the values are the unqualified Go import paths. | ||
importedAs map[string]string | ||
|
||
// pkgRoot is the root value of the CUE package, necessary to tell if a referenced value | ||
// belongs to the current package or not. | ||
pkgRoot cue.Value | ||
} | ||
|
||
func (p *printer) appendf(format string, args ...any) { | ||
p.dst = fmt.Appendf(p.dst, format, args...) | ||
} | ||
|
||
func (p *printer) emitType(val cue.Value) error { | ||
// References to existing names, either from the same package or an imported package. | ||
// TODO(mvdan): surely there is a better way to check whether ReferencePath returned "no path", | ||
// such as a possible path.IsValid method? | ||
if root, path := val.ReferencePath(); len(path.Selectors()) > 0 { | ||
if root != p.pkgRoot { | ||
inst := root.BuildInstance() | ||
|
||
// Go has no notion of qualified import paths; if a CUE file imports | ||
// "foo.com/bar:qualified", we import just "foo.com/bar" on the Go side. | ||
// TODO(mvdan): deal with multiple packages existing in the same directory. | ||
parts := module.ParseImportPath(inst.ImportPath) | ||
p.importedAs[inst.ImportPath] = parts.Unqualified().String() | ||
|
||
p.appendf("%s.", inst.PkgName) | ||
} | ||
// TODO(mvdan): surely there is a better way to handle definition hashes. | ||
p.appendf("%s", strings.ReplaceAll(path.String(), "#", "")) | ||
return nil | ||
} | ||
// Inline types. | ||
switch k := val.IncompleteKind(); k { | ||
case cue.StructKind: | ||
if elem := val.LookupPath(cue.MakePath(cue.AnyString)); elem.Err() == nil { | ||
p.appendf("map[string]") | ||
if err := p.emitType(elem); err != nil { | ||
return err | ||
} | ||
break | ||
} | ||
// TODO: treat a single embedding like `{[string]: int}` like we would `[string]: int` | ||
p.appendf("struct {\n") | ||
iter, err := val.Fields(cue.Optional(true)) | ||
if err != nil { | ||
return err | ||
} | ||
for iter.Next() { | ||
sel := iter.Selector() | ||
val := iter.Value() | ||
cueName := sel.Unquoted() | ||
p.emitDocs(cueName, val.Doc()) | ||
optional := sel.ConstraintType()&cue.OptionalConstraint != 0 | ||
|
||
goName := strings.Title(cueName) | ||
goAttr := val.Attribute("go") | ||
if s, _ := goAttr.String(0); s != "" { | ||
goName = s | ||
} | ||
|
||
p.appendf("%s ", goName) | ||
if err := p.emitType(val); err != nil { | ||
return err | ||
} | ||
p.appendf(" `json:\"%s", cueName) | ||
if optional { | ||
p.appendf(",omitempty") | ||
} | ||
p.appendf("\"`") | ||
p.appendf("\n\n") | ||
} | ||
p.appendf("}") | ||
case cue.ListKind: | ||
// We mainly care about patterns like [...string]. | ||
// Anything else can convert into []any as a fallback. | ||
p.appendf("[]") | ||
elem := val.LookupPath(cue.MakePath(cue.AnyIndex)) | ||
if err := p.emitType(elem); err != nil { | ||
return err | ||
} | ||
case cue.StringKind: | ||
p.appendf("string") | ||
case cue.IntKind: | ||
p.appendf("int64") | ||
default: | ||
p.appendf("any /* TODO: IncompleteKind: %s */", k) | ||
} | ||
return nil | ||
} | ||
|
||
func (p *printer) emitDocs(name string, groups []*ast.CommentGroup) { | ||
// TODO(mvdan): place the comment group starting with `// $name ...` first. | ||
for i, group := range groups { | ||
if i > 0 { | ||
p.appendf("//\n") | ||
} | ||
for _, line := range group.List { | ||
p.appendf("%s\n", line.Text) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.