-
Notifications
You must be signed in to change notification settings - Fork 14
/
lineage.go
270 lines (230 loc) · 7.54 KB
/
lineage.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
package thema
import (
"fmt"
"sort"
"cuelang.org/go/cue"
terrors "github.com/grafana/thema/errors"
"github.com/grafana/thema/internal/util"
)
var (
_ Lineage = &UnaryLineage{}
_ ConvergentLineage[Assignee] = &unaryConvLineage[Assignee]{}
)
// A UnaryLineage is a Go facade over a valid CUE lineage that does not compose
// other lineage.
type UnaryLineage struct {
validated bool
name string
raw cue.Value
rt *Runtime
allv []SyntacticVersion
allsch []*UnarySchema
}
func defPathFor(name string, v SyntacticVersion) cue.Path {
return cue.MakePath(cue.Def(fmt.Sprintf("%s%v%v", name, v[0], v[1])))
}
// 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, rt *Runtime, opts ...BindOption) (Lineage, error) {
// We could be more selective than this, but this isn't supposed to be forever, soooooo
rt.l()
defer rt.u()
p := raw.Path().String()
// The candidate lineage must exist.
if !raw.Exists() {
if p != "" {
return nil, fmt.Errorf("%w: path was %q", terrors.ErrValueNotExist, p)
}
return nil, terrors.ErrValueNotExist
}
if p == "" {
p = "instance root"
}
// The candidate lineage must be error-free.
// TODO replace this with Err, this check isn't actually what we want up here. Only schemas themselves must be cycle-free
if err := raw.Validate(cue.Concrete(false), cue.DisallowCycles(true)); err != nil {
return nil, err
}
// The candidate lineage must be an instance of #Lineage.
dlin := rt.linDef()
err := dlin.Subsume(raw, cue.Raw(), cue.Schema(), cue.Final())
if err != nil {
// FIXME figure out how to wrap both the sentinel and CUE error sanely
return nil, fmt.Errorf("%w (%s): %s", terrors.ErrValueNotALineage, p, err)
}
nam, err := raw.LookupPath(cue.MakePath(cue.Str("name"))).String()
if err != nil {
return nil, fmt.Errorf("%w (%s): name field is not concrete", terrors.ErrInvalidLineage, p)
}
cfg := &bindConfig{}
for _, opt := range opts {
opt(cfg)
}
lin := &UnaryLineage{
validated: true,
raw: raw,
rt: rt,
name: nam,
}
// Populate the version list and enforce compat/subsumption invariants
seqiter, _ := raw.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() {
v := synv(seqv, schv)
lin.allv = append(lin.allv, v)
sch := schiter.Value()
defname := fmt.Sprintf("%s%v%v", util.SanitizeLabelString(nam), v[0], v[1])
defpath := cue.MakePath(cue.Def(defname))
defsch := rt.Context().
CompileString(fmt.Sprintf("#%s: _", defname)).
FillPath(defpath, sch).
LookupPath(defpath)
if defsch.Validate() != nil {
panic(defsch.Validate())
}
lin.allsch = append(lin.allsch, &UnarySchema{
raw: sch,
defraw: defsch,
lin: lin,
v: v,
})
// No predecessor to compare against with the very first schema
if !(schv == 0 && seqv == 0) {
// TODO Marked as buggy until we figure out how to both _not_ require
// schema to be closed in the .cue file, _and_ how to detect default changes
if !cfg.skipbuggychecks {
// The sequences and schema in the candidate lineage must follow
// backwards [in]compatibility rules.
// TODO Subsumption may not be what we actually want to check here,
// as it does not allow the addition of required fields with defaults
bcompat := sch.Subsume(predecessor, cue.Raw(), cue.Schema(), cue.Definitions(true), cue.All(), cue.Final())
if (schv == 0 && bcompat == nil) || (schv != 0 && bcompat != nil) {
return nil, &compatInvariantError{
rawlin: raw,
violation: [2]SyntacticVersion{predsv, v},
detail: bcompat,
}
}
}
}
predecessor = sch
predsv = v
schv++
}
seqv++
}
return lin, nil
}
// Runtime returns the thema.Runtime instance with which this lineage was built.
func (lin *UnaryLineage) Runtime() *Runtime {
return lin.rt
}
// Latest returns the newest Schema in the lineage - largest minor version
// within the largest major version.
func (lin *UnaryLineage) Latest() Schema {
return lin.allsch[len(lin.allsch)-1]
}
// First returns the first Schema in the lineage (v0.0). Thema requires that all
// valid lineages contain at least one schema, so this is guaranteed to exist.
func (lin *UnaryLineage) First() Schema {
return lin.allsch[0]
}
func isValidLineage(lin Lineage) {
switch tlin := lin.(type) {
case nil:
panic("nil lineage")
case *UnaryLineage:
if !tlin.validated {
panic("lineage not validated")
}
default:
panic("unreachable")
}
}
func getLinLib(lin Lineage) *Runtime {
switch tlin := lin.(type) {
case *UnaryLineage:
return tlin.rt
default:
panic("unreachable")
}
}
// Underlying returns the cue.Value of the entire lineage.
func (lin *UnaryLineage) Underlying() cue.Value {
isValidLineage(lin)
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 {
isValidLineage(lin)
if !lin.validated {
panic("lineage not validated")
}
return lin.name
}
// ValidateAny checks that the provided data is valid with respect to at
// least one of the schemas in the lineage. The oldest (smallest) schema against
// which the data validates is chosen. A nil return indicates no validating
// schema was found.
//
// While this method 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 Underlying().
//
// TODO should this instead be interface{} (ugh ugh wish Go had tagged unions) like FillPath?
func (lin *UnaryLineage) ValidateAny(data cue.Value) *Instance {
isValidLineage(lin)
for sch := lin.schema(synv()); sch != nil; sch = sch.successor() {
if inst, err := sch.Validate(data); err == nil {
return inst
}
}
return nil
}
// Schema returns the schema identified by the provided version, if one exists.
//
// Only the [0, 0] schema is guaranteed to exist in all valid lineages.
func (lin *UnaryLineage) Schema(v SyntacticVersion) (Schema, error) {
isValidLineage(lin)
if !synvExists(lin.allv, v) {
return nil, &ErrNoSchemaWithVersion{
lin: lin,
v: v,
}
}
return lin.schema(v), nil
}
func (lin *UnaryLineage) schema(v SyntacticVersion) *UnarySchema {
return lin.allsch[searchSynv(lin.allv, v)]
}
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) })
}
func synvExists(a []SyntacticVersion, x SyntacticVersion) bool {
i := searchSynv(a, x)
return i < len(a) && a[i] == x
}
type unaryConvLineage[T Assignee] struct {
Lineage
tsch TypedSchema[T]
}
func (lin *unaryConvLineage[T]) TypedSchema() TypedSchema[T] {
return lin.tsch
}