// 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("", "{}") 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 }