cue: implement closed structs for subsumption
Also make it clearer in the API when an operation
applies to only values and when to all fields,
including definitions.
Issue #40
Change-Id: Ibfbd5fa4262a234b3c93afdb2b02214cc8b36e5c
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2945
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/builtin.go b/cue/builtin.go
index f7dbefa..58d0bcf 100644
--- a/cue/builtin.go
+++ b/cue/builtin.go
@@ -120,7 +120,7 @@
v := c.value(0)
switch v.Kind() {
case StructKind:
- s, _ := v.structVal(c.ctx)
+ s, _ := v.structValData(c.ctx)
c.ret = s.Len()
case ListKind:
i := 0
diff --git a/cue/examples_test.go b/cue/examples_test.go
index d065e87..7505f3b 100644
--- a/cue/examples_test.go
+++ b/cue/examples_test.go
@@ -16,6 +16,7 @@
import (
"fmt"
+ "log"
"cuelang.org/go/cue"
)
@@ -64,3 +65,53 @@
// {2 4}
// json: cannot unmarshal string into Go struct field ab.B of type int
}
+
+func ExampleSubsumes() {
+ // Check compatibility of successive APIs.
+ var r cue.Runtime
+
+ inst, err := r.Compile("apis", `
+ // Release notes:
+ // - You can now specify your age and your hobby!
+ V1 :: {
+ age: >=0 & <=100
+ hobby: string
+ }
+
+ // Release notes:
+ // - People get to be older than 100, so we relaxed it.
+ // - It seems not many people have a hobby, so we made it optional.
+ V2 :: {
+ age: >=0 & <=150 // people get older now
+ hobby?: string // some people don't have a hobby
+ }
+
+ // Release notes:
+ // - Actually no one seems to have a hobby nowadays anymore,
+ // so we dropped the field.
+ V3 :: {
+ age: >=0 & <=150
+ }`)
+
+ if err != nil {
+ fmt.Println(err)
+ // handle error
+ }
+ v1, err1 := inst.LookupField("V1")
+ v2, err2 := inst.LookupField("V2")
+ v3, err3 := inst.LookupField("V3")
+ if err1 != nil || err2 != nil || err3 != nil {
+ log.Println(err1, err2, err3)
+ }
+
+ // Is V2 backwards compatible with V1? In other words, does V2 subsume V1?
+ pass := v2.Value.Subsumes(v1.Value)
+ fmt.Println("V2 is backwards compatible with V1:", pass)
+
+ pass = v3.Value.Subsumes(v2.Value)
+ fmt.Println("V3 is backwards compatible with V2:", pass)
+
+ // Output:
+ // V2 is backwards compatible with V1: true
+ // V3 is backwards compatible with V2: false
+}
diff --git a/cue/instance.go b/cue/instance.go
index d79a0cd..8493ae0 100644
--- a/cue/instance.go
+++ b/cue/instance.go
@@ -15,6 +15,8 @@
package cue
import (
+ goast "go/ast"
+
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/errors"
@@ -231,7 +233,7 @@
ctx := inst.newContext()
val := newValueRoot(ctx, inst.rootValue)
- v, err := val.structVal(ctx)
+ v, err := val.structValFull(ctx)
if err != nil {
i.setError(val.toErr(err))
return i
@@ -252,16 +254,17 @@
return i
}
-// Lookup reports the value starting from the top level struct (not the emitted
-// value), or an error if the path is not found.
-// The empty path returns the top-level configuration struct, regardless of
-// whether an emit value was specified.
+// Lookup reports the value at a path starting from the top level struct (not
+// the emitted value). The Exists method of the returned value will report false
+// if the path did not exist. The Err method reports if any error occurred
+// during evaluation. The empty path returns the top-level configuration struct,
+// regardless of whether an emit value was specified.
func (inst *Instance) Lookup(path ...string) Value {
idx := inst.index
ctx := idx.newContext()
v := newValueRoot(ctx, inst.rootValue)
for _, k := range path {
- obj, err := v.structVal(ctx)
+ obj, err := v.structValData(ctx)
if err != nil {
return Value{idx, &valueData{arc: arc{cache: err, v: err}}}
}
@@ -270,6 +273,31 @@
return v
}
+// LookupField reports a Field at a path starting from v, or an error if the
+// path is not. The empty path returns v itself.
+//
+// It cannot look up hidden or unexported fields.
+func (inst *Instance) LookupField(path ...string) (f FieldInfo, err error) {
+ idx := inst.index
+ ctx := idx.newContext()
+ v := newValueRoot(ctx, inst.rootValue)
+ for i, k := range path {
+ s, err := v.Struct()
+ if err != nil {
+ return f, err
+ }
+
+ f, err = s.FieldByName(k)
+ if err != nil {
+ return f, err
+ }
+ if f.IsHidden || (i == 0 || f.IsDefinition) && !goast.IsExported(f.Name) {
+ return f, errNotFound
+ }
+ }
+ return f, err
+}
+
// Fill creates a new instance with the values of the old instance unified with
// the given value. It is not possible to update the emit value.
func (inst *Instance) Fill(x interface{}, path ...string) (*Instance, error) {
diff --git a/cue/subsume.go b/cue/subsume.go
index 7b728af..0440e54 100644
--- a/cue/subsume.go
+++ b/cue/subsume.go
@@ -96,6 +96,8 @@
b := o.lookup(ctx, a.feature)
if !a.optional && b.optional {
return false
+ } else if a.definition != b.definition {
+ return false
} else if b.val() == nil {
// If field a is optional and has value top, neither the
// omission of the field nor the field defined with any value
@@ -105,6 +107,15 @@
return false
}
}
+ // For closed structs, all arcs in b must exist in a.
+ if x.isClosed {
+ for _, b := range o.arcs {
+ a := x.lookup(ctx, b.feature)
+ if a.val() == nil {
+ return false
+ }
+ }
+ }
}
return !isBottom(v)
}
diff --git a/cue/subsume_test.go b/cue/subsume_test.go
index 0a7d79a..97dd9eb 100644
--- a/cue/subsume_test.go
+++ b/cue/subsume_test.go
@@ -320,6 +320,9 @@
312: {subsumes: false, in: `a: !=2 & !=4, b: >3`},
313: {subsumes: true, in: `a: !=2 & !=4, b: >5`},
+ 314: {subsumes: false, in: `a: >=0 & <=100, b: >=0 & <=150`},
+ 315: {subsumes: true, in: `a: >=0 & <=150, b: >=0 & <=100`},
+
// Disjunctions
330: {subsumes: true, in: `a: >5, b: >10 | 8`},
331: {subsumes: false, in: `a: >8, b: >10 | 8`},
@@ -371,6 +374,18 @@
512: {subsumes: false, in: `a: [{b: "foo"}], b: [{b: string}] `},
513: {subsumes: false, in: `a: [{b: string}], b: [{b: "foo"}, ...{b: "foo"}] `},
520: {subsumes: false, in: `a: [_, int, ...], b: [int, string, ...string] `},
+
+ // Closed structs.
+ 600: {subsumes: false, in: `a: close({}), b: {a: 1}`},
+ 601: {subsumes: true, in: `a: close({a: 1}), b: {a: 1}`},
+ 602: {subsumes: false, in: `a: close({a: 1, b: 1}), b: {a: 1}`},
+ 603: {subsumes: false, in: `a: {a: 1}, b: close({})`},
+ 604: {subsumes: true, in: `a: {a: 1}, b: close({a: 1})`},
+ 605: {subsumes: true, in: `a: {a: 1}, b: close({a: 1, b: 1})`},
+
+ // Definitions are not values.
+ 610: {subsumes: false, in: `a: {a :: 1}, b: {a: 1}`},
+ 611: {subsumes: false, in: `a: {a: 1}, b: {a :: 1}`},
}
re := regexp.MustCompile(`a: (.*).*b: ([^\n]*)`)
@@ -395,7 +410,8 @@
b = arc.v
}
}
- if got := subsumes(ctx, a, b, tc.mode); got != tc.subsumes {
+ got := subsumes(ctx, a, b, tc.mode)
+ if got != tc.subsumes {
t.Errorf("got %v; want %v (%v vs %v)", got, tc.subsumes, a.kind(), b.kind())
}
})
diff --git a/cue/types.go b/cue/types.go
index c31a2d2..6ca8760 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -18,6 +18,7 @@
"bytes"
"encoding/json"
"fmt"
+ goast "go/ast"
"io"
"math"
"math/big"
@@ -716,7 +717,7 @@
i := Iterator{ctx: ctx, val: v, iter: l, len: len(l.elem.arcs)}
return marshalList(&i)
case structKind:
- obj, _ := v.structVal(ctx)
+ obj, _ := v.structValData(ctx)
return obj.marshalJSON()
case bottomKind:
return nil, toMarshalErr(v, x.(*bottom))
@@ -1033,13 +1034,17 @@
// a structVal.
// structVal returns an structVal or an error if v is not a struct.
-func (v Value) structVal(ctx *context) (structValue, *bottom) {
+func (v Value) structValData(ctx *context) (structValue, *bottom) {
return v.structValOpts(ctx, options{
omitHidden: true,
omitOptional: true,
})
}
+func (v Value) structValFull(ctx *context) (structValue, *bottom) {
+ return v.structValOpts(ctx, options{})
+}
+
// structVal returns an structVal or an error if v is not a struct.
func (v Value) structValOpts(ctx *context, o options) (structValue, *bottom) {
v, _ = v.Default() // TODO: remove?
@@ -1131,8 +1136,9 @@
Name string
Value Value
- IsOptional bool
- IsHidden bool
+ IsDefinition bool
+ IsOptional bool
+ IsHidden bool
}
// field reports information about the ith field, i < o.Len().
@@ -1143,17 +1149,17 @@
v := Value{ctx.index, &valueData{s.v.path, uint32(i), a}}
str := ctx.labelStr(a.feature)
- return FieldInfo{str, v, a.optional, a.feature&hidden != 0}
+ return FieldInfo{str, v, a.definition, a.optional, a.feature&hidden != 0}
}
-func (s *Struct) FieldByName(name string) (FieldInfo, bool) {
+func (s *Struct) FieldByName(name string) (FieldInfo, error) {
f := s.v.ctx().strLabel(name)
for i, a := range s.s.arcs {
if a.feature == f {
- return s.field(i), true
+ return s.field(i), nil
}
}
- return FieldInfo{}, false
+ return FieldInfo{}, errNotFound
}
// Fields creates an iterator over the Struct's fields.
@@ -1175,14 +1181,15 @@
return Iterator{ctx: ctx, val: v, iter: obj.n, len: len(obj.n.arcs)}, nil
}
-// Lookup reports the value starting from v, or an error if the path is not
-// found. The empty path returns v itself.
+// Lookup reports the value at a path starting from v.
+// The empty path returns v itself.
//
-// Lookup cannot be used to look up hidden fields.
+// The Exists() method can be used to verify if the returned value existed.
+// Lookup cannot be used to look up hidden or optional fields or definitions.
func (v Value) Lookup(path ...string) Value {
ctx := v.ctx()
for _, k := range path {
- obj, err := v.structVal(ctx)
+ obj, err := v.structValData(ctx)
if err != nil {
// TODO: return a Value at the same location and a new error?
return newValueRoot(ctx, err)
@@ -1192,6 +1199,25 @@
return v
}
+var errNotFound = errors.Newf(token.NoPos, "field not found")
+
+// LookupField reports information about a field of v.
+func (v Value) LookupField(path string) (FieldInfo, error) {
+ s, err := v.Struct()
+ if err != nil {
+ // TODO: return a Value at the same location and a new error?
+ return FieldInfo{}, err
+ }
+ f, err := s.FieldByName(path)
+ if err != nil {
+ return f, err
+ }
+ if f.IsHidden || f.IsDefinition && !goast.IsExported(path) {
+ return f, errNotFound
+ }
+ return f, err
+}
+
// Template returns a function that represents the template definition for a
// struct in a configuration file. It returns nil if v is not a struct kind or
// if there is no template associated with the struct.
@@ -1502,7 +1528,8 @@
}
// Walk descends into all values of v, calling f. If f returns false, Walk
-// will not descent further.
+// will not descent further. It only visits values that are part of the data
+// model, so this excludes optional fields, hidden fields, and definitions.
func (v Value) Walk(before func(Value) bool, after func(Value)) {
ctx := v.ctx()
switch v.Kind() {
@@ -1510,7 +1537,7 @@
if before != nil && !before(v) {
return
}
- obj, _ := v.structVal(ctx)
+ obj, _ := v.structValData(ctx)
for i := 0; i < obj.Len(); i++ {
_, v := obj.At(i)
v.Walk(before, after)
diff --git a/cue/types_test.go b/cue/types_test.go
index ea8ef72..764228a 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -1992,7 +1992,7 @@
ctx, st := compileFile(t, tc.config)
v := newValueRoot(ctx, st)
for _, k := range strings.Split(tc.in, ".") {
- obj, err := v.structVal(ctx)
+ obj, err := v.structValFull(ctx)
if err != nil {
t.Fatal(err)
}