-
Notifications
You must be signed in to change notification settings - Fork 292
/
cuego.go
211 lines (184 loc) · 5.86 KB
/
cuego.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// 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
}