| // 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/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(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 |
| ) |
| |
| func init() { |
| context := build.NewContext() |
| inst := context.NewInstance("<cuego>", nil) |
| if err := inst.AddFile("<cuego>", "{}"); 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 |
| } |