Skip to content

Commit

Permalink
cue: better document Context.Encode and friends
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
mpvl committed Apr 30, 2021
1 parent 97cac92 commit 67c6b6f
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 19 deletions.
80 changes: 80 additions & 0 deletions cue/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,86 @@ func NilIsAny(isAny bool) EncodeOption {
//
// 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:
Expand Down
5 changes: 2 additions & 3 deletions cue/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1585,7 +1585,8 @@ func (v hiddenValue) Fill(x interface{}, path ...string) Value {
// 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.
Expand All @@ -1606,8 +1607,6 @@ func (v Value) FillPath(p Path, x interface{}) Value {
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)
Expand Down
34 changes: 34 additions & 0 deletions cue/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,40 @@ func TestFillPath(t *testing.T) {
}
}

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{}

Expand Down
5 changes: 5 additions & 0 deletions internal/core/adt/composite.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,12 @@ func (v *Vertex) Err(c *OpContext, state VertexStatus) *Bottom {
// 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) {
Expand Down
32 changes: 18 additions & 14 deletions internal/core/convert/go.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import (
"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"
Expand Down Expand Up @@ -209,7 +209,7 @@ func isZero(v reflect.Value) bool {
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
}
Expand Down Expand Up @@ -321,10 +321,8 @@ func convertRec(ctx *adt.OpContext, nilIsTop bool, x interface{}) adt.Value {
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:
Expand Down Expand Up @@ -377,9 +375,11 @@ func convertRec(ctx *adt.OpContext, nilIsTop bool, x interface{}) adt.Value {

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,
Expand Down Expand Up @@ -475,11 +475,17 @@ func convertRec(ctx *adt.OpContext, nilIsTop bool, x interface{}) adt.Value {

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])
Expand All @@ -494,7 +500,7 @@ func convertRec(ctx *adt.OpContext, nilIsTop bool, x interface{}) adt.Value {
// 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
Expand All @@ -514,9 +520,6 @@ func convertRec(ctx *adt.OpContext, nilIsTop bool, x interface{}) adt.Value {
}
v.Arcs = append(v.Arcs, arc)
}

default:
return ctx.AddErrf("unsupported Go type for map key (%v)", key)
}

return v
Expand All @@ -528,7 +531,8 @@ func convertRec(ctx *adt.OpContext, nilIsTop bool, x interface{}) adt.Value {
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
Expand Down
35 changes: 33 additions & 2 deletions internal/core/convert/go_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@

package convert_test

// TODO: generate tests from Go's json encoder.

import (
"encoding"
"math/big"
"reflect"
"testing"
Expand All @@ -32,7 +35,21 @@ import (

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)
Expand All @@ -51,7 +68,7 @@ func TestConvert(t *testing.T) {
}, {
"foo", `(string){ "foo" }`,
}, {
"\x80", "(_|_){\n // [eval] cannot convert result to string: invalid UTF-8\n}",
"\x80", `(string){ "�" }`,
}, {
3, "(int){ 3 }",
}, {
Expand Down Expand Up @@ -198,7 +215,21 @@ func TestConvert(t *testing.T) {
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{})
Expand Down

0 comments on commit 67c6b6f

Please sign in to comment.