encoding/gocode: add Go code generator
Also:
- add gocodec used by generator
- added ability to create temporary Runtime (for concurrency)
- removed index freezing
The latter was only for precondition checking of
internal code. At the same time, freezing could
lead to spurious races if Runtime is used
concurrently.
Note:
- gocodec is a separate package as it is useful
on its own and that way it can be used without
pulling all the dependencies of gocode.
Change-Id: Ib59b65084038b616c9fb725cbe152a56b8869416
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2742
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/gocode/doc.go b/encoding/gocode/doc.go
new file mode 100644
index 0000000..7bf0af6
--- /dev/null
+++ b/encoding/gocode/doc.go
@@ -0,0 +1,20 @@
+// Copyright 2019 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 gocode defines functions for extracting CUE definitions from Go code
+// and generating Go code from CUE values.
+//
+// This package is used for offline processing. For converting Go values to and
+// from CUE at runtime, use the gocodec package.
+package gocode
diff --git a/encoding/gocode/gen_test.go b/encoding/gocode/gen_test.go
new file mode 100644
index 0000000..6f01c2b
--- /dev/null
+++ b/encoding/gocode/gen_test.go
@@ -0,0 +1,89 @@
+// Copyright 2019 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.
+
+// +build !gen
+
+package gocode
+
+import (
+ "strings"
+ "testing"
+
+ "cuelang.org/go/encoding/gocode/testdata/pkg1"
+ "cuelang.org/go/encoding/gocode/testdata/pkg2"
+)
+
+func TestPackages(t *testing.T) {
+ testCases := []struct {
+ name string
+ got error
+ want string
+ }{{
+ name: "failing int",
+ got: func() error {
+ v := pkg2.PickMe(4)
+ return v.Validate()
+ }(),
+ want: "invalid value 4 (out of bound >5):\n pkg2/instance.cue:x:x",
+ }, {
+ name: "failing field with validator",
+ got: func() error {
+ v := &pkg1.OtherStruct{A: "car"}
+ return v.Validate()
+ }(),
+ want: "A: invalid value \"car\" (does not satisfy strings.ContainsAny(\"X\")):\n pkg1/instance.cue:x:x",
+ }, {
+ name: "failing field of type int",
+ got: func() error {
+ v := &pkg1.MyStruct{A: 11, B: "dog"}
+ return v.Validate()
+ }(),
+ want: "A: invalid value 11 (out of bound <=10):\n pkg1/instance.cue:x:x",
+ }, {
+ name: "failing nested struct ",
+ got: func() error {
+ v := &pkg1.MyStruct{A: 5, B: "dog", O: &pkg1.OtherStruct{A: "car"}}
+ return v.Validate()
+ }(),
+ want: "O.A: invalid value \"car\" (does not satisfy strings.ContainsAny(\"X\")):\n pkg1/instance.cue:x:x",
+ }, {
+ name: "fail nested struct of different package",
+ got: func() error {
+ v := &pkg1.MyStruct{A: 5, B: "dog", O: &pkg1.OtherStruct{A: "X", P: 4}}
+ return v.Validate()
+ }(),
+ want: "O.P: invalid value 4 (out of bound >5):\n pkg2/instance.cue:x:x",
+ }, {
+ name: "all good",
+ got: func() error {
+ v := &pkg1.MyStruct{
+ A: 5,
+ B: "dog",
+ I: &pkg2.ImportMe{A: 1000, B: "a"},
+ }
+ return v.Validate()
+ }(),
+ want: "nil",
+ }}
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ got := strings.TrimSpace(errStr(tc.got))
+ want := tc.want
+ if got != want {
+ t.Errorf("got:\n%q\nwant:\n%q", got, want)
+ }
+
+ })
+ }
+}
diff --git a/encoding/gocode/generator.go b/encoding/gocode/generator.go
new file mode 100644
index 0000000..494edf1
--- /dev/null
+++ b/encoding/gocode/generator.go
@@ -0,0 +1,277 @@
+// Copyright 2019 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 gocode
+
+import (
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/format"
+ "go/types"
+ "text/template"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/errors"
+ "cuelang.org/go/internal"
+ "golang.org/x/tools/go/packages"
+)
+
+// Config defines options for generation Go code.
+type Config struct {
+ // Prefix is used as a prefix to all generated variables. It defaults to
+ // cuegen.
+ Prefix string
+
+ // ValidateName defines the default name for validation methods or prefix
+ // for validation functions. The default is "Validate". Set to "-" to
+ // disable generating validators.
+ ValidateName string
+
+ // CompleteName defines the default name for complete methods or prefix
+ // for complete functions. The default is "-" (disabled).
+ CompleteName string
+
+ // The cue.Runtime variable name to use for initializing Codecs.
+ // A new Runtime is created by default.
+ RuntimeVar string
+}
+
+const defaultPrefix = "cuegen"
+
+// Generate generates Go code for the given instance in the directory of the
+// given package.
+//
+// Generate converts top-level declarations to corresponding Go code. By default,
+// it will only generate validation functions of methods for exported top-level
+// declarations. The behavior can be altered with the @go attribute.
+//
+// The go attribute has the following form @go(<name>{,<option>}), where option
+// is either a key-value pair or a flag. The name maps the CUE name to an
+// alternative Go name. The special value '-' is used to indicate the field
+// should be ignored for any Go generation.
+//
+// The following options are supported:
+//
+// type=<gotype> The Go type as which this value should be interpreted.
+// This defaults to the type with the (possibly overridden)
+// name of the field.
+// validate=<name> Alternative name for the validation function or method
+// Setting this to the empty string disables generation.
+// complete=<name> Alternative name for the validation function or method.
+// Setting this to the empty string disables generation.
+// func Generate as a function instead of a method.
+//
+//
+// Selection and Naming
+//
+// Generate will not generate any code for fields that have no go attribute
+// and that are not exported or for which there is no namesake Go type.
+// If the go attribute has the special value '-' as its name it wil be dropped
+// as well. In all other cases Generate will generate Go code, even if the
+// resulting code will not compile. For instance, Generate will generate Go
+// code even if the user defines a Go type in the attribute that does not
+// exist.
+//
+// If a field selected for generation and the go name matches that the name of
+// the Go type, the corresponding validate and complete code are generated as
+// methods by default. If not, it will be generated as a function. The default
+// function name is the default operation name with the Go name as a suffix.
+//
+//
+// Caveats
+// Currently not supported:
+// - option to generate Go structs (or automatically generate if undefined)
+// - for type option to refer to types outside the package.
+//
+func Generate(pkgPath string, inst *cue.Instance, c *Config) (b []byte, err error) {
+ // TODO: if inst is nil, the instance is loaded from CUE files in the same
+ // package directory with the same package name.
+ if err = inst.Value().Validate(); err != nil {
+ return nil, err
+ }
+ if c == nil {
+ c = &Config{}
+ }
+ g := &generator{
+ Config: *c,
+
+ typeMap: map[string]types.Type{},
+ }
+
+ pkgName := inst.Name
+
+ if pkgPath != "" {
+ loadCfg := &packages.Config{
+ Mode: packages.LoadAllSyntax,
+ }
+ pkgs, err := packages.Load(loadCfg, pkgPath)
+ if err != nil {
+ return nil, fmt.Errorf("generating failed: %v", err)
+ }
+
+ if len(pkgs) != 1 {
+ return nil, fmt.Errorf(
+ "generate only allowed for one package at a time, found %d",
+ len(pkgs))
+ }
+
+ g.pkg = pkgs[0]
+ if len(g.pkg.Errors) > 0 {
+ for _, err := range g.pkg.Errors {
+ g.addErr(err)
+ }
+ return nil, g.err
+ }
+
+ pkgName = g.pkg.Name
+
+ for _, obj := range g.pkg.TypesInfo.Defs {
+ if obj == nil || obj.Pkg() != g.pkg.Types {
+ continue
+ }
+ g.typeMap[obj.Name()] = obj.Type()
+ }
+ }
+
+ // TODO: add package doc if there is no existing Go package or if it doesn't
+ // have package documentation already.
+ g.exec(headerCode, map[string]string{
+ "pkgName": pkgName,
+ })
+
+ iter, err := inst.Value().Fields()
+ g.addErr(err)
+
+ for iter.Next() {
+ g.decl(iter.Label(), iter.Value())
+ }
+
+ r := internal.GetRuntime(inst).(*cue.Runtime)
+ b, err = r.Marshal(inst)
+ g.addErr(err)
+
+ g.exec(loadCode, map[string]string{
+ "runtime": g.RuntimeVar,
+ "prefix": strValue(g.Prefix, defaultPrefix),
+ "data": string(b),
+ })
+
+ if g.err != nil {
+ return nil, g.err
+ }
+
+ b, err = format.Source(g.w.Bytes())
+ if err != nil {
+ // Return bytes as well to allow analysis of the failed Go code.
+ return g.w.Bytes(), err
+ }
+
+ return b, err
+}
+
+type generator struct {
+ Config
+ pkg *packages.Package
+ typeMap map[string]types.Type
+
+ w bytes.Buffer
+ err errors.Error
+}
+
+func (g *generator) addErr(err error) {
+ if err != nil {
+ g.err = errors.Append(g.err, errors.Promote(err, "generate failed"))
+ }
+}
+
+func (g *generator) exec(t *template.Template, data interface{}) {
+ g.addErr(t.Execute(&g.w, data))
+}
+
+func (g *generator) decl(name string, v cue.Value) {
+ attr := v.Attribute("go")
+
+ if !ast.IsExported(name) && attr.Err() != nil {
+ return
+ }
+
+ goName := name
+ switch s, _ := attr.String(0); s {
+ case "":
+ case "-":
+ return
+ default:
+ goName = s
+ }
+
+ goTypeName := goName
+ goType := ""
+ if str, ok, _ := attr.Lookup(1, "type"); ok {
+ goType = str
+ goTypeName = str
+ }
+
+ isFunc, _ := attr.Flag(1, "func")
+ if goTypeName != goName {
+ isFunc = true
+ }
+
+ zero := "nil"
+
+ typ := g.typeMap[goTypeName]
+ if goType == "" {
+ goType = goTypeName
+ if typ != nil {
+ switch typ.Underlying().(type) {
+ case *types.Struct, *types.Array:
+ goType = "*" + goTypeName
+ zero = fmt.Sprintf("&%s{}", goTypeName)
+ case *types.Pointer:
+ zero = fmt.Sprintf("%s(nil)", goTypeName)
+ isFunc = true
+ }
+ }
+ }
+
+ g.exec(stubCode, map[string]interface{}{
+ "prefix": strValue(g.Prefix, defaultPrefix),
+ "cueName": name, // the field name of the CUE type
+ "goType": goType, // the receiver or argument type
+ "zero": zero, // the zero value of the underlying type
+
+ // @go attribute options
+ "func": isFunc,
+ "validate": lookupName(attr, "validate", strValue(g.ValidateName, "Validate")),
+ "complete": lookupName(attr, "complete", g.CompleteName),
+ })
+}
+
+func lookupName(attr cue.Attribute, option, config string) string {
+ name, ok, _ := attr.Lookup(1, option)
+ if !ok {
+ name = config
+ }
+ if name == "-" {
+ return ""
+ }
+ return name
+}
+
+func strValue(have, fallback string) string {
+ if have == "" {
+ return fallback
+ }
+ return have
+}
diff --git a/encoding/gocode/generator_test.go b/encoding/gocode/generator_test.go
new file mode 100644
index 0000000..70e0aa8
--- /dev/null
+++ b/encoding/gocode/generator_test.go
@@ -0,0 +1,84 @@
+// Copyright 2019 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 gocode
+
+import (
+ "bytes"
+ "flag"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "testing"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/load"
+)
+
+var update = flag.Bool("update", false, "update test files")
+
+func TestGenerate(t *testing.T) {
+ dirs, err := ioutil.ReadDir("testdata")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, d := range dirs {
+ if !d.IsDir() {
+ continue
+ }
+ t.Run(d.Name(), func(t *testing.T) {
+ dir := filepath.Join(cwd, "testdata")
+ pkg := "." + string(filepath.Separator) + d.Name()
+ inst := cue.Build(load.Instances([]string{pkg}, &load.Config{
+ Dir: dir,
+ ModuleRoot: dir,
+ Module: "cuelang.org/go/encoding/gocode/testdata",
+ }))[0]
+ if err := inst.Err; err != nil {
+ t.Fatal(err)
+ }
+
+ goPkg := "./testdata/" + d.Name()
+ b, err := Generate(goPkg, inst, nil)
+ if err != nil {
+ t.Fatal(errStr(err))
+ }
+ // t.Log(string(b))
+
+ goFile := filepath.Join("testdata", d.Name(), "cue_gen.go")
+ if *update {
+ _ = ioutil.WriteFile(goFile, b, 0644)
+ return
+ }
+ })
+ }
+}
+
+func errStr(err error) string {
+ if err == nil {
+ return "nil"
+ }
+ buf := &bytes.Buffer{}
+ errors.Print(buf, err, nil)
+ r := regexp.MustCompile(`.cue:\d+:\d+`)
+ return r.ReplaceAllString(buf.String(), ".cue:x:x")
+}
diff --git a/encoding/gocode/gocodec/codec.go b/encoding/gocode/gocodec/codec.go
new file mode 100644
index 0000000..0037d2a
--- /dev/null
+++ b/encoding/gocode/gocodec/codec.go
@@ -0,0 +1,157 @@
+// Copyright 2019 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 codec converts Go to and from CUE and validates Go values based on
+// CUE constraints.
+//
+// CUE constraints can be used to validate Go types as well as fill out
+// missing struct fields that are implied from the constraints and the values
+// already defined by the struct value.
+package gocodec
+
+import (
+ "sync"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/internal"
+)
+
+// Config has no options yet, but is defined for future extensibility.
+type Config struct {
+}
+
+// A Codec decodes and encodes CUE from and to Go values and validates and
+// completes Go values based on CUE templates.
+type Codec struct {
+ runtime *cue.Runtime
+ mutex sync.RWMutex
+}
+
+// New creates a new Codec for the given instance.
+//
+// It is safe to use the methods of Codec concurrently as long as the given
+// Runtime is not used elsewhere while using Codec. However, only the concurrent
+// use of Decode, Validate, and Complete is efficient.
+func New(r *cue.Runtime, c *Config) *Codec {
+ return &Codec{runtime: r}
+}
+
+// ExtractType extracts a CUE value from a Go type.
+//
+// The type represented by x is converted as the underlying type. Specific
+// values, such as map or slice elements or field values of structs are ignored.
+// If x is of type reflect.Type, the type represented by x is extracted.
+//
+// Fields of structs can be annoted using additional constrains using the 'cue'
+// field tag. The value of the tag is a CUE expression, which may contain
+// references to the JSON name of other fields in a struct.
+//
+// type Sum struct {
+// A int `cue:"c-b" json:"a,omitempty"`
+// B int `cue:"c-a" json:"b,omitempty"`
+// C int `cue:"a+b" json:"c,omitempty"`
+// }
+//
+func (c *Codec) ExtractType(x interface{}) (cue.Value, error) {
+ // ExtractType cannot introduce new fields on repeated calls. We could
+ // consider optimizing the lock usage based on this property.
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ return fromGoType(c.runtime, x)
+}
+
+// TODO: allow extracting constraints and type info separately?
+
+// Decode converts x to a CUE value.
+//
+// If x is of type reflect.Value it will convert the value represented by x.
+func (c *Codec) Decode(x interface{}) (cue.Value, error) {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ // Depending on the type, can introduce new labels on repeated calls.
+ return fromGoValue(c.runtime, x, false)
+}
+
+// Encode converts v to a Go value.
+func (c *Codec) Encode(v cue.Value, x interface{}) error {
+ c.mutex.RLock()
+ defer c.mutex.RUnlock()
+
+ return v.Decode(x)
+}
+
+// Validate checks whether x satisfies the constraints defined by v.
+//
+// The given value must be created using the same Runtime with which c was
+// initialized.
+func (c *Codec) Validate(v cue.Value, x interface{}) error {
+ c.mutex.RLock()
+ defer c.mutex.RUnlock()
+
+ r := checkAndForkRuntime(c.runtime, v)
+ w, err := fromGoValue(r, x, false)
+ if err != nil {
+ return err
+ }
+ return w.Unify(v).Err()
+}
+
+// Complete sets previously undefined values in x that can be uniquely
+// determined form the constraints defined by v if validation passes, or returns
+// an error, without modifying anything, otherwise.
+//
+// Only undefined values are modified. A value is considered undefined if it is
+// pointer type and is nil or if it is a field with a zero value that has a json
+// tag with the omitempty flag.
+//
+// The given value must be created using the same Runtime with which c was
+// initialized.
+//
+// Complete does a JSON round trip. This means that data not preserved in such a
+// round trip, such as the location name of a time.Time, is lost after a
+// successful update.
+func (c *Codec) Complete(v cue.Value, x interface{}) error {
+ c.mutex.RLock()
+ defer c.mutex.RUnlock()
+
+ r := checkAndForkRuntime(c.runtime, v)
+ w, err := fromGoValue(r, x, true)
+ if err != nil {
+ return err
+ }
+
+ return w.Unify(v).Decode(x)
+}
+
+func fromGoValue(r *cue.Runtime, x interface{}, allowDefault bool) (cue.Value, error) {
+ v := internal.FromGoValue(r, x, allowDefault).(cue.Value)
+ if err := v.Err(); err != nil {
+ return v, err
+ }
+ return v, nil
+}
+
+func fromGoType(r *cue.Runtime, x interface{}) (cue.Value, error) {
+ v := internal.FromGoType(r, x).(cue.Value)
+ if err := v.Err(); err != nil {
+ return v, err
+ }
+ return v, nil
+}
+
+func checkAndForkRuntime(r *cue.Runtime, v cue.Value) *cue.Runtime {
+ return internal.CheckAndForkRuntime(r, v).(*cue.Runtime)
+}
diff --git a/encoding/gocode/gocodec/codec_test.go b/encoding/gocode/gocodec/codec_test.go
new file mode 100644
index 0000000..cb38865
--- /dev/null
+++ b/encoding/gocode/gocodec/codec_test.go
@@ -0,0 +1,286 @@
+// Copyright 2019 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 gocodec
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+
+ "cuelang.org/go/cue"
+ "github.com/google/go-cmp/cmp"
+)
+
+type Sum struct {
+ A int `cue:"C-B" json:",omitempty"`
+ B int `cue:"C-A" json:",omitempty"`
+ C int `cue:"A+B & >=5" json:",omitempty"`
+}
+
+func checkErr(t *testing.T, got error, want string) {
+ t.Helper()
+ if (got == nil) != (want == "") {
+ t.Errorf("error: got %v; want %v", got, want)
+ }
+}
+func TestValidate(t *testing.T) {
+ fail := "some error"
+ testCases := []struct {
+ name string
+ value interface{}
+ constraints string
+ err string
+ }{{
+ name: "*Sum: nil disallowed by constraint",
+ value: (*Sum)(nil),
+ constraints: "!=null",
+ err: fail,
+ }, {
+ name: "Sum",
+ value: Sum{A: 1, B: 4, C: 5},
+ }, {
+ name: "*Sum",
+ value: &Sum{A: 1, B: 4, C: 5},
+ }, {
+ name: "*Sum: incorrect sum",
+ value: &Sum{A: 1, B: 4, C: 6},
+ err: fail,
+ }, {
+ name: "*Sum: field C is too low",
+ value: &Sum{A: 1, B: 3, C: 4},
+ err: fail,
+ }, {
+ name: "*Sum: nil value",
+ value: (*Sum)(nil),
+ }, {
+ // Not a typical constraint, but it is possible.
+ name: "string list",
+ value: []string{"a", "b", "c"},
+ constraints: `[_, "b", ...]`,
+ }, {
+ // Not a typical constraint, but it is possible.
+ name: "string list incompatible lengths",
+ value: []string{"a", "b", "c"},
+ constraints: `4*[string]`,
+ err: fail,
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ r := &cue.Runtime{}
+ codec := New(r, nil)
+
+ v, err := codec.ExtractType(tc.value)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if tc.constraints != "" {
+ inst, err := r.Compile(tc.name, tc.constraints)
+ if err != nil {
+ t.Fatal(err)
+ }
+ v = v.Unify(inst.Value())
+ fmt.Println("XXX", v)
+ fmt.Println("XXX", inst.Value())
+ fmt.Println("UUU", v)
+ }
+
+ err = codec.Validate(v, tc.value)
+ checkErr(t, err, tc.err)
+ })
+ }
+}
+
+func TestComplete(t *testing.T) {
+ type updated struct {
+ A []*int `cue:"[...int|*1]"` // arbitrary length slice with values 1
+ B []int `cue:"3*[int|*1]"` // slice of length 3, defaults to [1,1,1]
+
+ // TODO: better errors if the user forgets to quote.
+ M map[string]int `cue:",opt"`
+ }
+ type sump struct {
+ A *int `cue:"C-B"`
+ B *int `cue:"C-A"`
+ C *int `cue:"A+B"`
+ }
+ one := 1
+ two := 2
+ fail := "some error"
+ _ = fail
+ _ = one
+ testCases := []struct {
+ name string
+ value interface{}
+ result interface{}
+ constraints string
+ err string
+ }{{
+ name: "*Sum",
+ value: &Sum{A: 1, B: 4, C: 5},
+ result: &Sum{A: 1, B: 4, C: 5},
+ }, {
+ name: "*Sum",
+ value: &Sum{A: 1, B: 4},
+ result: &Sum{A: 1, B: 4, C: 5},
+ }, {
+ name: "*sump",
+ value: &sump{A: &one, B: &one},
+ result: &sump{A: &one, B: &one, C: &two},
+ }, {
+ name: "*Sum: backwards",
+ value: &Sum{B: 4, C: 8},
+ result: &Sum{A: 4, B: 4, C: 8},
+ }, {
+ name: "*Sum: sum too low",
+ value: &Sum{A: 1, B: 3},
+ result: &Sum{A: 1, B: 3}, // Value should not be updated
+ err: fail,
+ }, {
+ name: "*Sum: sum underspecified",
+ value: &Sum{A: 1},
+ result: &Sum{A: 1}, // Value should not be updated
+ err: fail,
+ }, {
+ name: "Sum: cannot modify",
+ value: Sum{A: 3, B: 4, C: 7},
+ result: Sum{A: 3, B: 4, C: 7},
+ err: fail,
+ }, {
+ name: "*Sum: cannot update nil value",
+ value: (*Sum)(nil),
+ result: (*Sum)(nil),
+ err: fail,
+ }, {
+ name: "cannot modify slice",
+ value: []string{"a", "b", "c"},
+ result: []string{"a", "b", "c"},
+ err: fail,
+ }, {
+ name: "composite values update",
+ // allocate a slice with uninitialized values and let Update fill
+ // out default values.
+ value: &updated{A: make([]*int, 3)},
+ result: &updated{
+ A: []*int{&one, &one, &one},
+ B: []int{1, 1, 1},
+ M: map[string]int(nil),
+ },
+ }, {
+ name: "composite values update with unsatisfied map constraints",
+ value: &updated{},
+ result: &updated{},
+
+ constraints: ` { M: {foo: bar, bar: foo} } `,
+ err: fail, // incomplete values
+ }, {
+ name: "composite values update with map constraints",
+ value: &updated{M: map[string]int{"foo": 1}},
+ constraints: ` { M: {foo: bar, bar: foo} } `,
+ result: &updated{
+ // TODO: would be better if this is nil, but will not matter for
+ // JSON output: if omitempty is false, an empty list will be
+ // printed regardless, and if it is true, it will be omitted
+ // regardless.
+ A: []*int{},
+ B: []int{1, 1, 1},
+ M: map[string]int{"bar": 1, "foo": 1},
+ },
+ }}
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ r := &cue.Runtime{}
+ codec := New(r, nil)
+
+ v, err := codec.ExtractType(tc.value)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if tc.constraints != "" {
+ inst, err := r.Compile(tc.name, tc.constraints)
+ if err != nil {
+ t.Fatal(err)
+ }
+ v = v.Unify(inst.Value())
+ }
+
+ err = codec.Complete(v, tc.value)
+ checkErr(t, err, tc.err)
+ if !reflect.DeepEqual(tc.value, tc.result) {
+ t.Errorf("value:\n got: %#v;\nwant: %#v", tc.value, tc.result)
+ }
+ })
+ }
+}
+
+func TestEncode(t *testing.T) {
+ testCases := []struct {
+ in string
+ dst interface{}
+ want interface{}
+ }{{
+ in: "4",
+ dst: new(int),
+ want: 4,
+ }}
+ r := &cue.Runtime{}
+ c := New(r, nil)
+
+ for _, tc := range testCases {
+ t.Run("", func(t *testing.T) {
+ inst, err := r.Compile("test", tc.in)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = c.Encode(inst.Value(), tc.dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ got := reflect.ValueOf(tc.dst).Elem().Interface()
+ if !cmp.Equal(got, tc.want) {
+ t.Error(cmp.Diff(got, tc.want))
+ }
+ })
+ }
+}
+
+func TestDecode(t *testing.T) {
+ testCases := []struct {
+ in interface{}
+ want string
+ }{{
+ in: "str",
+ want: `"str"`,
+ }}
+ c := New(&cue.Runtime{}, nil)
+
+ for _, tc := range testCases {
+ t.Run("", func(t *testing.T) {
+ v, err := c.Decode(tc.in)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ got := fmt.Sprint(v)
+ if got != tc.want {
+ t.Errorf("got %v; want %v", got, tc.want)
+ }
+ })
+ }
+}
diff --git a/encoding/gocode/templates.go b/encoding/gocode/templates.go
new file mode 100644
index 0000000..db1b906
--- /dev/null
+++ b/encoding/gocode/templates.go
@@ -0,0 +1,99 @@
+// Copyright 2019 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 gocode
+
+import "text/template"
+
+// Inputs:
+// .pkgName the Go package name
+var headerCode = template.Must(template.New("header").Parse(
+ `// Code generated by gogen.Generate; DO NOT EDIT.
+
+package {{.pkgName}}
+
+import (
+ "fmt"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/encoding/gocode/gocodec"
+)
+
+`))
+
+// Inputs:
+// .prefix prefix to all generated variable names
+// .cueName name of the top-level CUE value
+// .goType Go type of the receiver or argument
+// .zero zero value of the Go type; nil indicates no value
+// .validate name of the validate function; "" means no validate
+// .complete name of the complete function; "" means no complete
+var stubCode = template.Must(template.New("type").Parse(`
+var {{.prefix}}val{{.cueName}} = {{.prefix}}Make("{{.cueName}}", {{.zero}})
+
+{{ $sig := .goType | printf "(x %s)" -}}
+{{if .validate}}
+// {{.validate}}{{if .func}}{{.cueName}}{{end}} validates x.
+func {{if .func}}{{.validate}}{{.cueName}}{{$sig}}
+ {{- else -}}{{$sig}} {{.validate}}(){{end}} error {
+ return {{.prefix}}Codec.Validate({{.prefix}}val{{.cueName}}, x)
+}
+{{end}}
+{{if .complete}}
+// {{.complete}}{{if .func}}{{.cueName}}{{end}} completes x.
+func {{if .func}}{{.complete}}{{.cueName}}{{$sig}}
+ {{- else -}}{{$sig}} {{.complete}}(){{end}} error {
+ return {{.prefix}}Codec.Complete({{.prefix}}val{{.cueName}}, x)
+}
+{{end}}
+`))
+
+// Inputs:
+// .prefix prefix to all generated variable names
+// .runtime the variable name of a user-supplied runtime, if any
+// .data bytes obtained from Instance.MarshalBinary
+var loadCode = template.Must(template.New("load").Parse(`
+var {{.prefix}}Codec, {{.prefix}}Instance = func() (*gocodec.Codec, *cue.Instance) {
+ var r *cue.Runtime
+ r = {{if .runtime}}{{.runtime}}{{else}}&cue.Runtime{}{{end}}
+ instances, err := r.Unmarshal({{.prefix}}InstanceData)
+ if err != nil {
+ panic(err)
+ }
+ if len(instances) != 1 {
+ panic("expected encoding of exactly one instance")
+ }
+ return gocodec.New(r, nil), instances[0]
+}()
+
+// {{.prefix}}Make is called in the init phase to initialize CUE values for
+// validation functions.
+func {{.prefix}}Make(name string, x interface{}) cue.Value {
+ v := {{.prefix}}Instance.Lookup(name)
+ if !v.Exists() {
+ panic(fmt.Errorf("could not find type %q in instance", name))
+ }
+ if x != nil {
+ w, err := {{.prefix}}Codec.ExtractType(x)
+ if err != nil {
+ panic(err)
+ }
+ v = v.Unify(w)
+ }
+ return v
+}
+
+// Data size: {{len .data}} bytes.
+var {{.prefix}}InstanceData = []byte({{printf "%+q" .data}})
+`))
diff --git a/encoding/gocode/test.cue b/encoding/gocode/test.cue
new file mode 100644
index 0000000..1d62a37
--- /dev/null
+++ b/encoding/gocode/test.cue
@@ -0,0 +1,3 @@
+import "strings"
+
+a: strings.ContainsAny("X") & "car"
diff --git a/encoding/gocode/testdata/cue.mod b/encoding/gocode/testdata/cue.mod
new file mode 100644
index 0000000..eaf98ca
--- /dev/null
+++ b/encoding/gocode/testdata/cue.mod
@@ -0,0 +1 @@
+module: "cuelang.org/go/encoding/gocode/testdata"
diff --git a/encoding/gocode/testdata/pkg1/code.go b/encoding/gocode/testdata/pkg1/code.go
new file mode 100644
index 0000000..4f5fc3b
--- /dev/null
+++ b/encoding/gocode/testdata/pkg1/code.go
@@ -0,0 +1,43 @@
+// Copyright 2019 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 pkg1
+
+import (
+ "time"
+
+ "cuelang.org/go/encoding/gocode/testdata/pkg2"
+)
+
+type MyStruct struct {
+ A int
+ B string
+ T time.Time // maps to builtin
+ O *OtherStruct
+ I *pkg2.ImportMe
+}
+
+type OtherStruct struct {
+ A string
+ // D time.Duration // maps to builtin
+ P pkg2.PickMe
+}
+
+type String string
+
+type Omit int
+
+type Ptr *struct {
+ A int
+}
diff --git a/encoding/gocode/testdata/pkg1/cue_gen.go b/encoding/gocode/testdata/pkg1/cue_gen.go
new file mode 100644
index 0000000..1fe592f
--- /dev/null
+++ b/encoding/gocode/testdata/pkg1/cue_gen.go
@@ -0,0 +1,83 @@
+// Code generated by gogen.Generate; DO NOT EDIT.
+
+package pkg1
+
+import (
+ "fmt"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/encoding/gocode/gocodec"
+)
+
+var cuegenvalMyStruct = cuegenMake("MyStruct", &MyStruct{})
+
+// Validate validates x.
+func (x *MyStruct) Validate() error {
+ return cuegenCodec.Validate(cuegenvalMyStruct, x)
+}
+
+// Complete completes x.
+func (x *MyStruct) Complete() error {
+ return cuegenCodec.Complete(cuegenvalMyStruct, x)
+}
+
+var cuegenvalOtherStruct = cuegenMake("OtherStruct", &OtherStruct{})
+
+// Validate validates x.
+func (x *OtherStruct) Validate() error {
+ return cuegenCodec.Validate(cuegenvalOtherStruct, x)
+}
+
+var cuegenvalString = cuegenMake("String", nil)
+
+// ValidateCUE validates x.
+func (x String) ValidateCUE() error {
+ return cuegenCodec.Validate(cuegenvalString, x)
+}
+
+var cuegenvalSpecialString = cuegenMake("SpecialString", nil)
+
+// ValidateSpecialString validates x.
+func ValidateSpecialString(x string) error {
+ return cuegenCodec.Validate(cuegenvalSpecialString, x)
+}
+
+var cuegenvalPtr = cuegenMake("Ptr", Ptr(nil))
+
+// ValidatePtr validates x.
+func ValidatePtr(x Ptr) error {
+ return cuegenCodec.Validate(cuegenvalPtr, x)
+}
+
+var cuegenCodec, cuegenInstance = func() (*gocodec.Codec, *cue.Instance) {
+ var r *cue.Runtime
+ r = &cue.Runtime{}
+ instances, err := r.Unmarshal(cuegenInstanceData)
+ if err != nil {
+ panic(err)
+ }
+ if len(instances) != 1 {
+ panic("expected encoding of exactly one instance")
+ }
+ return gocodec.New(r, nil), instances[0]
+}()
+
+// cuegenMake is called in the init phase to initialize CUE values for
+// validation functions.
+func cuegenMake(name string, x interface{}) cue.Value {
+ v := cuegenInstance.Lookup(name)
+ if !v.Exists() {
+ panic(fmt.Errorf("could not find type %q in instance", name))
+ }
+ if x != nil {
+ w, err := cuegenCodec.ExtractType(x)
+ if err != nil {
+ panic(err)
+ }
+ v = v.Unify(w)
+ }
+ return v
+}
+
+// Data size: 518 bytes.
+var cuegenInstanceData = []byte("\x01\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\x94R]k\xd4@\x14\x9d\x9b\xae`.U\xf0\a\b\xe3<m\xa4\xcd~\x80\b\xa1Qk\xad\u0407v\x17\x8b\"\x88\x0f\xe3\xeclv\xd8l&$w\u0165\xb6\xa0\xd6\xda_\xe7\xab\xff\xa6\x91|\xb5\xabo\xcd\xcb\\n\xe6\x9c{\xe6\xdcs\xaf\xf8\xe5\x80S\\2(\xbe3\xf6\xb4\xf8\xb6\x01\xb0i\x92\x9cd\xa2\xf4+I\xb2\xec\xc3\x06t\xdeXK\xe00\xe8\x8c%\xcd`\x93\xc1\x9d\xd7&\xd69\x14\x17\x8c\xb1\x87\xc5O\a\xe0\xfe\x87\x8fj\xa9\xfd\xa9\x89\x1b\xe4\x05\x83\u2731n\xf1c\x03\xe0\xeeM\xff\x9c\x81\x03\x9d#\xb9\xd0%Q\xa7j\"c\xec\xca\xf9]\\2\a\x00\xb6\xd4R\xc72\x89|\x9bE\xbd\xc8\xf6t\xa2\xec\xc4$e\xad\xecD\xf7H\xe74\x91${\xe9<\x1a\x00\xc0\x83\xf2\ucd7a}\xb5\xd4p\x05\u007fR\xa9\xe62\u04bc\xfc\x89h\x16\xa9\u0348w\xd1\x15\xb7`\x1f\nt\xc5B\u04ac<s\xcaL\x12\xe5\x02=\xc4\xc3\xd51eKE\x01?Aw7\xe0|'\x1c\xf4\xd1}\x19p\x1e\x9e\t%I\xf0\xaf\xfc\xb1\x98\xd8H\xa0;z\x1e\xf0\x11\xcdtVc\xd0=\bx)k\xe8\x1fT\xaa\x0e5\x9e\xf2\x17\x91\xedn)\xbbHcM:\xdck\n\x0f\u05c0\xed\xb0F\x88\xbfg\x13\x92&\xc9w\x93UW\xbc\x17\x1e\xba\xe3\xa0\xe6\x1d\x1b5/Y\xf1\xb8\xba\x1a\xf0\xe6{\x14\n\xd1\xd6\xd5\xc0\xcf26\x13I:|\xd7\x14{o\xf7=<N\xb522n\xc1\xe1\x99\xc8\ub3a8Q\xb4JuX\xab\xf0p\xb40t=\x80s\x93\x10_\x1f\xb1\xed\xe1\x91M\xf6\xbf\x98\x9c*\xb2\x93\u02a5\x1a\xdc<{\xdb\u00e9\xb5A\t\xc51e\xed3K\xdf\xfd\xc3eL&\x8d\xf5h\xda\x1d\xf4=<E\u019c\xdb\xe4c\xd8\xe4c\xf8o>\xe4Z:\x86\xd7\xe9\xb8Y1\xb6\x9bi\xc5\xec\f\xfa\xfd5\xe5\xff\x99/?)Q\x8a\xab}\x0f\xf8\xb3'\xc8\xd8\xdf\x00\x00\x00\xff\xff\xa9a\xee\x8d^\x03\x00\x00")
diff --git a/encoding/gocode/testdata/pkg1/instance.cue b/encoding/gocode/testdata/pkg1/instance.cue
new file mode 100644
index 0000000..3358cbe
--- /dev/null
+++ b/encoding/gocode/testdata/pkg1/instance.cue
@@ -0,0 +1,37 @@
+package pkg1
+
+import (
+ "math"
+ "strings"
+ "cuelang.org/go/encoding/gocode/testdata/pkg2"
+)
+
+MyStruct: {
+ A: <=10
+ B: =~"cat" | *"dog"
+ O?: OtherStruct
+ I: pkg2.ImportMe
+} @go(,complete=Complete)
+
+OtherStruct: {
+ A: strings.ContainsAny("X")
+ P: pkg2.PickMe
+}
+
+String: !="" @go(,validate=ValidateCUE)
+
+SpecialString: =~"special" @go(,type=string)
+
+Omit: int @go(-)
+
+// NonExisting will be omitted as there is no equivalent Go type.
+NonExisting: {
+ B: string
+} @go(-)
+
+// ignore unexported unless explicitly enabled.
+foo: int
+
+Ptr: {
+ A: math.MultipleOf(10)
+}
diff --git a/encoding/gocode/testdata/pkg2/code.go b/encoding/gocode/testdata/pkg2/code.go
new file mode 100644
index 0000000..3620911
--- /dev/null
+++ b/encoding/gocode/testdata/pkg2/code.go
@@ -0,0 +1,22 @@
+// Copyright 2019 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 pkg2
+
+type ImportMe struct {
+ A int `json:"a"`
+ B string `json:"b"`
+}
+
+type PickMe int
diff --git a/encoding/gocode/testdata/pkg2/cue_gen.go b/encoding/gocode/testdata/pkg2/cue_gen.go
new file mode 100644
index 0000000..74917b3
--- /dev/null
+++ b/encoding/gocode/testdata/pkg2/cue_gen.go
@@ -0,0 +1,57 @@
+// Code generated by gogen.Generate; DO NOT EDIT.
+
+package pkg2
+
+import (
+ "fmt"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/encoding/gocode/gocodec"
+)
+
+var cuegenvalImportMe = cuegenMake("ImportMe", &ImportMe{})
+
+// Validate validates x.
+func (x *ImportMe) Validate() error {
+ return cuegenCodec.Validate(cuegenvalImportMe, x)
+}
+
+var cuegenvalPickMe = cuegenMake("PickMe", nil)
+
+// Validate validates x.
+func (x PickMe) Validate() error {
+ return cuegenCodec.Validate(cuegenvalPickMe, x)
+}
+
+var cuegenCodec, cuegenInstance = func() (*gocodec.Codec, *cue.Instance) {
+ var r *cue.Runtime
+ r = &cue.Runtime{}
+ instances, err := r.Unmarshal(cuegenInstanceData)
+ if err != nil {
+ panic(err)
+ }
+ if len(instances) != 1 {
+ panic("expected encoding of exactly one instance")
+ }
+ return gocodec.New(r, nil), instances[0]
+}()
+
+// cuegenMake is called in the init phase to initialize CUE values for
+// validation functions.
+func cuegenMake(name string, x interface{}) cue.Value {
+ v := cuegenInstance.Lookup(name)
+ if !v.Exists() {
+ panic(fmt.Errorf("could not find type %q in instance", name))
+ }
+ if x != nil {
+ w, err := cuegenCodec.ExtractType(x)
+ if err != nil {
+ panic(err)
+ }
+ v = v.Unify(w)
+ }
+ return v
+}
+
+// Data size: 276 bytes.
+var cuegenInstanceData = []byte("\x01\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xffD\x8e\xc1J\x031\x10\x86\xe7\u07ee`C\x15|\x00a\xd9S\x05\u066d\x82\bE\x84\xaa\b\x1e\x94\xe2U<\x8ci\x8c\xa1mR\x9a\xecA\u0103Z\xabO\xe3+FR*\x9ef\xf8f\xfe\x9fo+~e\xc8\xe27!\xbe\x13\x1d\u01f7\x16\xd01\xd6\a\xb6R]p\xe0\xc4\xd1B~\xeb\\@F\u0207\x1c\x9e\xd0!l\\\x9a\x89\xf2\x88K\"\u068d\x9f\x19\xb0}w/\x1bU=\x9a\xc9:\xb9$\xc4\x05Q7~\xb4\x80\xcd\u007f\xbe d\xc8ox\xaaRQ\xbe\x82\x82\x88\xe2O\x12\x01\xb0/\x1b5a\xab+7\u05f5v\xb5\xb2\u048d\x8cM\xbbt#U\a\xe5\u00c8\x03\u05f3\xb1>\x04\xb0\x93f\xfd\xa7]\xc9F\x81g,\u01ecU\x91NB\x98\xe9\xcc\xcdCQ\xfa07V\xfbR\x88\xab\x15\xb9V\xfd\xe2E\xb4\a\xfd\xe2\xe4\xa0\xd7\x13\xed\xb3~\xb1~\xa9\u039d\rl\xac\x1f\xd8\xe7n\xc9\x0f\xb2\xdc\x13\xafbh\xe48eN\x8f\x04\xd1o\x00\x00\x00\xff\xff\ue135\t=\x01\x00\x00")
diff --git a/encoding/gocode/testdata/pkg2/instance.cue b/encoding/gocode/testdata/pkg2/instance.cue
new file mode 100644
index 0000000..5a6e606
--- /dev/null
+++ b/encoding/gocode/testdata/pkg2/instance.cue
@@ -0,0 +1,10 @@
+package pkg2
+
+import "strings"
+
+ImportMe: {
+ A: <100
+ B: strings.ContainsAny("abc")
+}
+
+PickMe: >5