cue: add function to convert Go type to a CUE value
Note that this is different from converting a value.
Updates #24
Change-Id: Ibb83c1e2169ee637c549d2961f54954581188503
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/1788
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/go.go b/cue/go.go
index a45fbc6..34755e4 100644
--- a/cue/go.go
+++ b/cue/go.go
@@ -15,27 +15,223 @@
package cue
import (
+ "encoding"
+ "encoding/json"
"fmt"
"math/big"
"reflect"
"sort"
"strings"
+ "sync"
- "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/parser"
"github.com/cockroachdb/apd"
)
// This file contains functionality for converting Go to CUE.
+func convertValue(inst *Instance, x interface{}) Value {
+ ctx := inst.index.newContext()
+ v := convert(ctx, baseValue{}, x)
+ return newValueRoot(ctx, v)
+}
+
+func convertType(inst *Instance, x interface{}) Value {
+ ctx := inst.index.newContext()
+ v := convertGoType(inst, reflect.TypeOf(x))
+ return newValueRoot(ctx, v)
+
+}
+
+// parseTag parses a CUE expression from a cue tag.
+func parseTag(ctx *context, obj *structLit, field label, tag string) value {
+ if p := strings.Index(tag, ","); p >= 0 {
+ tag = tag[:p]
+ }
+ if tag == "" {
+ return &top{}
+ }
+ expr, err := parser.ParseExpr(ctx.index.fset, "<field:>", tag)
+ if err != nil {
+ field := ctx.labelStr(field)
+ return ctx.mkErr(baseValue{}, "invalid tag %q for field %q: %v", tag, field, err)
+ }
+ v := newVisitor(ctx.index, nil, nil, obj)
+ return v.walk(expr)
+}
+
+// TODO: should we allow mapping names in cue tags? This only seems like a good
+// idea if we ever want to allow mapping CUE to a different name than JSON.
+var tagsWithNames = []string{"json", "yaml", "protobuf"}
+
+func getName(f *reflect.StructField) string {
+ name := f.Name
+ for _, s := range tagsWithNames {
+ if tag, ok := f.Tag.Lookup(s); ok {
+ if p := strings.Index(tag, ","); p >= 0 {
+ tag = tag[:p]
+ }
+ if tag != "" {
+ name = tag
+ break
+ }
+ }
+ }
+ return name
+}
+
+// isOptional indicates whether a field should be marked as optional.
+func isOptional(f *reflect.StructField) bool {
+ isOptional := false
+ switch f.Type.Kind() {
+ case reflect.Ptr, reflect.Map, reflect.Chan, reflect.Interface, reflect.Slice:
+ // Note: it may be confusing to distinguish between an empty slice and
+ // a nil slice. However, it is also surprizing to not be able to specify
+ // a default value for a slice. So for now we will allow it.
+ isOptional = true
+ }
+ if tag, ok := f.Tag.Lookup("cue"); ok {
+ // TODO: only if first field is not empty.
+ isOptional = false
+ for _, f := range strings.Split(tag, ",")[1:] {
+ switch f {
+ case "opt":
+ isOptional = true
+ case "req":
+ return false
+ }
+ }
+ } else if tag, ok = f.Tag.Lookup("json"); ok {
+ isOptional = false
+ for _, f := range strings.Split(tag, ",")[1:] {
+ if f == "omitempty" {
+ return true
+ }
+ }
+ }
+ return isOptional
+}
+
+// isOmitEmpty means that the zero value is interpreted as undefined.
+func isOmitEmpty(f *reflect.StructField) bool {
+ isOmitEmpty := false
+ switch f.Type.Kind() {
+ case reflect.Ptr, reflect.Map, reflect.Chan, reflect.Interface, reflect.Slice:
+ // Note: it may be confusing to distinguish between an empty slice and
+ // a nil slice. However, it is also surprizing to not be able to specify
+ // a default value for a slice. So for now we will allow it.
+ isOmitEmpty = true
+
+ default:
+ // TODO: we can also infer omit empty if a type cannot be nil if there
+ // is a constraint that unconditionally disallows the zero value.
+ }
+ tag, ok := f.Tag.Lookup("json")
+ if ok {
+ isOmitEmpty = false
+ for _, f := range strings.Split(tag, ",")[1:] {
+ if f == "omitempty" {
+ return true
+ }
+ }
+ }
+ return isOmitEmpty
+}
+
+// parseJSON parses JSON into a CUE value. b must be valid JSON.
+func parseJSON(ctx *context, b []byte) evaluated {
+ expr, err := parser.ParseExpr(ctx.index.fset, "json", b)
+ if err != nil {
+ panic(err) // cannot happen
+ }
+ v := newVisitor(ctx.index, nil, nil, nil)
+ return v.walk(expr).evalPartial(ctx)
+}
+
+func isZero(v reflect.Value) bool {
+ x := v.Interface()
+ if x == nil {
+ return true
+ }
+ switch k := v.Kind(); k {
+ case reflect.Struct, reflect.Array:
+ // we never allow optional values for these types.
+ return false
+
+ case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
+ reflect.Slice:
+ // Note that for maps we preserve the distinction between a nil map and
+ // an empty map.
+ return v.IsNil()
+
+ case reflect.String:
+ return v.Len() == 0
+
+ default:
+ return x == reflect.Zero(v.Type()).Interface()
+ }
+}
+
func convert(ctx *context, src source, x interface{}) evaluated {
switch v := x.(type) {
- case evaluated:
- return v
case nil:
- return &nullLit{src.base()}
- case ast.Expr:
- x := newVisitorCtx(ctx, nil, nil, nil)
- return ctx.manifest(x.walk(v))
+ // Interpret a nil pointer as an undefined value that is only
+ // null by default, but may still be set: *null | _.
+ return &disjunction{values: []dValue{
+ {val: &nullLit{src.base()}, marked: true},
+ {val: &top{src.base()}}},
+ }
+
+ case *big.Int:
+ n := newNum(src, intKind)
+ n.v.Coeff.Set(v)
+ if v.Sign() < 0 {
+ n.v.Coeff.Neg(&n.v.Coeff)
+ n.v.Negative = true
+ }
+ return n
+
+ case *big.Rat:
+ // should we represent this as a binary operation?
+ n := newNum(src, numKind)
+ ctx.Quo(&n.v, apd.NewWithBigInt(v.Num(), 0), apd.NewWithBigInt(v.Denom(), 0))
+ if !v.IsInt() {
+ n.k = floatKind
+ }
+ return n
+
+ case *big.Float:
+ n := newNum(src, floatKind)
+ n.v.SetString(v.String())
+ return n
+
+ case *apd.Decimal:
+ n := newNum(src, floatKind|intKind)
+ n.v.Set(v)
+ if !n.isInt(ctx) {
+ n.k = floatKind
+ }
+ return n
+
+ case json.Marshaler:
+ b, err := v.MarshalJSON()
+ if err != nil {
+ return ctx.mkErr(src, err)
+ }
+
+ return parseJSON(ctx, b)
+
+ case encoding.TextMarshaler:
+ b, err := v.MarshalText()
+ if err != nil {
+ return ctx.mkErr(src, err)
+ }
+ b, err = json.Marshal(string(b))
+ if err != nil {
+ return ctx.mkErr(src, err)
+ }
+ return parseJSON(ctx, b)
+
case error:
return ctx.mkErr(src, v.Error())
case bool:
@@ -68,32 +264,7 @@
r := newNum(src, floatKind)
r.v.SetString(fmt.Sprintf("%g", v))
return r
- case *big.Int:
- n := newNum(src, intKind)
- n.v.Coeff.Set(v)
- if v.Sign() < 0 {
- n.v.Coeff.Neg(&n.v.Coeff)
- n.v.Negative = true
- }
- return n
- case *big.Rat:
- n := newNum(src, numKind)
- ctx.Quo(&n.v, apd.NewWithBigInt(v.Num(), 0), apd.NewWithBigInt(v.Denom(), 0))
- if !v.IsInt() {
- n.k = floatKind
- }
- return n
- case *big.Float:
- n := newNum(src, floatKind)
- n.v.SetString(v.String())
- return n
- case *apd.Decimal:
- n := newNum(src, floatKind|intKind)
- n.v.Set(v)
- if !n.isInt(ctx) {
- n.k = floatKind
- }
- return n
+
case reflect.Value:
if v.CanInterface() {
return convert(ctx, src, v.Interface())
@@ -104,9 +275,15 @@
switch value.Kind() {
case reflect.Ptr:
if value.IsNil() {
- return &nullLit{src.base()}
+ // Interpret a nil pointer as an undefined value that is only
+ // null by default, but may still be set: *null | _.
+ return &disjunction{values: []dValue{
+ {val: &nullLit{src.base()}, marked: true},
+ {val: &top{src.base()}}},
+ }
}
return convert(ctx, src, value.Elem().Interface())
+
case reflect.Struct:
obj := newStruct(src)
t := value.Type()
@@ -115,20 +292,16 @@
if t.PkgPath != "" {
continue
}
- sub := convert(ctx, src, value.Field(i).Interface())
+ val := value.Field(i)
+ if isOmitEmpty(&t) && isZero(val) {
+ continue
+ }
+ sub := convert(ctx, src, val.Interface())
// leave errors like we do during normal evaluation or do we
// want to return the error?
- name := t.Name
- for _, s := range []string{"cue", "json", "protobuf"} {
- if tag, ok := t.Tag.Lookup(s); ok {
- if p := strings.Index(tag, ","); p >= 0 {
- tag = tag[:p]
- }
- if tag != "" {
- name = tag
- break
- }
- }
+ name := getName(&t)
+ if name == "-" {
+ continue
}
f := ctx.strLabel(name)
obj.arcs = append(obj.arcs, arc{feature: f, v: sub})
@@ -187,3 +360,191 @@
n.v.Coeff.SetUint64(x)
return n
}
+
+var (
+ typeCache sync.Map // map[reflect.Type]evaluated
+ mutex sync.Mutex
+)
+
+func convertGoType(inst *Instance, t reflect.Type) value {
+ ctx := inst.newContext()
+ // TODO: this can be much more efficient.
+ mutex.Lock()
+ defer mutex.Unlock()
+ return goTypeToValue(ctx, t)
+}
+
+var (
+ jsonMarshaler = reflect.TypeOf(new(json.Marshaler)).Elem()
+ textMarshaler = reflect.TypeOf(new(encoding.TextMarshaler)).Elem()
+ topSentinel = &top{}
+)
+
+// goTypeToValue converts a Go Type to a value.
+//
+// TODO: if this value will always be unified with a concrete type in Go, then
+// many of the fields may be omitted.
+func goTypeToValue(ctx *context, t reflect.Type) (e value) {
+ if e, ok := typeCache.Load(t); ok {
+ return e.(value)
+ }
+
+ // Even if this is for types that we know cast to a certain type, it can't
+ // hurt to return top, as in these cases the concrete values will be
+ // strict instances and there cannot be any tags that further constrain
+ // the values.
+ if t.Implements(jsonMarshaler) || t.Implements(textMarshaler) {
+ return topSentinel
+ }
+
+ switch k := t.Kind(); k {
+ case reflect.Ptr:
+ elem := t.Elem()
+ for elem.Kind() == reflect.Ptr {
+ elem = elem.Elem()
+ }
+ e = wrapOrNull(goTypeToValue(ctx, elem))
+
+ case reflect.Interface:
+ switch t.Name() {
+ case "error":
+ // This is really null | _|_. There is no error if the error is null.
+ e = &nullLit{} // null
+ default:
+ e = topSentinel // `_`
+ }
+
+ case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+ reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ e = predefinedRanges[t.Kind().String()]
+
+ case reflect.Uint, reflect.Uintptr:
+ e = predefinedRanges["uint64"]
+
+ case reflect.Int:
+ e = predefinedRanges["int64"]
+
+ case reflect.String:
+ e = &basicType{k: stringKind}
+
+ case reflect.Bool:
+ e = &basicType{k: boolKind}
+
+ case reflect.Float32, reflect.Float64:
+ e = &basicType{k: floatKind}
+
+ case reflect.Struct:
+ // Some of these values have MarshalJSON methods, but that may not be
+ // the case for older Go versions.
+ name := fmt.Sprint(t)
+ switch name {
+ case "big.Int":
+ e = &basicType{k: intKind}
+ goto store
+ case "big.Rat", "big.Float", "apd.Decimal":
+ e = &basicType{k: floatKind}
+ goto store
+ case "time.Time":
+ // We let the concrete value decide.
+ e = topSentinel
+ goto store
+ }
+
+ // First iterate to create struct, then iterate another time to
+ // resolve field tags to allow field tags to refer to the struct fields.
+ tags := map[label]string{}
+ obj := newStruct(baseValue{})
+ typeCache.Store(t, obj)
+
+ for i := 0; i < t.NumField(); i++ {
+ f := t.Field(i)
+ if f.PkgPath != "" {
+ continue
+ }
+ elem := goTypeToValue(ctx, f.Type)
+
+ // leave errors like we do during normal evaluation or do we
+ // want to return the error?
+ name := getName(&f)
+ if name == "-" {
+ continue
+ }
+ l := ctx.strLabel(name)
+ obj.arcs = append(obj.arcs, arc{
+ feature: l,
+ // The GO JSON decoder always allows a value to be undefined.
+ optional: isOptional(&f),
+ v: elem,
+ })
+
+ if tag, ok := f.Tag.Lookup("cue"); ok {
+ tags[l] = tag
+ }
+ }
+ sort.Sort(obj)
+
+ for label, tag := range tags {
+ v := parseTag(ctx, obj, label, tag)
+ for i, a := range obj.arcs {
+ if a.feature == label {
+ // Instead of unifying with the existing type, we substitute
+ // with the constraints from the tags. The type constraints
+ // will be implied when unified with a concrete value.
+ obj.arcs[i].v = mkBin(ctx, 0, opUnify, a.v, v)
+ // obj.arcs[i].v = v
+ }
+ }
+ }
+
+ return obj
+
+ case reflect.Array, reflect.Slice:
+ if t.Elem().Kind() == reflect.Uint8 {
+ e = &basicType{k: bytesKind}
+ } else {
+ elem := goTypeToValue(ctx, t.Elem())
+
+ var ln value = &top{}
+ if t.Kind() == reflect.Array {
+ ln = toInt(ctx, baseValue{}, int64(t.Len()))
+ }
+ e = &list{typ: elem, len: ln}
+ }
+ if k == reflect.Slice {
+ e = wrapOrNull(e)
+ }
+
+ case reflect.Map:
+ if key := t.Key(); key.Kind() != reflect.String {
+ // What does the JSON library do here?
+ e = ctx.mkErr(baseValue{}, "type %v not supported as key type", key)
+ break
+ }
+
+ obj := newStruct(baseValue{})
+ sig := ¶ms{}
+ sig.add(ctx.label("_", true), &basicType{k: stringKind})
+ v := goTypeToValue(ctx, t.Elem())
+ obj.template = &lambdaExpr{params: sig, value: v}
+
+ e = wrapOrNull(obj)
+ }
+
+store:
+ // TODO: store error if not nil?
+ if e != nil {
+ typeCache.Store(t, e)
+ }
+ return e
+}
+
+func wrapOrNull(e value) value {
+ if e.kind().isAnyOf(nullKind) {
+ return e
+ }
+ e = &disjunction{values: []dValue{
+ {val: &nullLit{}, marked: true},
+ {val: e}},
+ }
+ return e
+}
diff --git a/cue/go_test.go b/cue/go_test.go
index 88895c3..a2f1d92 100644
--- a/cue/go_test.go
+++ b/cue/go_test.go
@@ -15,24 +15,25 @@
package cue
import (
- "go/ast"
"math/big"
"reflect"
"testing"
"time"
+ "cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
)
func TestConvert(t *testing.T) {
i34 := big.NewInt(34)
d34 := mkBigInt(34)
+ n34 := mkBigInt(-34)
f34 := big.NewFloat(34.0000)
testCases := []struct {
goVal interface{}
want string
}{{
- nil, "null",
+ nil, "(*null | _)",
}, {
true, "true",
}, {
@@ -70,6 +71,8 @@
}, {
&d34, "34",
}, {
+ &n34, "-34",
+ }, {
[]int{1, 2, 3, 4}, "[1,2,3,4]",
}, {
[]interface{}{}, "[]",
@@ -109,7 +112,7 @@
}, {
&struct{ A int }{3}, "<0>{A: 3}",
}, {
- (*struct{ A int })(nil), "null",
+ (*struct{ A int })(nil), "(*null | _)",
}, {
reflect.ValueOf(3), "3",
}, {
@@ -128,3 +131,78 @@
})
}
}
+
+func TestConvertType(t *testing.T) {
+ testCases := []struct {
+ goTyp interface{}
+ want string
+ }{{
+ struct {
+ A int `cue:">=0&<100"`
+ B *big.Int `cue:">=0"`
+ C *big.Int
+ D big.Int
+ F *big.Float
+ }{},
+ // TODO: indicate that B is explicitly an int only.
+ `<0>{A: ((>=-9223372036854775808 & <=9223372036854775807) & (>=0 & <100)), ` +
+ `B: >=0, ` +
+ `C?: _, ` +
+ `D: int, ` +
+ `F?: _}`,
+ }, {
+ &struct {
+ A int16 `cue:">=0&<100"`
+ B error `json:"b"`
+ C string
+ D bool
+ F float64
+ L []byte
+ T time.Time
+ }{},
+ `(*null | <0>{A: ((>=-32768 & <=32767) & (>=0 & <100)), ` +
+ `C: string, ` +
+ `D: bool, ` +
+ `F: float, ` +
+ `b: null, ` +
+ `L?: (*null | bytes), ` +
+ `T: _})`,
+ }, {
+ struct {
+ A int8 `cue:"C-B"`
+ B int8 `cue:"C-A,opt"`
+ C int8 `cue:"A+B"`
+ }{},
+ // TODO: should B be marked as optional?
+ `<0>{A: ((>=-128 & <=127) & (<0>.C - <0>.B)), ` +
+ `B?: ((>=-128 & <=127) & (<0>.C - <0>.A)), ` +
+ `C: ((>=-128 & <=127) & (<0>.A + <0>.B))}`,
+ }, {
+ []string{},
+ `(*null | [, ...string])`,
+ }, {
+ [4]string{},
+ `4*[string]`,
+ }, {
+ map[string]struct{ A map[string]uint }{},
+ `(*null | ` +
+ `<0>{<>: <1>(_: string)-><2>{` +
+ `A?: (*null | ` +
+ `<3>{<>: <4>(_: string)->(>=0 & <=18446744073709551615), })}, })`,
+ }, {
+ map[float32]int{},
+ `_|_(type float32 not supported as key type)`,
+ }}
+ inst := getInstance(t, "foo")
+
+ for _, tc := range testCases {
+ ctx := inst.newContext()
+ t.Run("", func(t *testing.T) {
+ v := goTypeToValue(ctx, reflect.TypeOf(tc.goTyp))
+ got := debugStr(ctx, v)
+ if got != tc.want {
+ t.Errorf("\n got %q;\nwant %q", got, tc.want)
+ }
+ })
+ }
+}