From 957003c092fd439a0a54e899f0ae5aa3f1a1b867 Mon Sep 17 00:00:00 2001 From: Marcel van Lohuizen Date: Thu, 8 Apr 2021 10:26:31 +0200 Subject: [PATCH] cue: support optional field lookup in LookupPath This is done by allowing Selectors to be converted to an optional form (if they aren't already). It also supports the use of the Selectors AnyIndex and AnyString for getting `T` for `[string]: T` and `[...T]`. This also allows deprecating Elem and Template, which will be done in a separate CL. Change-Id: I6074382f12259c3dd87557471d80f82720a84779 Reviewed-on: https://cue-review.googlesource.com/c/cue/+/9347 Reviewed-by: CUE cueckoo Reviewed-by: Marcel van Lohuizen --- cue/path.go | 48 +++++++++++-- cue/query.go | 18 ++++- cue/query_test.go | 125 ++++++++++++++++++++++++++++++++++ internal/core/adt/feature.go | 1 + internal/core/adt/optional.go | 14 ++-- 5 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 cue/query_test.go diff --git a/cue/path.go b/cue/path.go index 29edd0b28..4ffb2dbef 100644 --- a/cue/path.go +++ b/cue/path.go @@ -70,11 +70,18 @@ var ( anyLabel = Selector{sel: anySelector(adt.AnyRegular)} ) +// Optional converts sel into an optional equivalent. +// foo -> foo? +func (sel Selector) Optional() Selector { + return wrapOptional(sel) +} + type selector interface { String() string feature(ctx adt.Runtime) adt.Feature kind() adt.FeatureType + optional() bool } // A Path is series of selectors to query a CUE value. @@ -144,6 +151,17 @@ func (p Path) String() string { return b.String() } +// Optional returns the optional form of a Path. For instance, +// foo.bar --> foo?.bar? +// +func (p Path) Optional() Path { + q := make([]Selector, 0, len(p.path)) + for _, s := range p.path { + q = appendSelector(q, wrapOptional(s)) + } + return Path{path: q} +} + func toSelectors(expr ast.Expr) []Selector { switch x := expr.(type) { case *ast.Ident: @@ -293,6 +311,7 @@ type scopedSelector struct { func (s scopedSelector) String() string { return s.name } +func (scopedSelector) optional() bool { return false } func (s scopedSelector) kind() adt.FeatureType { switch { @@ -331,6 +350,8 @@ func (d definitionSelector) String() string { return string(d) } +func (d definitionSelector) optional() bool { return false } + func (d definitionSelector) kind() adt.FeatureType { return adt.DefinitionLabel } @@ -354,6 +375,7 @@ func (s stringSelector) String() string { return str } +func (s stringSelector) optional() bool { return false } func (s stringSelector) kind() adt.FeatureType { return adt.StringLabel } func (s stringSelector) feature(r adt.Runtime) adt.Feature { @@ -376,6 +398,7 @@ func (s indexSelector) String() string { } func (s indexSelector) kind() adt.FeatureType { return adt.IntLabel } +func (s indexSelector) optional() bool { return false } func (s indexSelector) feature(r adt.Runtime) adt.Feature { return adt.Feature(s) @@ -385,6 +408,7 @@ func (s indexSelector) feature(r adt.Runtime) adt.Feature { type anySelector adt.Feature func (s anySelector) String() string { return "_" } +func (s anySelector) optional() bool { return true } func (s anySelector) kind() adt.FeatureType { return adt.Feature(s).Typ() } func (s anySelector) feature(r adt.Runtime) adt.Feature { @@ -398,16 +422,27 @@ func (s anySelector) feature(r adt.Runtime) adt.Feature { // func ImportPath(s string) Selector { // return importSelector(s) // } +type optionalSelector struct { + selector +} -// type importSelector string +func wrapOptional(sel Selector) Selector { + if !sel.sel.optional() { + sel = Selector{optionalSelector{sel.sel}} + } + return sel +} -// func (s importSelector) String() string { -// return literal.String.Quote(string(s)) +// func isOptional(sel selector) bool { +// _, ok := sel.(optionalSelector) +// return ok // } -// func (s importSelector) feature(r adt.Runtime) adt.Feature { -// return adt.InvalidLabel -// } +func (s optionalSelector) optional() bool { return true } + +func (s optionalSelector) String() string { + return s.selector.String() + "?" +} // TODO: allow looking up in parent scopes? @@ -429,6 +464,7 @@ type pathError struct { } func (p pathError) String() string { return p.Error.Error() } +func (p pathError) optional() bool { return false } func (p pathError) kind() adt.FeatureType { return 0 } func (p pathError) feature(r adt.Runtime) adt.Feature { return adt.InvalidLabel diff --git a/cue/query.go b/cue/query.go index 3cef7c260..542a9af5c 100644 --- a/cue/query.go +++ b/cue/query.go @@ -55,7 +55,12 @@ func resolveExpr(ctx *context, v *adt.Vertex, x ast.Expr) adt.Value { // LookupPath reports the value for path p relative to v. func (v Value) LookupPath(p Path) Value { + if v.v == nil { + return Value{} + } n := v.v + ctx := v.ctx().opCtx + outer: for _, sel := range p.path { f := sel.sel.feature(v.idx.Runtime) @@ -65,7 +70,18 @@ outer: continue outer } } - // TODO: if optional, look up template for name. + if sel.sel.optional() { + x := &adt.Vertex{ + Parent: v.v, + Label: sel.sel.feature(ctx), + } + n.MatchAndInsert(ctx, x) + if len(x.Conjuncts) > 0 { + x.Finalize(ctx) + n = x + continue + } + } var x *adt.Bottom if err, ok := sel.sel.(pathError); ok { diff --git a/cue/query_test.go b/cue/query_test.go new file mode 100644 index 000000000..5301be2d3 --- /dev/null +++ b/cue/query_test.go @@ -0,0 +1,125 @@ +// Copyright 2021 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 cue_test + +import ( + "bytes" + "testing" + + "cuelang.org/go/cue" + "cuelang.org/go/internal/diff" +) + +func TestLookupPath(t *testing.T) { + r := &cue.Runtime{} + + testCases := []struct { + in string + path cue.Path + out string `test:"update"` // :nerdSnipe: + notExist bool `test:"update"` // :nerdSnipe: + }{{ + in: ` + [Name=string]: { a: Name } + `, + path: cue.MakePath(cue.Str("a")), + notExist: true, + }, { + in: ` + #V: { + x: int + } + #X: { + [string]: int64 + } & #V + v: #X + `, + path: cue.ParsePath("v.x"), + out: `int64`, + }, { + in: ` + a: [...int] + `, + path: cue.MakePath(cue.Str("a"), cue.AnyIndex), + out: `int`, + }, { + in: ` + [Name=string]: { a: Name } + `, + path: cue.MakePath(cue.AnyString, cue.Str("a")), + out: `string`, + }, { + in: ` + [Name=string]: { a: Name } + `, + path: cue.MakePath(cue.Str("b").Optional(), cue.Str("a")), + out: `"b"`, + }, { + in: ` + [Name=string]: { a: Name } + `, + path: cue.MakePath(cue.AnyString), + out: `{a: string}`, + }, { + in: ` + a: [Foo=string]: [Bar=string]: { b: Foo+Bar } + `, + path: cue.MakePath(cue.Str("a"), cue.Str("b"), cue.Str("c")).Optional(), + out: `{b: "bc"}`, + }, { + in: ` + a: [Foo=string]: b: [Bar=string]: { c: Foo } + a: foo: b: [Bar=string]: { d: Bar } + `, + path: cue.MakePath(cue.Str("a"), cue.Str("foo"), cue.Str("b"), cue.AnyString), + out: `{c: "foo", d: string}`, + }, { + in: ` + [Name=string]: { a: Name } + `, + path: cue.MakePath(cue.Str("a")), + notExist: true, + }} + for _, tc := range testCases { + t.Run(tc.path.String(), func(t *testing.T) { + v := compileT(t, r, tc.in) + + v = v.LookupPath(tc.path) + + if exists := v.Exists(); exists != !tc.notExist { + t.Fatalf("exists: got %v; want: %v", exists, !tc.notExist) + } else if !exists { + return + } + + w := compileT(t, r, tc.out) + + if k, d := diff.Diff(v, w); k != diff.Identity { + b := &bytes.Buffer{} + diff.Print(b, d) + t.Error(b) + } + }) + } +} + +func compileT(t *testing.T, r *cue.Runtime, s string) cue.Value { + t.Helper() + inst, err := r.Compile("", s) + if err != nil { + t.Fatal(err) + } + return inst.Value() +} diff --git a/internal/core/adt/feature.go b/internal/core/adt/feature.go index d2f4111e5..d6f44b262 100644 --- a/internal/core/adt/feature.go +++ b/internal/core/adt/feature.go @@ -127,6 +127,7 @@ func (f Feature) ToValue(ctx *OpContext) Value { if !f.IsRegular() { panic("not a regular label") } + // TODO: Handle special regular values: invalid and AnyRegular. if f.IsInt() { return ctx.NewInt64(int64(f.Index())) } diff --git a/internal/core/adt/optional.go b/internal/core/adt/optional.go index bcb8e652c..a58947a84 100644 --- a/internal/core/adt/optional.go +++ b/internal/core/adt/optional.go @@ -36,18 +36,22 @@ outer: } } - if !arc.Label.IsRegular() { + f := arc.Label + if !f.IsRegular() { return } + if int64(f.Index()) == MaxIndex { + f = 0 + } var label Value - if o.types&HasComplexPattern != 0 && arc.Label.IsString() { - label = arc.Label.ToValue(c) + if o.types&HasComplexPattern != 0 && f.IsString() { + label = f.ToValue(c) } if len(o.Bulk) > 0 { bulkEnv := *env - bulkEnv.DynamicLabel = arc.Label + bulkEnv.DynamicLabel = f bulkEnv.Deref = nil bulkEnv.Cycles = nil @@ -56,7 +60,7 @@ outer: // if matched && f.additional { // continue // } - if matchBulk(c, env, b, arc.Label, label) { + if matchBulk(c, env, b, f, label) { matched = true info := closeInfo.SpawnSpan(b.Value, ConstraintSpan) arc.AddConjunct(MakeConjunct(&bulkEnv, b, info))