Skip to content

Commit

Permalink
fix targets indexing and field access
Browse files Browse the repository at this point in the history
  • Loading branch information
thampiotr committed Mar 5, 2025
1 parent f6aea6f commit a623df2
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 0 deletions.
104 changes: 104 additions & 0 deletions internal/component/discovery/target_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,115 @@ import (
"github.com/stretchr/testify/require"

"github.com/grafana/alloy/internal/runtime/equality"
"github.com/grafana/alloy/syntax"
"github.com/grafana/alloy/syntax/parser"
"github.com/grafana/alloy/syntax/token/builder"
"github.com/grafana/alloy/syntax/vm"
)

func TestUsingTargetCapsule(t *testing.T) {
type testCase struct {
name string
inputTarget map[string]string
expression string
decodeInto interface{}
decodedString string
expectedEvalError string
}

testCases := []testCase{
{
name: "target to map of string -> string",
inputTarget: map[string]string{"a1a": "beachfront avenue", "ice": "ice"},
expression: "t",
decodeInto: map[string]string{},
decodedString: `{"a1a"="beachfront avenue", "ice"="ice"}`,
},
{
name: "target to map of string -> any",
inputTarget: map[string]string{"a1a": "beachfront avenue", "ice": "ice"},
expression: "t",
decodeInto: map[string]any{},
decodedString: `{"a1a"="beachfront avenue", "ice"="ice"}`,
},
{
name: "target to map of any -> any",
inputTarget: map[string]string{"a1a": "beachfront avenue", "ice": "ice"},
expression: "t",
decodeInto: map[any]any{},
decodedString: `{"a1a"="beachfront avenue", "ice"="ice"}`,
},
{
name: "target to map of string -> syntax.Value",
inputTarget: map[string]string{"a1a": "beachfront avenue"},
expression: "t",
decodeInto: map[string]syntax.Value{},
decodedString: `{"a1a"="beachfront avenue"}`,
},
{
name: "target indexing a string value",
inputTarget: map[string]string{"a1a": "beachfront avenue", "hip": "hop"},
expression: `t["hip"]`,
decodeInto: "",
decodedString: `hop`,
},
{
name: "target indexing a non-existing string value",
inputTarget: map[string]string{"a1a": "beachfront avenue", "hip": "hop"},
expression: `t["boom"]`,
decodeInto: "",
decodedString: "<nil>",
},
{
name: "target indexing a value like an object field",
inputTarget: map[string]string{"a1a": "beachfront avenue", "hip": "hop"},
expression: `t.boom`,
decodeInto: "",
expectedEvalError: `field "boom" does not exist`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
target := NewTargetFromMap(tc.inputTarget)
scope := vm.NewScope(map[string]interface{}{"t": target})
expr, err := parser.ParseExpression(tc.expression)
require.NoError(t, err)
eval := vm.New(expr)
evalError := eval.Evaluate(scope, &tc.decodeInto)
if tc.expectedEvalError != "" {
require.ErrorContains(t, evalError, tc.expectedEvalError)
} else {
require.NoError(t, evalError)
}
require.Equal(t, tc.decodedString, fmt.Sprintf("%v", tc.decodeInto))
})
}
}

func TestNestedIndexing(t *testing.T) {
targets := []Target{
NewTargetFromMap(map[string]string{"foo": "bar", "boom": "bap"}),
NewTargetFromMap(map[string]string{"hip": "hop", "dont": "stop"}),
}
scope := vm.NewScope(map[string]interface{}{"targets": targets})

expr, err := parser.ParseExpression(`targets[1]["dont"]`)
require.NoError(t, err)
eval := vm.New(expr)
actual := ""
err = eval.Evaluate(scope, &actual)
require.NoError(t, err)
require.Equal(t, "stop", actual)

expr, err = parser.ParseExpression(`targets[0].boom`)
require.NoError(t, err)
eval = vm.New(expr)
actual = ""
err = eval.Evaluate(scope, &actual)
require.NoError(t, err)
require.Equal(t, "bap", actual)
}

func TestDecodeMap(t *testing.T) {
type testCase struct {
name string
Expand Down
44 changes: 44 additions & 0 deletions syntax/vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,27 @@ func (vm *Evaluator) evaluateExpr(scope *Scope, assoc map[value.Value]ast.Node,
}

switch val.Type() {
case value.TypeCapsule:
if val.Implements(reflect.TypeFor[value.ConvertibleIntoCapsule]()) {
// Check if this capsule can be converted into Alloy object for more detailed description:
newVal := make(map[string]value.Value)
if err := val.ReflectAddr().Interface().(value.ConvertibleIntoCapsule).ConvertInto(&newVal); err == nil {
field, ok := newVal[expr.Name.Name]
if !ok {
return value.Null, diag.Diagnostic{
Severity: diag.SeverityLevelError,
StartPos: ast.StartPos(expr.Name).Position(),
EndPos: ast.EndPos(expr.Name).Position(),
Message: fmt.Sprintf("field %q does not exist", expr.Name.Name),
}
}
return field, nil
}
}
return value.Null, value.Error{
Value: val,
Inner: fmt.Errorf("expected object or array or a capsule convertible to an object, got %s", val.Type()),
}
case value.TypeObject:
res, ok := val.Key(expr.Name.Name)
if !ok {
Expand Down Expand Up @@ -413,6 +434,29 @@ func (vm *Evaluator) evaluateExpr(scope *Scope, assoc map[value.Value]ast.Node,
}
return field, nil

case value.TypeCapsule:
if val.Implements(reflect.TypeFor[value.ConvertibleIntoCapsule]()) {
// Check if this capsule can be converted into Alloy object for more detailed description:
newVal := make(map[string]value.Value)
if err := val.ReflectAddr().Interface().(value.ConvertibleIntoCapsule).ConvertInto(&newVal); err == nil {
// Objects are indexed with a string.
if idx.Type() != value.TypeString {
return value.Null, value.TypeError{Value: idx, Expected: value.TypeString}
}

field, ok := newVal[idx.Text()]
if !ok {
// If a key doesn't exist in an object accessed with [], return null.
return value.Null, nil
}
return field, nil
}
}
return value.Null, value.Error{
Value: val,
Inner: fmt.Errorf("expected object or array or a capsule convertible to an object, got %s", val.Type()),
}

default:
return value.Null, value.Error{
Value: val,
Expand Down

0 comments on commit a623df2

Please sign in to comment.