cue: better document Context.Encode and friends
The behavior has been slightly modified to be more in line
with Go's JSON encoding, where it makes sense.
Only `FillPath` is documented as `Fill` is deprecated.
The main docuemntation is at Context.Encode.
Fixes #676
Change-Id: I1d885cfbe655a41064a37b82a98ed66d3865a61e
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/9569
Reviewed-by: Paul Jolly <paul@myitcv.org.uk>
diff --git a/cue/context.go b/cue/context.go
index 3257fb7..e84f4fb 100644
--- a/cue/context.go
+++ b/cue/context.go
@@ -211,6 +211,86 @@
//
// The returned Value will represent an error, accessible through Err, if any
// error occurred.
+//
+// Encode traverses the value v recursively. If an encountered value implements
+// the json.Marshaler interface and is not a nil pointer, Encode calls its
+// MarshalJSON method to produce JSON and convert that to CUE instead. If no
+// MarshalJSON method is present but the value implements encoding.TextMarshaler
+// instead, Encode calls its MarshalText method and encodes the result as a
+// string.
+//
+// Otherwise, Encode uses the following type-dependent default encodings:
+//
+// Boolean values encode as CUE booleans.
+//
+// Floating point, integer, and *big.Int and *big.Float values encode as CUE
+// numbers.
+//
+// String values encode as CUE strings coerced to valid UTF-8, replacing
+// sequences of invalid bytes with the Unicode replacement rune as per Unicode's
+// and W3C's recommendation.
+//
+// Array and slice values encode as CUE lists, except that []byte encodes as a
+// bytes value, and a nil slice encodes as the null.
+//
+// Struct values encode as CUE structs. Each exported struct field becomes a
+// member of the object, using the field name as the object key, unless the
+// field is omitted for one of the reasons given below.
+//
+// The encoding of each struct field can be customized by the format string
+// stored under the "json" key in the struct field's tag. The format string
+// gives the name of the field, possibly followed by a comma-separated list of
+// options. The name may be empty in order to specify options without overriding
+// the default field name.
+//
+// The "omitempty" option specifies that the field should be omitted from the
+// encoding if the field has an empty value, defined as false, 0, a nil pointer,
+// a nil interface value, and any empty array, slice, map, or string.
+//
+// See the documentation for Go's json.Marshal for more details on the field
+// tags and their meaning.
+//
+// Anonymous struct fields are usually encoded as if their inner exported
+// fields were fields in the outer struct, subject to the usual Go visibility
+// rules amended as described in the next paragraph. An anonymous struct field
+// with a name given in its JSON tag is treated as having that name, rather than
+// being anonymous. An anonymous struct field of interface type is treated the
+// same as having that type as its name, rather than being anonymous.
+//
+// The Go visibility rules for struct fields are amended for when deciding which
+// field to encode or decode. If there are multiple fields at the same level,
+// and that level is the least nested (and would therefore be the nesting level
+// selected by the usual Go rules), the following extra rules apply:
+//
+// 1) Of those fields, if any are JSON-tagged, only tagged fields are
+// considered, even if there are multiple untagged fields that would otherwise
+// conflict.
+//
+// 2) If there is exactly one field (tagged or not according to the first rule),
+// that is selected.
+//
+// 3) Otherwise there are multiple fields, and all are ignored; no error occurs.
+//
+// Map values encode as CUE structs. The map's key type must either be a string,
+// an integer type, or implement encoding.TextMarshaler. The map keys are sorted
+// and used as CUE struct field names by applying the following rules, subject
+// to the UTF-8 coercion described for string values above:
+//
+// - keys of any string type are used directly
+// - encoding.TextMarshalers are marshaled
+// - integer keys are converted to strings
+//
+// Pointer values encode as the value pointed to. A nil pointer encodes as the
+// null CUE value.
+//
+// Interface values encode as the value contained in the interface. A nil
+// interface value encodes as the null CUE value. The NilIsAny EncodingOption
+// can be used to interpret nil as any (_) instead.
+//
+// Channel, complex, and function values cannot be encoded in CUE. Attempting to
+// encode such a value results in the returned value being an error, accessible
+// through the Err method.
+//
func (c *Context) Encode(x interface{}, option ...EncodeOption) Value {
switch v := x.(type) {
case adt.Value:
diff --git a/cue/types.go b/cue/types.go
index 9e7aa70..a0c5aa5 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -1585,7 +1585,8 @@
// If x is a Value, it will be used as is. It panics if x is not created
// from the same Runtime as v.
//
-// Otherwise, the given Go value will be converted to CUE.
+// Otherwise, the given Go value will be converted to CUE using the same rules
+// as Context.Encode.
//
// Any reference in v referring to the value at the given path will resolve to x
// in the newly created value. The resulting value is not validated.
@@ -1606,8 +1607,6 @@
panic("values are not from the same runtime")
}
expr = x.v
- case adt.Node, adt.Feature:
- panic("cannot set internal Value or Feature type")
case ast.Expr:
n := getScopePrefix(v, p)
expr = resolveExpr(ctx, n, x)
diff --git a/cue/types_test.go b/cue/types_test.go
index 1751b94..e949336 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -1194,6 +1194,40 @@
}
}
+func TestFillPathError(t *testing.T) {
+ r := &Runtime{}
+
+ type key struct{ a int }
+
+ testCases := []struct {
+ in string
+ x interface{}
+ path Path
+ err string
+ }{{
+ // unsupported type.
+ in: `_`,
+ x: make(chan int),
+ err: "unsupported Go type (chan int)",
+ }}
+
+ for _, tc := range testCases {
+ t.Run("", func(t *testing.T) {
+ v := compileT(t, r, tc.in).Value()
+ v = v.FillPath(tc.path, tc.x)
+
+ err := v.Err()
+ if err == nil {
+ t.Errorf("unexpected success")
+ }
+
+ if got := err.Error(); !strings.Contains(got, tc.err) {
+ t.Errorf("\ngot: %s\nwant: %s", got, tc.err)
+ }
+ })
+ }
+}
+
func TestAllows(t *testing.T) {
r := &Runtime{}
diff --git a/internal/core/adt/composite.go b/internal/core/adt/composite.go
index c6bd023..afbe67c 100644
--- a/internal/core/adt/composite.go
+++ b/internal/core/adt/composite.go
@@ -438,7 +438,12 @@
// func (v *Vertex) Evaluate()
func (v *Vertex) Finalize(c *OpContext) {
+ // Saving and restoring the error context prevents v from panicking in
+ // case the caller did not handle existing errors in the context.
+ err := c.errs
+ c.errs = nil
c.Unify(v, Finalized)
+ c.errs = err
}
func (v *Vertex) AddErr(ctx *OpContext, b *Bottom) {
diff --git a/internal/core/convert/go.go b/internal/core/convert/go.go
index 5c4d646..b6cc20d 100644
--- a/internal/core/convert/go.go
+++ b/internal/core/convert/go.go
@@ -24,9 +24,9 @@
"sort"
"strconv"
"strings"
- "unicode/utf8"
"github.com/cockroachdb/apd/v2"
+ "golang.org/x/text/encoding/unicode"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/ast/astutil"
@@ -209,7 +209,7 @@
func GoValueToExpr(ctx *adt.OpContext, nilIsTop bool, x interface{}) adt.Expr {
e := convertRec(ctx, nilIsTop, x)
if e == nil {
- return ctx.AddErrf("unsupported Go type (%v)", e)
+ return ctx.AddErrf("unsupported Go type (%T)", x)
}
return e
}
@@ -321,10 +321,8 @@
case bool:
return &adt.Bool{Src: ctx.Source(), B: v}
case string:
- if !utf8.ValidString(v) {
- return ctx.AddErrf("cannot convert result to string: invalid UTF-8")
- }
- return &adt.String{Src: ctx.Source(), Str: v}
+ s, _ := unicode.UTF8.NewEncoder().String(v)
+ return &adt.String{Src: ctx.Source(), Str: s}
case []byte:
return &adt.Bytes{Src: ctx.Source(), B: v}
case int:
@@ -377,9 +375,11 @@
case reflect.String:
str := value.String()
- if !utf8.ValidString(str) {
- return ctx.AddErrf("cannot convert result to string: invalid UTF-8")
- }
+ str, _ = unicode.UTF8.NewEncoder().String(str)
+ // TODO: here and above: allow to fail on invalid strings.
+ // if !utf8.ValidString(str) {
+ // return ctx.AddErrf("cannot convert result to string: invalid UTF-8")
+ // }
return &adt.String{Src: ctx.Source(), Str: str}
case reflect.Int, reflect.Int8, reflect.Int16,
@@ -475,11 +475,17 @@
t := value.Type()
switch key := t.Key(); key.Kind() {
+ default:
+ if !key.Implements(textMarshaler) {
+ return ctx.AddErrf("unsupported Go type for map key (%v)", key)
+ }
+ fallthrough
case reflect.String,
reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+
keys := value.MapKeys()
sort.Slice(keys, func(i, j int) bool {
return fmt.Sprint(keys[i]) < fmt.Sprint(keys[j])
@@ -494,7 +500,7 @@
// mimic behavior of encoding/json: report error of
// unsupported type.
if sub == nil {
- return ctx.AddErrf("unsupported Go type (%v)", val)
+ return ctx.AddErrf("unsupported Go type (%T)", val.Interface())
}
if isBottom(sub) {
return sub
@@ -514,9 +520,6 @@
}
v.Arcs = append(v.Arcs, arc)
}
-
- default:
- return ctx.AddErrf("unsupported Go type for map key (%v)", key)
}
return v
@@ -528,7 +531,8 @@
val := value.Index(i)
x := convertRec(ctx, nilIsTop, val.Interface())
if x == nil {
- return ctx.AddErrf("unsupported Go type (%v)", val)
+ return ctx.AddErrf("unsupported Go type (%T)",
+ val.Interface())
}
if isBottom(x) {
return x
diff --git a/internal/core/convert/go_test.go b/internal/core/convert/go_test.go
index 9d3a35f..0b46873 100644
--- a/internal/core/convert/go_test.go
+++ b/internal/core/convert/go_test.go
@@ -14,7 +14,10 @@
package convert_test
+// TODO: generate tests from Go's json encoder.
+
import (
+ "encoding"
"math/big"
"reflect"
"testing"
@@ -32,7 +35,21 @@
func mkBigInt(a int64) (v apd.Decimal) { v.SetInt64(a); return }
+type textMarshaller struct {
+ b string
+}
+
+func (t *textMarshaller) MarshalText() (b []byte, err error) {
+ return []byte(t.b), nil
+}
+
+var _ encoding.TextMarshaler = &textMarshaller{}
+
func TestConvert(t *testing.T) {
+ type key struct {
+ a int
+ }
+ type stringType string
i34 := big.NewInt(34)
d35 := mkBigInt(35)
n36 := mkBigInt(-36)
@@ -51,7 +68,7 @@
}, {
"foo", `(string){ "foo" }`,
}, {
- "\x80", "(_|_){\n // [eval] cannot convert result to string: invalid UTF-8\n}",
+ "\x80", `(string){ "�" }`,
}, {
3, "(int){ 3 }",
}, {
@@ -198,7 +215,21 @@
A: (string){ "" }
B: (int){ 0 }
}`,
- }}
+ },
+ {map[key]string{{a: 1}: "foo"},
+ "(_|_){\n // [eval] unsupported Go type for map key (convert_test.key)\n}"},
+ {map[*textMarshaller]string{{b: "bar"}: "foo"},
+ "(struct){\n \"&{bar}\": (string){ \"foo\" }\n}"},
+ {map[int]string{1: "foo"},
+ "(struct){\n \"1\": (string){ \"foo\" }\n}"},
+ {map[string]encoding.TextMarshaler{"foo": nil},
+ "(struct){\n foo: (_){ _ }\n}"},
+ {make(chan int),
+ "(_|_){\n // [eval] unsupported Go type (chan int)\n}"},
+ {[]interface{}{func() {}},
+ "(_|_){\n // [eval] unsupported Go type (func())\n}"},
+ {stringType("\x80"), `(string){ "�" }`},
+ }
r := runtime.New()
for _, tc := range testCases {
ctx := adt.NewContext(r, &adt.Vertex{})