diff --git a/.chloggen/ottl-add-parser-collection-utility.yaml b/.chloggen/ottl-add-parser-collection-utility.yaml new file mode 100644 index 000000000000..dac6947b7a10 --- /dev/null +++ b/.chloggen/ottl-add-parser-collection-utility.yaml @@ -0,0 +1,30 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: pkg/ottl + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: "Add the `ottl.ParserCollection` utility to help handling parsers for multiple OTTL contexts" + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [29017] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: | + The `ottl.ParserCollection` groups contexts' `ottl.Parser`s, choosing the suitable one + to parse a given statement. It supports context inference using the given statements, + and allows prepending the context name to the statements' paths. + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [api] diff --git a/pkg/ottl/parser_collection.go b/pkg/ottl/parser_collection.go new file mode 100644 index 000000000000..72d0d6abb3f0 --- /dev/null +++ b/pkg/ottl/parser_collection.go @@ -0,0 +1,334 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottl // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" + +import ( + "fmt" + "reflect" + + "go.opentelemetry.io/collector/component" + "go.uber.org/zap" +) + +// Safeguard to statically ensure the Parser.ParseStatements method can be reflectively +// invoked by the ottlParserWrapper.parseStatements +var _ interface { + ParseStatements(statements []string) ([]*Statement[any], error) +} = (*Parser[any])(nil) + +// Safeguard to statically ensure any ParsedStatementConverter method can be reflectively +// invoked by the statementsConverterWrapper.call +var _ ParsedStatementConverter[any, any] = func( + _ *ParserCollection[any], + _ *Parser[any], + _ string, + _ StatementsGetter, + _ []*Statement[any], +) (any, error) { + return nil, nil +} + +// StatementsGetter represents a set of statements to be parsed. +// +// Experimental: *NOTE* this API is subject to change or removal in the future. +type StatementsGetter interface { + // GetStatements retrieves the OTTL statements to be parsed + GetStatements() []string +} + +type defaultStatementsGetter []string + +func (d defaultStatementsGetter) GetStatements() []string { + return d +} + +// NewStatementsGetter creates a new StatementsGetter. +// +// Experimental: *NOTE* this API is subject to change or removal in the future. +func NewStatementsGetter(statements []string) StatementsGetter { + return defaultStatementsGetter(statements) +} + +// ottlParserWrapper wraps an ottl.Parser using reflection, so it can invoke exported +// methods without knowing its generic type (transform context). +type ottlParserWrapper struct { + parser reflect.Value + prependContextToStatementPaths func(context string, statement string) (string, error) +} + +func newParserWrapper[K any](parser *Parser[K]) *ottlParserWrapper { + return &ottlParserWrapper{ + parser: reflect.ValueOf(parser), + prependContextToStatementPaths: parser.prependContextToStatementPaths, + } +} + +func (g *ottlParserWrapper) parseStatements(statements []string) (reflect.Value, error) { + method := g.parser.MethodByName("ParseStatements") + parseStatementsRes := method.Call([]reflect.Value{reflect.ValueOf(statements)}) + err := parseStatementsRes[1] + if !err.IsNil() { + return reflect.Value{}, err.Interface().(error) + } + return parseStatementsRes[0], nil +} + +func (g *ottlParserWrapper) prependContextToStatementsPaths(context string, statements []string) ([]string, error) { + result := make([]string, 0, len(statements)) + for _, s := range statements { + prependedStatement, err := g.prependContextToStatementPaths(context, s) + if err != nil { + return nil, err + } + result = append(result, prependedStatement) + } + return result, nil +} + +// statementsConverterWrapper is a reflection-based wrapper to the ParsedStatementConverter function, +// which does not require knowing all generic parameters to be called. +type statementsConverterWrapper reflect.Value + +func newStatementsConverterWrapper[K any, R any](converter ParsedStatementConverter[K, R]) statementsConverterWrapper { + return statementsConverterWrapper(reflect.ValueOf(converter)) +} + +func (s statementsConverterWrapper) call( + parserCollection reflect.Value, + ottlParser *ottlParserWrapper, + context string, + statements StatementsGetter, + parsedStatements reflect.Value, +) (reflect.Value, error) { + result := reflect.Value(s).Call([]reflect.Value{ + parserCollection, + ottlParser.parser, + reflect.ValueOf(context), + reflect.ValueOf(statements), + parsedStatements, + }) + + resultValue := result[0] + resultError := result[1] + if !resultError.IsNil() { + return reflect.Value{}, resultError.Interface().(error) + } + + return resultValue, nil +} + +// parserCollectionParser holds an ottlParserWrapper and its respectively +// statementsConverter function. +type parserCollectionParser struct { + ottlParser *ottlParserWrapper + statementsConverter statementsConverterWrapper +} + +// ParserCollection is a configurable set of ottl.Parser that can handle multiple OTTL contexts +// parsings, inferring the context, choosing the right parser for the given statements, and +// transforming the parsed ottl.Statement[K] slice into a common result of type R. +// +// Experimental: *NOTE* this API is subject to change or removal in the future. +type ParserCollection[R any] struct { + contextParsers map[string]*parserCollectionParser + contextInferrer contextInferrer + modifiedStatementLogging bool + Settings component.TelemetrySettings + ErrorMode ErrorMode +} + +// ParserCollectionOption is a configurable ParserCollection option. +// +// Experimental: *NOTE* this API is subject to change or removal in the future. +type ParserCollectionOption[R any] func(*ParserCollection[R]) error + +// NewParserCollection creates a new ParserCollection. +// +// Experimental: *NOTE* this API is subject to change or removal in the future. +func NewParserCollection[R any]( + settings component.TelemetrySettings, + options ...ParserCollectionOption[R], +) (*ParserCollection[R], error) { + pc := &ParserCollection[R]{ + Settings: settings, + contextParsers: map[string]*parserCollectionParser{}, + contextInferrer: defaultPriorityContextInferrer(), + } + + for _, op := range options { + err := op(pc) + if err != nil { + return nil, err + } + } + + return pc, nil +} + +// ParsedStatementConverter is a function that converts the parsed ottl.Statement[K] into +// a common representation to all parser collection contexts passed through WithParserCollectionContext. +// Given each parser has its own transform context type, they must agree on a common type [R] +// so it can be returned by the ParserCollection.ParseStatements and ParserCollection.ParseStatementsWithContext +// functions. +// +// Experimental: *NOTE* this API is subject to change or removal in the future. +type ParsedStatementConverter[K any, R any] func( + collection *ParserCollection[R], + parser *Parser[K], + context string, + statements StatementsGetter, + parsedStatements []*Statement[K], +) (R, error) + +func newNopParsedStatementConverter[K any]() ParsedStatementConverter[K, any] { + return func( + _ *ParserCollection[any], + _ *Parser[K], + _ string, + _ StatementsGetter, + parsedStatements []*Statement[K], + ) (any, error) { + return parsedStatements, nil + } +} + +// WithParserCollectionContext configures an ottl.Parser for the given context. +// The provided ottl.Parser must be configured to support the provided context using +// the ottl.WithPathContextNames option. +// +// Experimental: *NOTE* this API is subject to change or removal in the future. +func WithParserCollectionContext[K any, R any]( + context string, + parser *Parser[K], + converter ParsedStatementConverter[K, R], +) ParserCollectionOption[R] { + return func(mp *ParserCollection[R]) error { + if _, ok := parser.pathContextNames[context]; !ok { + return fmt.Errorf(`context "%s" must be a valid "%T" path context name`, context, parser) + } + mp.contextParsers[context] = &parserCollectionParser{ + ottlParser: newParserWrapper[K](parser), + statementsConverter: newStatementsConverterWrapper(converter), + } + return nil + } +} + +// WithParserCollectionErrorMode has no effect on the ParserCollection, but might be used +// by the ParsedStatementConverter functions to handle/create StatementSequence. +// +// Experimental: *NOTE* this API is subject to change or removal in the future. +func WithParserCollectionErrorMode[R any](errorMode ErrorMode) ParserCollectionOption[R] { + return func(tp *ParserCollection[R]) error { + tp.ErrorMode = errorMode + return nil + } +} + +// EnableParserCollectionModifiedStatementLogging controls the statements modification logs. +// When enabled, it logs any statements modifications performed by the parsing operations, +// instructing users to rewrite the statements accordingly. +// +// Experimental: *NOTE* this API is subject to change or removal in the future. +func EnableParserCollectionModifiedStatementLogging[R any](enabled bool) ParserCollectionOption[R] { + return func(tp *ParserCollection[R]) error { + tp.modifiedStatementLogging = enabled + return nil + } +} + +// ParseStatements parses the given statements into [R] using the configured context's ottl.Parser +// and subsequently calling the ParsedStatementConverter function. +// The statement's context is automatically inferred from the [Path.Context] values, choosing the +// highest priority context found. +// If no contexts are present in the statements, or if the inferred value is not supported by +// the [ParserCollection], it returns an error. +// If parsing the statements fails, it returns the underlying [ottl.Parser.ParseStatements] error. +// +// Experimental: *NOTE* this API is subject to change or removal in the future. +func (pc *ParserCollection[R]) ParseStatements(statements StatementsGetter) (R, error) { + statementsValues := statements.GetStatements() + inferredContext, err := pc.contextInferrer.infer(statementsValues) + if err != nil { + return *new(R), err + } + + if inferredContext == "" { + return *new(R), fmt.Errorf("unable to infer context from statements [%v], path's first segment must be a valid context name", statementsValues) + } + + return pc.ParseStatementsWithContext(inferredContext, statements, false) +} + +// ParseStatementsWithContext parses the given statements into [R] using the configured +// context's ottl.Parser and subsequently calling the ParsedStatementConverter function. +// Unlike ParseStatements, it uses the provided context and does not infer it +// automatically. The context value must be supported by the [ParserCollection], +// otherwise an error is returned. +// If the statement's Path does not provide their Path.Context value, the prependPathsContext +// argument should be set to true, so it rewrites the statements prepending the missing paths +// contexts. +// If parsing the statements fails, it returns the underlying [ottl.Parser.ParseStatements] error. +// +// Experimental: *NOTE* this API is subject to change or removal in the future. +func (pc *ParserCollection[R]) ParseStatementsWithContext(context string, statements StatementsGetter, prependPathsContext bool) (R, error) { + contextParser, ok := pc.contextParsers[context] + if !ok { + return *new(R), fmt.Errorf(`unknown context "%s" for stataments: %v`, context, statements.GetStatements()) + } + + var err error + var parsingStatements []string + if prependPathsContext { + originalStatements := statements.GetStatements() + parsingStatements, err = contextParser.ottlParser.prependContextToStatementsPaths(context, originalStatements) + if err != nil { + return *new(R), err + } + if pc.modifiedStatementLogging { + pc.logModifiedStatements(originalStatements, parsingStatements) + } + } else { + parsingStatements = statements.GetStatements() + } + + parsedStatements, err := contextParser.ottlParser.parseStatements(parsingStatements) + if err != nil { + return *new(R), err + } + + convertedStatements, err := contextParser.statementsConverter.call( + reflect.ValueOf(pc), + contextParser.ottlParser, + context, + statements, + parsedStatements, + ) + if err != nil { + return *new(R), err + } + + if convertedStatements.IsNil() { + return *new(R), nil + } + + return convertedStatements.Interface().(R), nil +} + +func (pc *ParserCollection[R]) logModifiedStatements(originalStatements, modifiedStatements []string) { + var fields []zap.Field + for i, original := range originalStatements { + if modifiedStatements[i] != original { + statementKey := fmt.Sprintf("[%v]", i) + fields = append(fields, zap.Dict( + statementKey, + zap.String("original", original), + zap.String("modified", modifiedStatements[i])), + ) + } + } + if len(fields) > 0 { + pc.Settings.Logger.Info("one or more statements were modified to include their paths context, please rewrite them accordingly", zap.Dict("statements", fields...)) + } +} diff --git a/pkg/ottl/parser_collection_test.go b/pkg/ottl/parser_collection_test.go new file mode 100644 index 000000000000..841f3a5fab60 --- /dev/null +++ b/pkg/ottl/parser_collection_test.go @@ -0,0 +1,402 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottl + +import ( + "context" + "errors" + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" +) + +type mockStatementsGetter struct { + values []string +} + +func (s mockStatementsGetter) GetStatements() []string { + return s.values +} + +type mockFailingContextInferrer struct { + err error +} + +func (r *mockFailingContextInferrer) infer(_ []string) (string, error) { + return "", r.err +} + +type mockStaticContextInferrer struct { + value string +} + +func (r *mockStaticContextInferrer) infer(_ []string) (string, error) { + return r.value, nil +} + +type mockSetArguments[K any] struct { + Target Setter[K] + Value Getter[K] +} + +func Test_NewParserCollection(t *testing.T) { + settings := componenttest.NewNopTelemetrySettings() + pc, err := NewParserCollection[any](settings) + require.NoError(t, err) + + assert.NotNil(t, pc) + assert.NotNil(t, pc.contextParsers) + assert.NotNil(t, pc.contextInferrer) +} + +func Test_NewParserCollection_OptionError(t *testing.T) { + _, err := NewParserCollection[any]( + componenttest.NewNopTelemetrySettings(), + func(_ *ParserCollection[any]) error { + return errors.New("option error") + }, + ) + + require.Error(t, err, "option error") +} + +func Test_WithParserCollectionContext(t *testing.T) { + ps := mockParser(t, WithPathContextNames[any]([]string{"testContext"})) + conv := newNopParsedStatementConverter[any]() + option := WithParserCollectionContext("testContext", ps, conv) + + pc, err := NewParserCollection[any](componenttest.NewNopTelemetrySettings(), option) + require.NoError(t, err) + + pw, exists := pc.contextParsers["testContext"] + assert.True(t, exists) + assert.NotNil(t, pw) + assert.Equal(t, reflect.ValueOf(ps), pw.ottlParser.parser) + assert.Equal(t, reflect.ValueOf(conv), reflect.Value(pw.statementsConverter)) +} + +func Test_WithParserCollectionContext_UnsupportedContext(t *testing.T) { + ps := mockParser(t, WithPathContextNames[any]([]string{"foo"})) + conv := newNopParsedStatementConverter[any]() + option := WithParserCollectionContext("bar", ps, conv) + + _, err := NewParserCollection[any](componenttest.NewNopTelemetrySettings(), option) + + require.ErrorContains(t, err, `context "bar" must be a valid "*ottl.Parser[interface {}]" path context name`) +} + +func Test_WithParserCollectionErrorMode(t *testing.T) { + pc, err := NewParserCollection[any]( + componenttest.NewNopTelemetrySettings(), + WithParserCollectionErrorMode[any](PropagateError), + ) + + require.NoError(t, err) + require.NotNil(t, pc) + require.Equal(t, PropagateError, pc.ErrorMode) +} + +func Test_EnableParserCollectionModifiedStatementLogging_True(t *testing.T) { + ps := mockParser(t, WithPathContextNames[any]([]string{"dummy"})) + core, observedLogs := observer.New(zap.InfoLevel) + telemetrySettings := componenttest.NewNopTelemetrySettings() + telemetrySettings.Logger = zap.New(core) + + pc, err := NewParserCollection( + telemetrySettings, + WithParserCollectionContext("dummy", ps, newNopParsedStatementConverter[any]()), + EnableParserCollectionModifiedStatementLogging[any](true), + ) + require.NoError(t, err) + + originalStatements := []string{ + `set(attributes["foo"], "foo")`, + `set(attributes["bar"], "bar")`, + } + + _, err = pc.ParseStatementsWithContext("dummy", mockStatementsGetter{originalStatements}, true) + require.NoError(t, err) + + logEntries := observedLogs.TakeAll() + require.Len(t, logEntries, 1) + logEntry := logEntries[0] + require.Equal(t, zap.InfoLevel, logEntry.Level) + require.Contains(t, logEntry.Message, "one or more statements were modified") + logEntryStatements := logEntry.ContextMap()["statements"] + require.NotNil(t, logEntryStatements) + + for i, originalStatement := range originalStatements { + k := fmt.Sprintf("[%d]", i) + logEntryStatementContext := logEntryStatements.(map[string]any)[k] + require.Equal(t, logEntryStatementContext.(map[string]any)["original"], originalStatement) + modifiedStatement, err := ps.prependContextToStatementPaths("dummy", originalStatement) + require.NoError(t, err) + require.Equal(t, logEntryStatementContext.(map[string]any)["modified"], modifiedStatement) + } +} + +func Test_EnableParserCollectionModifiedStatementLogging_False(t *testing.T) { + ps := mockParser(t, WithPathContextNames[any]([]string{"dummy"})) + core, observedLogs := observer.New(zap.InfoLevel) + telemetrySettings := componenttest.NewNopTelemetrySettings() + telemetrySettings.Logger = zap.New(core) + + pc, err := NewParserCollection( + telemetrySettings, + WithParserCollectionContext("dummy", ps, newNopParsedStatementConverter[any]()), + EnableParserCollectionModifiedStatementLogging[any](false), + ) + require.NoError(t, err) + + _, err = pc.ParseStatementsWithContext("dummy", mockStatementsGetter{[]string{`set(attributes["foo"], "foo")`}}, true) + require.NoError(t, err) + require.Empty(t, observedLogs.TakeAll()) +} + +func Test_NopParsedStatementConverter(t *testing.T) { + type dummyContext struct{} + + noop := newNopParsedStatementConverter[dummyContext]() + parsedStatements := []*Statement[dummyContext]{{}} + convertedStatements, err := noop(nil, nil, "", mockStatementsGetter{values: []string{}}, parsedStatements) + + require.NoError(t, err) + require.NotNil(t, convertedStatements) + assert.Equal(t, parsedStatements, convertedStatements) +} + +func Test_NewParserCollection_DefaultContextInferrer(t *testing.T) { + pc, err := NewParserCollection[any](componenttest.NewNopTelemetrySettings()) + require.NoError(t, err) + require.NotNil(t, pc) + require.NotNil(t, pc.contextInferrer) +} + +func Test_ParseStatements_Success(t *testing.T) { + ps := mockParser(t, WithPathContextNames[any]([]string{"foo"})) + + pc, err := NewParserCollection( + component.TelemetrySettings{}, + WithParserCollectionContext("foo", ps, newNopParsedStatementConverter[any]()), + ) + require.NoError(t, err) + pc.contextInferrer = &mockStaticContextInferrer{"foo"} + + statements := mockStatementsGetter{values: []string{`set(foo.attributes["bar"], "foo")`, `set(foo.attributes["bar"], "bar")`}} + result, err := pc.ParseStatements(statements) + require.NoError(t, err) + + assert.IsType(t, []*Statement[any]{}, result) + assert.Len(t, result.([]*Statement[any]), 2) + assert.NotNil(t, result) +} + +func Test_ParseStatements_MultipleContexts_Success(t *testing.T) { + fooParser := mockParser(t, WithPathContextNames[any]([]string{"foo"})) + barParser := mockParser(t, WithPathContextNames[any]([]string{"bar"})) + failingConverter := func( + _ *ParserCollection[any], + _ *Parser[any], + _ string, + _ StatementsGetter, + _ []*Statement[any], + ) (any, error) { + return nil, errors.New("failing converter") + } + + pc, err := NewParserCollection( + component.TelemetrySettings{}, + WithParserCollectionContext("foo", fooParser, failingConverter), + WithParserCollectionContext("bar", barParser, newNopParsedStatementConverter[any]()), + ) + require.NoError(t, err) + pc.contextInferrer = &mockStaticContextInferrer{"bar"} + + // The `foo` context is never used, so these statements will successfully parse. + statements := mockStatementsGetter{values: []string{`set(bar.attributes["bar"], "foo")`, `set(bar.attributes["bar"], "bar")`}} + result, err := pc.ParseStatements(statements) + require.NoError(t, err) + + assert.IsType(t, []*Statement[any]{}, result) + assert.Len(t, result.([]*Statement[any]), 2) + assert.NotNil(t, result) +} + +func Test_ParseStatements_NoContextInferredError(t *testing.T) { + pc, err := NewParserCollection[any](component.TelemetrySettings{}) + require.NoError(t, err) + pc.contextInferrer = &mockStaticContextInferrer{""} + + statements := mockStatementsGetter{values: []string{`set(bar.attributes["bar"], "foo")`}} + _, err = pc.ParseStatements(statements) + + assert.ErrorContains(t, err, "unable to infer context from statements") +} + +func Test_ParseStatements_ContextInferenceError(t *testing.T) { + pc, err := NewParserCollection[any](component.TelemetrySettings{}) + require.NoError(t, err) + pc.contextInferrer = &mockFailingContextInferrer{err: errors.New("inference error")} + + statements := mockStatementsGetter{values: []string{`set(bar.attributes["bar"], "foo")`}} + _, err = pc.ParseStatements(statements) + + assert.EqualError(t, err, "inference error") +} + +func Test_ParseStatements_UnknownContextError(t *testing.T) { + pc, err := NewParserCollection[any](component.TelemetrySettings{}) + require.NoError(t, err) + pc.contextInferrer = &mockStaticContextInferrer{"foo"} + + statements := mockStatementsGetter{values: []string{`set(foo.attributes["bar"], "foo")`}} + _, err = pc.ParseStatements(statements) + + assert.ErrorContains(t, err, `unknown context "foo"`) +} + +func Test_ParseStatements_ParseStatementsError(t *testing.T) { + ps := mockParser(t, WithPathContextNames[any]([]string{"foo"})) + ps.pathParser = func(_ Path[any]) (GetSetter[any], error) { + return nil, errors.New("parse statements error") + } + + pc, err := NewParserCollection( + component.TelemetrySettings{}, + WithParserCollectionContext("foo", ps, newNopParsedStatementConverter[any]()), + ) + require.NoError(t, err) + pc.contextInferrer = &mockStaticContextInferrer{"foo"} + + statements := mockStatementsGetter{values: []string{`set(foo.attributes["bar"], "foo")`}} + _, err = pc.ParseStatements(statements) + assert.ErrorContains(t, err, "parse statements error") +} + +func Test_ParseStatements_ConverterError(t *testing.T) { + ps := mockParser(t, WithPathContextNames[any]([]string{"dummy"})) + conv := func(_ *ParserCollection[any], _ *Parser[any], _ string, _ StatementsGetter, _ []*Statement[any]) (any, error) { + return nil, errors.New("converter error") + } + + pc, err := NewParserCollection( + component.TelemetrySettings{}, + WithParserCollectionContext("dummy", ps, conv), + ) + require.NoError(t, err) + pc.contextInferrer = &mockStaticContextInferrer{"dummy"} + + statements := mockStatementsGetter{values: []string{`set(dummy.attributes["bar"], "foo")`}} + _, err = pc.ParseStatements(statements) + + assert.EqualError(t, err, "converter error") +} + +func Test_ParseStatements_ConverterNilReturn(t *testing.T) { + ps := mockParser(t, WithPathContextNames[any]([]string{"dummy"})) + conv := func(_ *ParserCollection[any], _ *Parser[any], _ string, _ StatementsGetter, _ []*Statement[any]) (any, error) { + return nil, nil + } + + pc, err := NewParserCollection( + component.TelemetrySettings{}, + WithParserCollectionContext("dummy", ps, conv), + ) + require.NoError(t, err) + pc.contextInferrer = &mockStaticContextInferrer{"dummy"} + + statements := mockStatementsGetter{values: []string{`set(dummy.attributes["bar"], "foo")`}} + result, err := pc.ParseStatements(statements) + assert.NoError(t, err) + assert.Nil(t, result) +} + +func Test_ParseStatements_StatementsConverterGetterType(t *testing.T) { + ps := mockParser(t, WithPathContextNames[any]([]string{"dummy"})) + statements := mockStatementsGetter{values: []string{`set(dummy.attributes["bar"], "foo")`}} + conv := func(_ *ParserCollection[any], _ *Parser[any], _ string, statementsGetter StatementsGetter, _ []*Statement[any]) (any, error) { + switch statementsGetter.(type) { + case mockStatementsGetter: + return statements, nil + default: + return nil, fmt.Errorf("invalid StatementsGetter type, expected: mockStatementsGetter, got: %T", statementsGetter) + } + } + + pc, err := NewParserCollection(component.TelemetrySettings{}, WithParserCollectionContext("dummy", ps, conv)) + require.NoError(t, err) + pc.contextInferrer = &mockStaticContextInferrer{"dummy"} + + _, err = pc.ParseStatements(statements) + require.NoError(t, err) +} + +func Test_ParseStatementsWithContext_UnknownContextError(t *testing.T) { + pc, err := NewParserCollection[any](component.TelemetrySettings{}) + require.NoError(t, err) + + statements := mockStatementsGetter{[]string{`set(attributes["bar"], "bar")`}} + _, err = pc.ParseStatementsWithContext("bar", statements, false) + + assert.ErrorContains(t, err, `unknown context "bar"`) +} + +func Test_ParseStatementsWithContext_PrependPathContext(t *testing.T) { + ps := mockParser(t, WithPathContextNames[any]([]string{"dummy"})) + pc, err := NewParserCollection( + component.TelemetrySettings{}, + WithParserCollectionContext("dummy", ps, newNopParsedStatementConverter[any]()), + ) + require.NoError(t, err) + + result, err := pc.ParseStatementsWithContext( + "dummy", + mockStatementsGetter{[]string{ + `set(attributes["foo"], "foo")`, + `set(attributes["bar"], "bar")`, + }}, + true, + ) + + require.NoError(t, err) + require.Len(t, result, 2) + parsedStatements := result.([]*Statement[any]) + assert.Equal(t, `set(dummy.attributes["foo"], "foo")`, parsedStatements[0].origText) + assert.Equal(t, `set(dummy.attributes["bar"], "bar")`, parsedStatements[1].origText) +} + +func Test_NewStatementsGetter(t *testing.T) { + statements := []string{`set(foo, "bar")`, `set(bar, "foo")`} + statementsGetter := NewStatementsGetter(statements) + assert.Implements(t, (*StatementsGetter)(nil), statementsGetter) + assert.Equal(t, statements, statementsGetter.GetStatements()) +} + +func mockParser(t *testing.T, options ...Option[any]) *Parser[any] { + mockSetFactory := NewFactory("set", &mockSetArguments[any]{}, + func(_ FunctionContext, _ Arguments) (ExprFunc[any], error) { + return func(_ context.Context, _ any) (any, error) { + return nil, nil + }, nil + }) + + ps, err := NewParser( + CreateFactoryMap[any](mockSetFactory), + testParsePath[any], + componenttest.NewNopTelemetrySettings(), + append([]Option[any]{ + WithEnumParser[any](testParseEnum), + }, options...)..., + ) + + require.NoError(t, err) + return &ps +}