-
Notifications
You must be signed in to change notification settings - Fork 12
/
impl.go
271 lines (233 loc) · 6.85 KB
/
impl.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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
package thema
import (
"fmt"
"sort"
"cuelang.org/go/cue"
)
// ErrValueNotExist indicates that an operation failed because a provided
// cue.Value does not exist.
type ErrValueNotExist struct {
path string
}
func (e *ErrValueNotExist) Error() string {
return fmt.Sprintf("value from path %q does not exist, absent values cannot be lineages", e.path)
}
// BindLineage takes a raw cue.Value, checks that it is a valid lineage (that it
// upholds the invariants which undergird Thema's translatability guarantees),
// and returns the cue.Value wrapped in a Lineage, iff validity checks succeed.
// The Lineage type provides access to all the types and functions for working
// with Thema in Go.
//
// This function is the sole intended mechanism for creating Lineage objects,
// thereby providing a practical promise that all instances of Lineage uphold
// Thema's invariants. It is primarily intended for use by authors of lineages
// in the creation of a LineageFactory.
func BindLineage(raw cue.Value, lib Library, opts ...BindOption) (Lineage, error) {
// The candidate lineage must exist.
if !raw.Exists() {
return nil, &ErrValueNotExist{
path: raw.Path().String(),
}
}
// The candidate lineage must be error-free.
if err := raw.Validate(cue.Concrete(false)); err != nil {
return nil, err
}
// The candidate lineage must be an instance of #Lineage.
dlin := lib.linDef()
if err := dlin.Subsume(raw, cue.Raw(), cue.Schema()); err != nil {
return nil, err
}
cfg := &bindConfig{}
for _, opt := range opts {
opt(cfg)
}
if !cfg.skipbuggychecks {
// The sequences and schema in the candidate lineage must follow
// backwards [in]compatibility rules.
if err := verifySeqCompatInvariants(raw, lib); err != nil {
return nil, err
}
}
lin := &UnaryLineage{
validated: true,
raw: raw,
lib: lib,
}
allv, err := cueArgs{
"lin": raw,
}.call("_allv", lin.lib)
if err != nil {
// This can't happen without a name change or something
panic(err)
}
_ = allv.Decode(&lin.allv)
return lin, nil
}
func getLinLib(lin Lineage) Library {
switch tlin := lin.(type) {
case *UnaryLineage:
return tlin.lib
default:
panic("unreachable")
}
}
type compatInvariantError struct {
rawlin cue.Value
violation [2]SyntacticVersion
detail error
}
func (e *compatInvariantError) Error() string {
panic("TODO")
}
// Assumes that lin has already been verified to be subsumed by #Lineage
func verifySeqCompatInvariants(lin cue.Value, lib Library) error {
seqiter, _ := lin.LookupPath(cue.MakePath(cue.Str("seqs"))).List()
var seqv uint
var predecessor cue.Value
var predsv SyntacticVersion
for seqiter.Next() {
var schv uint
schemas := seqiter.Value().LookupPath(cue.MakePath(cue.Str("schemas")))
schiter, _ := schemas.List()
for schiter.Next() {
if schv == 0 && seqv == 0 {
// Very first schema, no predecessor to compare against
continue
}
sch := schiter.Value()
bcompat := sch.Subsume(predecessor, cue.Raw(), cue.Schema())
if (schv == 0 && bcompat == nil) || (schv != 0 && bcompat != nil) {
return &compatInvariantError{
rawlin: lin,
violation: [2]SyntacticVersion{predsv, {seqv, schv}},
detail: bcompat,
}
}
predecessor = sch
predsv = SyntacticVersion{seqv, schv}
schv++
}
seqv++
}
return nil
}
// A UnaryLineage is a Go facade over a valid CUE lineage that does not compose
// other lineage.
type UnaryLineage struct {
validated bool
name string
first Schema
raw cue.Value
lib Library
allv []SyntacticVersion
}
// First returns the first schema in the lineage.
func (lin *UnaryLineage) First() Schema {
if !lin.validated {
panic("lineage not validated")
}
return lin.first
}
// RawValue returns the cue.Value of the entire lineage.
func (lin *UnaryLineage) RawValue() cue.Value {
if !lin.validated {
panic("lineage not validated")
}
return lin.raw
}
// Name returns the name of the object schematized by the lineage, as declared in
// the lineage's name field.
func (lin *UnaryLineage) Name() string {
if !lin.validated {
panic("lineage not validated")
}
return lin.name
}
func (lin *UnaryLineage) _lineage() {}
func searchSynv(a []SyntacticVersion, x SyntacticVersion) int {
return sort.Search(len(a), func(i int) bool { return !a[i].less(x) })
}
// A UnarySchema is a Go facade over a Thema schema that does not compose any
// schemas from any other lineages.
type UnarySchema struct {
raw cue.Value
lin *UnaryLineage
v SyntacticVersion
}
// Validate checks that the provided data is valid with respect to the
// schema. If valid, the data is wrapped in an Instance and returned.
// Otherwise, a nil Instance is returned along with an error detailing the
// validation failure.
//
// While Validate takes a cue.Value, this is only to avoid having to trigger
// the translation internally; input values must be concrete. To use
// incomplete CUE values with Thema schemas, prefer working directly in CUE,
// or if you must, rely on the RawValue().
//
// TODO should this instead be interface{} (ugh ugh wish Go had tagged unions) like FillPath?
func (sch *UnarySchema) Validate(data cue.Value) (*Instance, error) {
err := sch.raw.Subsume(data, cue.Concrete(true))
if err != nil {
return nil, err
}
return &Instance{
raw: data,
sch: sch,
name: "", // FIXME how are we getting this out?
}, nil
}
// Successor returns the next schema in the lineage, or nil if it is the last schema.
func (sch *UnarySchema) Successor() Schema {
if sch.lin.allv[len(sch.lin.allv)-1] == sch.v {
return nil
}
succv := sch.lin.allv[searchSynv(sch.lin.allv, sch.v)+1]
succ, _ := Pick(sch.lin, succv)
return succ
}
// Predecessor returns the previous schema in the lineage, or nil if it is the first schema.
func (sch *UnarySchema) Predecessor() Schema {
if sch.v == synv() {
return nil
}
predv := sch.lin.allv[searchSynv(sch.lin.allv, sch.v)-1]
pred, _ := Pick(sch.lin, predv)
return pred
}
// RawValue returns the cue.Value that represents the underlying CUE schema.
func (sch *UnarySchema) RawValue() cue.Value {
return sch.raw
}
// Version returns the schema's version number.
func (sch *UnarySchema) Version() SyntacticVersion {
return sch.v
}
// Lineage returns the lineage that contains this schema.
func (sch *UnarySchema) Lineage() Lineage {
return sch.lin
}
func (sch *UnarySchema) _schema() {}
// Call with no args to get init v, {0, 0}
// Call with one to get first version in a seq, {x, 0}
// Call with two because smooth brackets are prettier than curly
// Call with three or more because len(synv) < len(panic)
func synv(v ...uint) SyntacticVersion {
switch len(v) {
case 0:
return SyntacticVersion{0, 0}
case 1:
return SyntacticVersion{v[0], 0}
case 2:
return SyntacticVersion{v[0], v[1]}
default:
panic("cmon")
}
}
func tosynv(v cue.Value) SyntacticVersion {
var sv SyntacticVersion
if err := v.Decode(&sv); err != nil {
panic(err)
}
return sv
}