cue: implement marshaling and restoring instances
Also fixed bug in export that prevented proper
raw exporting.
Change-Id: I128760ed89efa1907e15b999722806267ec6144a
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2707
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/build.go b/cue/build.go
index c0fb628..f0fb2a5 100644
--- a/cue/build.go
+++ b/cue/build.go
@@ -15,12 +15,16 @@
package cue
import (
+ "bytes"
+ "compress/gzip"
+ "encoding/gob"
"path"
"strconv"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/format"
"cuelang.org/go/cue/token"
)
@@ -29,9 +33,8 @@
// Any operation that involves two Values or Instances should originate from
// the same Runtime.
type Runtime struct {
- Context *build.Context
+ Context *build.Context // TODO: remove
idx *index
- ctxt *build.Context
}
func dummyLoad(token.Pos, string) *build.Instance { return nil }
@@ -56,6 +59,73 @@
return inst, nil
}
+type instanceData struct {
+ Filename string
+ PkgPath string
+ Data []byte
+}
+
+// Unmarshal creates an Instance from bytes generated by the MarshalBinary
+// method of an instance.
+func (r *Runtime) Unmarshal(b []byte) (*Instance, error) {
+ reader, err := gzip.NewReader(bytes.NewReader(b))
+ if err != nil {
+ return nil, errors.Newf(token.NoPos, "restore failed: %v", err)
+ }
+ version := [1]byte{}
+ _, _ = reader.Read(version[:])
+
+ data := &instanceData{}
+ if err = gob.NewDecoder(reader).Decode(data); err != nil {
+ return nil, errors.Newf(token.NoPos, "restore failed: %v", err)
+ }
+
+ inst, err := r.Compile(data.Filename, data.Data)
+ if inst != nil {
+ inst.ImportPath = data.PkgPath
+ }
+ return inst, err
+}
+
+// MarshalBinary marshals c into binary data.
+func (inst *Instance) MarshalBinary() (b []byte, err error) {
+ ctx := inst.index.newContext()
+ n := export(ctx, inst.rootValue, options{raw: true})
+
+ file, ok := n.(*ast.File)
+ if !ok {
+ file = &ast.File{}
+ if obj, ok := n.(*ast.StructLit); ok {
+ file.Decls = append(file.Decls, obj.Elts...)
+ } else {
+ file.Decls = append(file.Decls, &ast.EmbedDecl{Expr: n.(ast.Expr)})
+ }
+ }
+ if inst.Name != "" {
+ file.Name = ast.NewIdent(inst.Name)
+ }
+
+ b, err = format.Node(file)
+ if err != nil {
+ return nil, err
+ }
+
+ buf := &bytes.Buffer{}
+ zw := gzip.NewWriter(buf)
+ _, _ = zw.Write([]byte{0}) // version marker
+ enc := gob.NewEncoder(zw)
+ err = enc.Encode(&instanceData{
+ Filename: inst.Value().Pos().Filename(),
+ PkgPath: inst.ImportPath,
+ Data: b,
+ })
+ if err != nil {
+ return nil, err
+ }
+ err = zw.Close()
+ return buf.Bytes(), err
+}
+
// Compile compiles the given source into an Instance. The source code may be
// provided as a string, byte slice, io.Reader. The name is used as the file
// name in position information. The source may import builtin packages. Use
@@ -119,15 +189,7 @@
//
// Deprecated: use Compile
func (r *Runtime) Parse(name string, source interface{}) (*Instance, error) {
- ctx := r.Context
- if ctx == nil {
- ctx = build.NewContext()
- }
- p := ctx.NewInstance(name, dummyLoad)
- if err := p.AddFile(name, source); err != nil {
- return nil, err
- }
- return r.complete(p)
+ return r.Compile(name, source)
}
// Build creates an Instance from the given build.Instance. A returned Instance
diff --git a/cue/build_test.go b/cue/build_test.go
index cb3607c..8377e8a 100644
--- a/cue/build_test.go
+++ b/cue/build_test.go
@@ -24,6 +24,73 @@
"cuelang.org/go/cue/token"
)
+func TestMarshalling(t *testing.T) {
+ testCases := []struct {
+ filename string
+ input string
+ pkg string
+ }{{
+ filename: "foo.cue",
+ pkg: "foo",
+ input: `package foo
+
+ A: int
+ B: string
+ `,
+ }, {
+ filename: "bar.cue",
+ pkg: "bar",
+ input: `package bar
+
+ "Hello world!"
+ `,
+ }, {
+ filename: "qux.cue",
+ input: `
+ "Hello world!"
+ `,
+ }, {
+ filename: "baz.cue",
+ pkg: "baz",
+ input: `package baz
+
+ import "strings"
+
+ a: strings.TrimSpace(" Hello world! ")
+ `}}
+ for _, tc := range testCases {
+ t.Run(tc.filename, func(t *testing.T) {
+ r := &Runtime{}
+ inst, err := r.Compile(tc.filename, tc.input)
+ if err != nil {
+ t.Fatal(err)
+ }
+ inst.ImportPath = "test/pkg"
+ got := fmt.Sprint(inst.Value())
+
+ b, err := inst.MarshalBinary()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ r2 := &Runtime{}
+ inst, err = r2.Unmarshal(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if inst.ImportPath != "test/pkg" {
+ t.Error("import path was not restored")
+ }
+ want := fmt.Sprint(inst.Value())
+
+ if got != want {
+ t.Errorf("\ngot: %q;\nwant: %q", got, want)
+ }
+ })
+ }
+}
+
func TestFromExpr(t *testing.T) {
testCases := []struct {
expr ast.Expr
diff --git a/cue/export.go b/cue/export.go
index df4c898..61454d3 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -44,6 +44,7 @@
value := e.expr(v)
if len(e.imports) == 0 {
+ // TODO: unwrap structs?
return value
}
imports := make([]string, 0, len(e.imports))
@@ -73,7 +74,6 @@
})
}
- // TODO: should we unwrap structs?
if obj, ok := value.(*ast.StructLit); ok {
file.Decls = append(file.Decls, obj.Elts...)
} else {
@@ -275,7 +275,13 @@
}
case *callExpr:
- call := &ast.CallExpr{Fun: p.expr(x.x)}
+ call := &ast.CallExpr{}
+ b := x.x.evalPartial(p.ctx)
+ if b, ok := b.(*builtin); ok {
+ call.Fun = p.expr(b)
+ } else {
+ call.Fun = p.expr(x.x)
+ }
for _, a := range x.args {
call.Args = append(call.Args, p.expr(a))
}
@@ -335,14 +341,14 @@
obj := &ast.StructLit{}
if doEval(p.mode) {
x = x.expandFields(p.ctx)
- for _, a := range x.arcs {
- p.stack = append(p.stack, remap{
- key: x,
- from: a.feature,
- to: nil,
- syn: obj,
- })
- }
+ }
+ for _, a := range x.arcs {
+ p.stack = append(p.stack, remap{
+ key: x,
+ from: a.feature,
+ to: nil,
+ syn: obj,
+ })
}
if x.emit != nil {
obj.Elts = append(obj.Elts, &ast.EmbedDecl{Expr: p.expr(x.emit)})
diff --git a/cue/export_test.go b/cue/export_test.go
index c3ca25b..9da8857 100644
--- a/cue/export_test.go
+++ b/cue/export_test.go
@@ -305,7 +305,8 @@
}][a]
a: int
c: 1
- }`)}}
+ }`),
+ }}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
body := fmt.Sprintf("Test: %s", tc.in)
@@ -350,6 +351,16 @@
in: `
import "strings"
+ a: strings.TrimSpace(" c ")
+ `,
+ out: unindent(`
+ import "strings"
+
+ a: strings.TrimSpace(" c ")`),
+ }, {
+ in: `
+ import "strings"
+
stringsx = strings
a: {
@@ -361,6 +372,16 @@
STRINGS = strings
a strings: STRINGS.ContainsAny("c")`),
+ }, {
+ in: `
+ a: b - 100
+ b: a + 100
+ `,
+ out: unindent(`
+ {
+ a: b - 100
+ b: a + 100
+ }`),
}}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
@@ -372,8 +393,8 @@
v := inst.Value()
ctx := r.index().newContext()
- opts := options{raw: false}
- b, err := format.Node(export(ctx, v.eval(ctx), opts), format.Simplify())
+ opts := options{raw: true}
+ b, err := format.Node(export(ctx, v.path.v, opts), format.Simplify())
if err != nil {
log.Fatal(err)
}