From 311bfa788c9309be262575e6dc79b0ca9d2b1db2 Mon Sep 17 00:00:00 2001 From: Roger Peppe Date: Tue, 21 Apr 2026 14:24:38 +0100 Subject: [PATCH] cue/cuecontext: allow multiple injections for a given kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's sometimes useful to be able to have multiple injections active for a particular kind, for example to have some kind of aspect-oriented checking that guards an already present injection. This change makes it so that new injections unify with existing injections rather than replacing them. Signed-off-by: Roger Peppe Change-Id: Ib2135df7d8d725df63425309458b87155e174cc2 Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1235993 Unity-Result: CUE porcuepine TryBot-Result: CUEcueckoo Reviewed-by: Daniel Martí --- cue/cuecontext/cuecontext.go | 6 +- cue/cuecontext/multi_inject_test.go | 160 ++++++++++++++++++++++++++++ internal/core/runtime/extern.go | 73 ++++++++++++- 3 files changed, 232 insertions(+), 7 deletions(-) create mode 100644 cue/cuecontext/multi_inject_test.go diff --git a/cue/cuecontext/cuecontext.go b/cue/cuecontext/cuecontext.go index 9ae775e72..b8d4fa13b 100644 --- a/cue/cuecontext/cuecontext.go +++ b/cue/cuecontext/cuecontext.go @@ -45,7 +45,7 @@ type Option struct { func New(options ...Option) *cue.Context { r := runtime.New() // Embedding is always available. - r.SetInjection(embed.New()) + r.AddInjection(embed.New()) for _, o := range options { o.apply(r) } @@ -65,9 +65,11 @@ func Interpreter(i ExternInterpreter) Option { } // WithInjection associates an injection for external code with this context. +// Note that several injections can be associated with the same extern +// kind; if so, all apply and their results are unifed. func WithInjection(i Injection) Option { return Option{func(r *runtime.Runtime) { - r.SetInjection(i) + r.AddInjection(i) }} } diff --git a/cue/cuecontext/multi_inject_test.go b/cue/cuecontext/multi_inject_test.go new file mode 100644 index 000000000..b3f28dd1c --- /dev/null +++ b/cue/cuecontext/multi_inject_test.go @@ -0,0 +1,160 @@ +// Copyright 2026 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 cuecontext + +import ( + "testing" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/build" + "cuelang.org/go/cue/errors" + "cuelang.org/go/cue/token" + "cuelang.org/go/internal/core/adt" + "cuelang.org/go/internal/core/runtime" + + qt "github.com/go-quicktest/qt" +) + +func TestMultiInjectionUnifiesValues(t *testing.T) { + inj1 := &constInjection{ + kind: "test", + value: &adt.BasicType{K: adt.StringKind}, + } + inj2 := &constInjection{ + kind: "test", + value: &adt.String{Str: "hello"}, + } + ctx := New(WithInjection(inj1), WithInjection(inj2)) + + v := ctx.CompileString(` + @extern(test) + + package foo + + x: _ @test() + `) + qt.Assert(t, qt.IsNil(v.Err())) + + x := v.LookupPath(cue.ParsePath("x")) + qt.Assert(t, qt.IsNil(x.Err())) + + got, err := x.String() + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(got, "hello")) +} + +func TestMultiInjectionThreeInjectors(t *testing.T) { + inj1 := &constInjection{ + kind: "test", + value: &adt.BasicType{K: adt.StringKind}, + } + inj2 := &constInjection{ + kind: "test", + value: &adt.String{Str: "hello"}, + } + inj3 := &constInjection{ + kind: "test", + value: &adt.BasicType{K: adt.StringKind}, + } + ctx := New(WithInjection(inj1), WithInjection(inj2), WithInjection(inj3)) + + v := ctx.CompileString(` + @extern(test) + + package foo + + x: _ @test() + `) + qt.Assert(t, qt.IsNil(v.Err())) + + x := v.LookupPath(cue.ParsePath("x")) + qt.Assert(t, qt.IsNil(x.Err())) + + got, err := x.String() + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(got, "hello")) +} + +func TestMultiInjectionInjectedValueError(t *testing.T) { + inj1 := &constInjection{ + kind: "test", + value: &adt.String{Str: "hello"}, + } + inj2 := &constInjection{ + kind: "test", + valueErr: errors.Newf(token.NoPos, "injector2 failed"), + } + ctx := New(WithInjection(inj1), WithInjection(inj2)) + + v := ctx.CompileString(` + @extern(test) + + package foo + + x: _ @test() + `) + qt.Assert(t, qt.IsNotNil(v.Err())) + qt.Assert(t, qt.ErrorMatches(v.Err(), `.*injector2 failed.*`)) +} + +func TestMultiInjectionInstanceError(t *testing.T) { + inj1 := &constInjection{ + kind: "test", + value: &adt.String{Str: "hello"}, + } + inj2 := &constInjection{ + kind: "test", + instanceErr: errors.Newf(token.NoPos, "instance init failed"), + } + ctx := New(WithInjection(inj1), WithInjection(inj2)) + + v := ctx.CompileString(` + @extern(test) + + package foo + + x: _ @test() + `) + qt.Assert(t, qt.IsNotNil(v.Err())) + qt.Assert(t, qt.ErrorMatches(v.Err(), `.*instance init failed.*`)) +} + +type constInjection struct { + kind string + value adt.Expr + valueErr errors.Error + instanceErr errors.Error +} + +func (i *constInjection) Kind() string { return i.kind } + +func (i *constInjection) InjectorForInstance(_ *build.Instance, _ *runtime.Runtime) (runtime.Injector, errors.Error) { + if i.instanceErr != nil { + return nil, i.instanceErr + } + return &constInjector{ + value: i.value, + err: i.valueErr, + }, nil +} + +type constInjector struct { + value adt.Expr + err errors.Error +} + +func (j *constInjector) InjectedValue(_ *runtime.ExternAttr, _ *adt.Vertex) (adt.Expr, errors.Error) { + return j.value, j.err +} diff --git a/internal/core/runtime/extern.go b/internal/core/runtime/extern.go index f88c6ba3a..a2df1872d 100644 --- a/internal/core/runtime/extern.go +++ b/internal/core/runtime/extern.go @@ -26,13 +26,25 @@ import ( "cuelang.org/go/internal/core/walk" ) -// SetInjection sets the injection value to be used for injection +// AddInjection sets an injection value to be used for injection // of values with an @extern(kind) attribute where kind is i.Kind(). -func (r *Runtime) SetInjection(i Injection) { +// If there's more than one injection registered for a given kind, +// the results from all of them will be unified. +func (r *Runtime) AddInjection(i Injection) { if r.injections == nil { r.injections = map[string]Injection{} } - r.injections[i.Kind()] = i + switch ij := r.injections[i.Kind()].(type) { + case nil: + r.injections[i.Kind()] = i + case *multiInjection: + ij.injections = append(ij.injections, i) + default: + r.injections[i.Kind()] = &multiInjection{ + kind: i.Kind(), + injections: []Injection{ij, i}, + } + } } // Injection defines an entrypoint for creating per-instance injectors. @@ -405,6 +417,57 @@ func (d *externDecorator) injectedValue(astAttr *ast.Attribute, parent ast.Node, return b } -func ref[T any](x T) *T { - return &x +// multiInjection combines multiple [Injection] implementations +// into a single implementation that unifies the results from all of them. +type multiInjection struct { + kind string + injections []Injection +} + +func (ij *multiInjection) Kind() string { + return ij.kind +} + +func (ij *multiInjection) InjectorForInstance(b *build.Instance, r *Runtime) (Injector, errors.Error) { + var injectors multiInjector + var errs errors.Error + for _, inj := range ij.injections { + injector, err := inj.InjectorForInstance(b, r) + if err != nil { + errs = errors.Append(errs, err) + continue + } + injectors = append(injectors, injector) + } + if errs != nil { + return nil, errs + } + return injectors, nil +} + +type multiInjector []Injector + +func (ij multiInjector) InjectedValue(attr *ExternAttr, scope *adt.Vertex) (adt.Expr, errors.Error) { + var expr adt.Expr + var errs errors.Error + for _, inj := range ij { + e, err := inj.InjectedValue(attr, scope) + if err != nil { + errs = errors.Append(errs, err) + continue + } + if expr == nil { + expr = e + } else { + expr = &adt.BinaryExpr{ + Op: adt.AndOp, + X: expr, + Y: e, + } + } + } + if errs != nil { + return nil, errs + } + return expr, nil } -- 2.51.2