blob: ba72bdbdfbeda40f33b4582be4fa8d3c3ee7acf5 [file] [log] [blame]
// 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/cuecontext"
"cuelang.org/go/cue/parser"
"cuelang.org/go/internal/value"
)
// 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, false)
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, true)
if err != nil {
return err
}
v = a.Unify(v)
if err := v.Validate(cue.Concrete(true)); 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(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
runtime = cuecontext.New()
)
func init() {
var err error
instance, err = value.ConvertToRuntime(runtime).Compile("<cuego>", "{}")
if err != nil {
panic(err)
}
}
// fromGoValue converts a Go value to CUE
func fromGoValue(x interface{}, nilIsNull bool) (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 = value.FromGoValue(runtime, x, nilIsNull)
mutex.Unlock()
if err := v.Err(); err != nil {
return v, err
}
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 := value.FromGoType(runtime, x)
mutex.Unlock()
return v
}