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