cuego: new package for using CUE constrainst in Go

Updates #24

Change-Id: Ib6e173af2b8a805d2109bf291048a9fca8b72d61
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/1789
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/go.go b/cue/go.go
index 34755e4..491571a 100644
--- a/cue/go.go
+++ b/cue/go.go
@@ -25,10 +25,24 @@
 	"sync"
 
 	"cuelang.org/go/cue/parser"
+	"cuelang.org/go/internal"
 	"github.com/cockroachdb/apd"
 )
 
 // This file contains functionality for converting Go to CUE.
+//
+// The code in this file is a prototype implementation and is far from
+// optimized.
+
+func init() {
+	internal.FromGoValue = func(instance, x interface{}) interface{} {
+		return convertValue(instance.(*Instance), x)
+	}
+
+	internal.FromGoType = func(instance, x interface{}) interface{} {
+		return convertType(instance.(*Instance), x)
+	}
+}
 
 func convertValue(inst *Instance, x interface{}) Value {
 	ctx := inst.index.newContext()
diff --git a/cuego/cuego.go b/cuego/cuego.go
new file mode 100644
index 0000000..aaa8cc3
--- /dev/null
+++ b/cuego/cuego.go
@@ -0,0 +1,214 @@
+// 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 cuego
+
+import (
+	"fmt"
+	"reflect"
+	"sync"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/build"
+	"cuelang.org/go/cue/parser"
+	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal"
+)
+
+// DefaultContext is the shared context used with top-level functions.
+var DefaultContext = &Context{}
+
+// MustConstrain is like Constrain, but panics if there is an error.
+func MustConstrain(x interface{}, constraints string) {
+	if err := Constrain(x, constraints); err != nil {
+		panic(err)
+	}
+}
+
+// Constrain associates the given CUE constraints with the type of x or
+// reports an error if the constraints are invalid or not compatible with x.
+//
+// Constrain works across package boundaries and is typically called in the
+// package defining the type. Use a Context to apply constraints locally.
+func Constrain(x interface{}, constraints string) error {
+	return DefaultContext.Constrain(x, constraints)
+}
+
+// Validate is a wrapper for Validate called on the global context.
+func Validate(x interface{}) error {
+	return DefaultContext.Validate(x)
+}
+
+// Complete sets previously undefined values in x that can be uniquely
+// determined form the constraints defined on the type of x such that validation
+// passes, or returns an error, without modifying anything, if this is not
+// possible.
+//
+// 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 Complete(x interface{}) error {
+	return DefaultContext.Complete(x)
+}
+
+// A Context holds type constraints that are only applied within a given
+// context.
+// Global constraints that are defined at the time a constraint is
+// created are applied as well.
+type Context struct {
+	typeCache sync.Map // map[reflect.Type]cue.Value
+}
+
+// Validate checks whether x validates against the registered constraints for
+// the type of x.
+//
+// Constraints for x can be defined as field tags or through the Register
+// function.
+func (c *Context) Validate(x interface{}) error {
+	a := c.load(x)
+	v, err := fromGoValue(x)
+	if err != nil {
+		return err
+	}
+	v = a.Unify(v)
+	if err := v.Validate(); err != nil {
+		return err
+	}
+	// TODO: validate all values are concrete. (original value subsumes result?)
+	return nil
+}
+
+// Complete sets previously undefined values in x that can be uniquely
+// determined form the constraints defined on the type of x such that validation
+// passes, or returns an error, without modifying anything, if this is not
+// possible.
+//
+// A value is considered undefined if it is pointer type and is nil or if it
+// is a field with a zero value and a json tag with the omitempty tag.
+// 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 *Context) Complete(x interface{}) error {
+	a := c.load(x)
+	v, err := fromGoValue(x)
+	if err != nil {
+		return err
+	}
+	v = a.Unify(v)
+	if err := v.Validate(); err != nil {
+		return err
+	}
+	return v.Decode(x)
+}
+
+func (c *Context) load(x interface{}) cue.Value {
+	t := reflect.TypeOf(x)
+	if value, ok := c.typeCache.Load(t); ok {
+		return value.(cue.Value)
+	}
+
+	// fromGoType should prevent the work is done no more than once, but even
+	// if it is, there is no harm done.
+	v := fromGoType(x)
+	c.typeCache.Store(t, v)
+	return v
+}
+
+// TODO: should we require that Constrain be defined on exported,
+// named types types only?
+
+// Constrain associates the given CUE constraints with the type of x or reports
+// an error if the constraints are invalid or not compatible with x.
+func (c *Context) Constrain(x interface{}, constraints string) error {
+	c.load(x) // Ensure fromGoType is called outside of lock.
+
+	mutex.Lock()
+	defer mutex.Unlock()
+
+	expr, err := parser.ParseExpr(fset, fmt.Sprintf("<%T>", x), constraints)
+	if err != nil {
+		return err
+	}
+
+	v := instance.Eval(expr)
+	if v.Err() != nil {
+		return err
+	}
+
+	typ := c.load(x)
+	v = typ.Unify(v)
+
+	if err := v.Validate(); err != nil {
+		return err
+	}
+
+	t := reflect.TypeOf(x)
+	c.typeCache.Store(t, v)
+	return nil
+}
+
+var (
+	mutex    sync.Mutex
+	instance *cue.Instance
+	fset     *token.FileSet
+)
+
+func init() {
+	context := build.NewContext()
+	fset = context.FileSet()
+	inst := context.NewInstance("<cuego>", nil)
+	if err := inst.AddFile("<ceugo>", "{}"); err != nil {
+		panic(err)
+	}
+	instance = cue.Build([]*build.Instance{inst})[0]
+	if err := instance.Err; err != nil {
+		panic(err)
+	}
+}
+
+// fromGoValue converts a Go value to CUE
+func fromGoValue(x interface{}) (v cue.Value, err error) {
+	// TODO: remove the need to have a lock here. We could use a new index (new
+	// Instance) here as any previously unrecognized field can never match an
+	// existing one and can only be merged.
+	mutex.Lock()
+	v = internal.FromGoValue(instance, x).(cue.Value)
+	mutex.Unlock()
+	return v, nil
+
+	// // This should be equivalent to the following:
+	// b, err := json.Marshal(x)
+	// if err != nil {
+	// 	return v, err
+	// }
+	// expr, err := parser.ParseExpr(fset, "", b)
+	// if err != nil {
+	// 	return v, err
+	// }
+	// mutex.Lock()
+	// v = instance.Eval(expr)
+	// mutex.Unlock()
+	// return v, nil
+
+}
+
+func fromGoType(x interface{}) cue.Value {
+	// TODO: remove the need to have a lock here. We could use a new index (new
+	// Instance) here as any previously unrecognized field can never match an
+	// existing one and can only be merged.
+	mutex.Lock()
+	v := internal.FromGoType(instance, x).(cue.Value)
+	mutex.Unlock()
+	return v
+}
diff --git a/cuego/cuego_test.go b/cuego/cuego_test.go
new file mode 100644
index 0000000..7094e9e
--- /dev/null
+++ b/cuego/cuego_test.go
@@ -0,0 +1,205 @@
+// 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 cuego
+
+import (
+	"reflect"
+	"testing"
+)
+
+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",
+		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),
+		// }, {
+		// // TODO: figure out whether this constraint should constrain it
+		// // to a struct or not.
+		// name:        "*Sum: nil disallowed by constraint",
+		// value:       (*Sum)(nil),
+		// constraints: "!=null",
+		// err:         fail,
+	}, {
+		// 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) {
+			c := &Context{}
+			if tc.constraints != "" {
+				err := c.Constrain(tc.value, tc.constraints)
+				if err != nil {
+					t.Fatal(err)
+				}
+			}
+			err := c.Validate(tc.value)
+			checkErr(t, err, tc.err)
+		})
+	}
+}
+
+func TestUpdate(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) {
+			c := &Context{}
+			if tc.constraints != "" {
+				err := c.Constrain(tc.value, tc.constraints)
+				if err != nil {
+					t.Fatal(err)
+				}
+			}
+			err := c.Complete(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)
+			}
+		})
+	}
+}
diff --git a/cuego/doc.go b/cuego/doc.go
new file mode 100644
index 0000000..de3b2a1
--- /dev/null
+++ b/cuego/doc.go
@@ -0,0 +1,65 @@
+// 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 cuego allows using CUE constraints in Go programs.
+//
+// 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.
+//
+// CUE constraints can be added through field tags or by associating
+// CUE code with a Go type. The field tags method follows the usual
+// Go pattern:
+//
+//     type Sum struct {
+//         A int `cue:"C-B" json:",omitempty"`
+//         B int `cue:"C-A" json:",omitempty"`
+//         C int `cue:"A+B" json:",omitempty"`
+//     }
+//
+//     func main() {
+//         fmt.Println(cuego.Validate(&Sum{A: 1, B: 5, C: 7}))
+//     }
+//
+// AddConstraints allows annotating Go types with any CUE constraints.
+//
+//
+// Validating Go Values
+//
+// To check whether a struct's values satisfy its constraints, call Validate:
+//
+//   if err := cuego.Validate(p); err != nil {
+//      return err
+//   }
+//
+// Validation assumes that all values are filled in correctly and will not
+// infer values. To automatically infer values, use Complete.
+//
+//
+// Completing Go Values
+//
+// Package cuego can also be used to infer undefined values from a set of
+// CUE constraints, for instance to fill out fields in a struct. A value
+// is considered undefined if it is a nil pointer type or if it is a zero
+// value and there is a JSON field tag with the omitempty flag.
+// A Complete will implicitly validate a struct.
+//
+package cuego // import "cuelang.org/go/cuego"
+
+// The first goal of this packages is to get the semantics right. After that,
+// there are a lot of performance gains to be made:
+// - cache the type info extracted during value (as opposed to type) conversion
+// - remove the usage of mutex for value conversions
+// - avoid the JSON round trip for Decode, as used in Complete
+// - generate native code for validating and updating
diff --git a/cuego/examples_test.go b/cuego/examples_test.go
new file mode 100644
index 0000000..0a0c0f4
--- /dev/null
+++ b/cuego/examples_test.go
@@ -0,0 +1,96 @@
+// 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 cuego_test
+
+import (
+	"fmt"
+
+	"cuelang.org/go/cuego"
+)
+
+func ExampleComplete_structTag() {
+	type Sum struct {
+		A int `cue:"C-B" json:",omitempty"`
+		B int `cue:"C-A" json:",omitempty"`
+		C int `cue:"A+B" json:",omitempty"`
+	}
+
+	a := Sum{A: 1, B: 5}
+	err := cuego.Complete(&a)
+	fmt.Printf("completed: %#v (err: %v)\n", a, err)
+
+	a = Sum{A: 2, C: 8}
+	err = cuego.Complete(&a)
+	fmt.Printf("completed: %#v (err: %v)\n", a, err)
+
+	a = Sum{A: 2, B: 3, C: 8}
+	err = cuego.Complete(&a)
+	fmt.Println(err)
+
+	//Output:
+	// completed: cuego_test.Sum{A:1, B:5, C:6} (err: <nil>)
+	// completed: cuego_test.Sum{A:2, B:6, C:8} (err: <nil>)
+	// empty disjunction: unsupported op &(null, struct)
+}
+
+func ExampleConstrain() {
+	type Config struct {
+		Filename string
+		OptFile  string `json:",omitempty"`
+		MaxCount int
+		MinCount int
+
+		// TODO: show a field with time.Time
+	}
+
+	err := cuego.Constrain(&Config{}, `{
+		jsonFile = =~".json$"
+
+		// Filename must be defined and have a .json extension
+		Filename: jsonFile
+
+		// OptFile must be undefined or be a file name with a .json extension
+		OptFile?: jsonFile
+
+		MinCount: >0 & <=MaxCount
+		MaxCount: <=10_000
+	}`)
+
+	fmt.Println("error:", err)
+
+	fmt.Println("validate:", cuego.Validate(&Config{
+		Filename: "foo.json",
+		MaxCount: 1200,
+		MinCount: 39,
+	}))
+
+	fmt.Println("validate:", cuego.Validate(&Config{
+		Filename: "foo.json",
+		MaxCount: 12,
+		MinCount: 39,
+	}))
+
+	fmt.Println("validate:", cuego.Validate(&Config{
+		Filename: "foo.jso",
+		MaxCount: 120,
+		MinCount: 39,
+	}))
+
+	//Output:
+	// error: <nil>
+	// validate: <nil>
+	// validate: 39 not within bound <=12
+	// validate: "foo.jso" does not match =~".json$"
+}
diff --git a/internal/internal.go b/internal/internal.go
index 112cdd4..f9ff342 100644
--- a/internal/internal.go
+++ b/internal/internal.go
@@ -29,3 +29,13 @@
 //
 // TODO: extract interface
 var EvalExpr func(value, expr interface{}) (result interface{})
+
+// FromGoValue converts an arbitrary Go value to the corresponding CUE value.
+// instance must be of type *cue.Instance.
+// The returned value is a cue.Value, which the caller must cast to.
+var FromGoValue func(instance, x interface{}) interface{}
+
+// FromGoType converts an arbitrary Go type to the corresponding CUE value.
+// instance must be of type *cue.Instance.
+// The returned value is a cue.Value, which the caller must cast to.
+var FromGoType func(instance, x interface{}) interface{}