Skip to content

Commit a623df2

Browse files
committed
fix targets indexing and field access
1 parent f6aea6f commit a623df2

File tree

2 files changed

+148
-0
lines changed

2 files changed

+148
-0
lines changed

internal/component/discovery/target_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,115 @@ import (
1515
"github.com/stretchr/testify/require"
1616

1717
"github.com/grafana/alloy/internal/runtime/equality"
18+
"github.com/grafana/alloy/syntax"
1819
"github.com/grafana/alloy/syntax/parser"
1920
"github.com/grafana/alloy/syntax/token/builder"
2021
"github.com/grafana/alloy/syntax/vm"
2122
)
2223

24+
func TestUsingTargetCapsule(t *testing.T) {
25+
type testCase struct {
26+
name string
27+
inputTarget map[string]string
28+
expression string
29+
decodeInto interface{}
30+
decodedString string
31+
expectedEvalError string
32+
}
33+
34+
testCases := []testCase{
35+
{
36+
name: "target to map of string -> string",
37+
inputTarget: map[string]string{"a1a": "beachfront avenue", "ice": "ice"},
38+
expression: "t",
39+
decodeInto: map[string]string{},
40+
decodedString: `{"a1a"="beachfront avenue", "ice"="ice"}`,
41+
},
42+
{
43+
name: "target to map of string -> any",
44+
inputTarget: map[string]string{"a1a": "beachfront avenue", "ice": "ice"},
45+
expression: "t",
46+
decodeInto: map[string]any{},
47+
decodedString: `{"a1a"="beachfront avenue", "ice"="ice"}`,
48+
},
49+
{
50+
name: "target to map of any -> any",
51+
inputTarget: map[string]string{"a1a": "beachfront avenue", "ice": "ice"},
52+
expression: "t",
53+
decodeInto: map[any]any{},
54+
decodedString: `{"a1a"="beachfront avenue", "ice"="ice"}`,
55+
},
56+
{
57+
name: "target to map of string -> syntax.Value",
58+
inputTarget: map[string]string{"a1a": "beachfront avenue"},
59+
expression: "t",
60+
decodeInto: map[string]syntax.Value{},
61+
decodedString: `{"a1a"="beachfront avenue"}`,
62+
},
63+
{
64+
name: "target indexing a string value",
65+
inputTarget: map[string]string{"a1a": "beachfront avenue", "hip": "hop"},
66+
expression: `t["hip"]`,
67+
decodeInto: "",
68+
decodedString: `hop`,
69+
},
70+
{
71+
name: "target indexing a non-existing string value",
72+
inputTarget: map[string]string{"a1a": "beachfront avenue", "hip": "hop"},
73+
expression: `t["boom"]`,
74+
decodeInto: "",
75+
decodedString: "<nil>",
76+
},
77+
{
78+
name: "target indexing a value like an object field",
79+
inputTarget: map[string]string{"a1a": "beachfront avenue", "hip": "hop"},
80+
expression: `t.boom`,
81+
decodeInto: "",
82+
expectedEvalError: `field "boom" does not exist`,
83+
},
84+
}
85+
for _, tc := range testCases {
86+
t.Run(tc.name, func(t *testing.T) {
87+
target := NewTargetFromMap(tc.inputTarget)
88+
scope := vm.NewScope(map[string]interface{}{"t": target})
89+
expr, err := parser.ParseExpression(tc.expression)
90+
require.NoError(t, err)
91+
eval := vm.New(expr)
92+
evalError := eval.Evaluate(scope, &tc.decodeInto)
93+
if tc.expectedEvalError != "" {
94+
require.ErrorContains(t, evalError, tc.expectedEvalError)
95+
} else {
96+
require.NoError(t, evalError)
97+
}
98+
require.Equal(t, tc.decodedString, fmt.Sprintf("%v", tc.decodeInto))
99+
})
100+
}
101+
}
102+
103+
func TestNestedIndexing(t *testing.T) {
104+
targets := []Target{
105+
NewTargetFromMap(map[string]string{"foo": "bar", "boom": "bap"}),
106+
NewTargetFromMap(map[string]string{"hip": "hop", "dont": "stop"}),
107+
}
108+
scope := vm.NewScope(map[string]interface{}{"targets": targets})
109+
110+
expr, err := parser.ParseExpression(`targets[1]["dont"]`)
111+
require.NoError(t, err)
112+
eval := vm.New(expr)
113+
actual := ""
114+
err = eval.Evaluate(scope, &actual)
115+
require.NoError(t, err)
116+
require.Equal(t, "stop", actual)
117+
118+
expr, err = parser.ParseExpression(`targets[0].boom`)
119+
require.NoError(t, err)
120+
eval = vm.New(expr)
121+
actual = ""
122+
err = eval.Evaluate(scope, &actual)
123+
require.NoError(t, err)
124+
require.Equal(t, "bap", actual)
125+
}
126+
23127
func TestDecodeMap(t *testing.T) {
24128
type testCase struct {
25129
name string

syntax/vm/vm.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,27 @@ func (vm *Evaluator) evaluateExpr(scope *Scope, assoc map[value.Value]ast.Node,
356356
}
357357

358358
switch val.Type() {
359+
case value.TypeCapsule:
360+
if val.Implements(reflect.TypeFor[value.ConvertibleIntoCapsule]()) {
361+
// Check if this capsule can be converted into Alloy object for more detailed description:
362+
newVal := make(map[string]value.Value)
363+
if err := val.ReflectAddr().Interface().(value.ConvertibleIntoCapsule).ConvertInto(&newVal); err == nil {
364+
field, ok := newVal[expr.Name.Name]
365+
if !ok {
366+
return value.Null, diag.Diagnostic{
367+
Severity: diag.SeverityLevelError,
368+
StartPos: ast.StartPos(expr.Name).Position(),
369+
EndPos: ast.EndPos(expr.Name).Position(),
370+
Message: fmt.Sprintf("field %q does not exist", expr.Name.Name),
371+
}
372+
}
373+
return field, nil
374+
}
375+
}
376+
return value.Null, value.Error{
377+
Value: val,
378+
Inner: fmt.Errorf("expected object or array or a capsule convertible to an object, got %s", val.Type()),
379+
}
359380
case value.TypeObject:
360381
res, ok := val.Key(expr.Name.Name)
361382
if !ok {
@@ -413,6 +434,29 @@ func (vm *Evaluator) evaluateExpr(scope *Scope, assoc map[value.Value]ast.Node,
413434
}
414435
return field, nil
415436

437+
case value.TypeCapsule:
438+
if val.Implements(reflect.TypeFor[value.ConvertibleIntoCapsule]()) {
439+
// Check if this capsule can be converted into Alloy object for more detailed description:
440+
newVal := make(map[string]value.Value)
441+
if err := val.ReflectAddr().Interface().(value.ConvertibleIntoCapsule).ConvertInto(&newVal); err == nil {
442+
// Objects are indexed with a string.
443+
if idx.Type() != value.TypeString {
444+
return value.Null, value.TypeError{Value: idx, Expected: value.TypeString}
445+
}
446+
447+
field, ok := newVal[idx.Text()]
448+
if !ok {
449+
// If a key doesn't exist in an object accessed with [], return null.
450+
return value.Null, nil
451+
}
452+
return field, nil
453+
}
454+
}
455+
return value.Null, value.Error{
456+
Value: val,
457+
Inner: fmt.Errorf("expected object or array or a capsule convertible to an object, got %s", val.Type()),
458+
}
459+
416460
default:
417461
return value.Null, value.Error{
418462
Value: val,

0 commit comments

Comments
 (0)