Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pkg/ottl] Add BoolGetter and IsBool converter #27900

Merged
27 changes: 27 additions & 0 deletions .chloggen/issue-27897-IsBool-Converter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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 IsBool function into OTTL

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [27897]

# (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:

# 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: [user]
2 changes: 2 additions & 0 deletions pkg/ottl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ The following types are supported for single-value parameters in OTTL functions:
- `StringLikeGetter`
- `IntGetter`
- `IntLikeGetter`
- `BoolGetter`
- `BoolLikeGetter`
- `Enum`
- `string`
- `float64`
Expand Down
93 changes: 93 additions & 0 deletions pkg/ottl/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,41 @@ func (g StandardFloatGetter[K]) Get(ctx context.Context, tCtx K) (float64, error
}
}

// BoolGetter is a Getter that must return a bool.
type BoolGetter[K any] interface {
// Get retrieves a bool value.
Get(ctx context.Context, tCtx K) (bool, error)
}

// StandardBoolGetter is a basic implementation of BoolGetter
type StandardBoolGetter[K any] struct {
Getter func(ctx context.Context, tCtx K) (any, error)
}

// Get retrieves a bool value.
// If the value is not a bool a new TypeError is returned.
// If there is an error getting the value it will be returned.
func (g StandardBoolGetter[K]) Get(ctx context.Context, tCtx K) (bool, error) {
val, err := g.Getter(ctx, tCtx)
if err != nil {
return false, fmt.Errorf("error getting value in %T: %w", g, err)
}
if val == nil {
return false, TypeError("expected bool but got nil")
}
switch v := val.(type) {
case bool:
return v, nil
case pcommon.Value:
if v.Type() == pcommon.ValueTypeBool {
return v.Bool(), nil
}
return false, TypeError(fmt.Sprintf("expected bool but got %v", v.Type()))
default:
return false, TypeError(fmt.Sprintf("expected bool but got %T", val))
}
}

// FunctionGetter uses a function factory to return an instantiated function as an Expr.
type FunctionGetter[K any] interface {
Get(args Arguments) (Expr[K], error)
Expand Down Expand Up @@ -507,6 +542,64 @@ func (g StandardIntLikeGetter[K]) Get(ctx context.Context, tCtx K) (*int64, erro
return &result, nil
}

// BoolLikeGetter is a Getter that returns a bool by converting the underlying value to a bool if necessary.
type BoolLikeGetter[K any] interface {
// Get retrieves a bool value.
// Unlike `BoolGetter`, the expectation is that the underlying value is converted to a bool if possible.
// If the value cannot be converted to a bool, nil and an error are returned.
// If the value is nil, nil is returned without an error.
Get(ctx context.Context, tCtx K) (*bool, error)
}

type StandardBoolLikeGetter[K any] struct {
Getter func(ctx context.Context, tCtx K) (any, error)
}

func (g StandardBoolLikeGetter[K]) Get(ctx context.Context, tCtx K) (*bool, error) {
val, err := g.Getter(ctx, tCtx)
if err != nil {
return nil, fmt.Errorf("error getting value in %T: %w", g, err)
}
if val == nil {
return nil, nil
}
var result bool
switch v := val.(type) {
case bool:
result = v
case int:
result = v != 0
case int64:
result = v != 0
case string:
result, err = strconv.ParseBool(v)
if err != nil {
return nil, err
}
case float64:
result = v != 0.0
case pcommon.Value:
switch v.Type() {
case pcommon.ValueTypeBool:
result = v.Bool()
case pcommon.ValueTypeInt:
result = v.Int() != 0
case pcommon.ValueTypeStr:
result, err = strconv.ParseBool(v.Str())
if err != nil {
return nil, err
}
case pcommon.ValueTypeDouble:
result = v.Double() != 0.0
default:
return nil, TypeError(fmt.Sprintf("unsupported value type: %v", v.Type()))
}
default:
return nil, TypeError(fmt.Sprintf("unsupported type: %T", val))
}
return &result, nil
}

func (p *Parser[K]) newGetter(val value) (Getter[K], error) {
if val.IsNil != nil && *val.IsNil {
return &literal[K]{value: nil}, nil
Expand Down
233 changes: 233 additions & 0 deletions pkg/ottl/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1449,6 +1449,239 @@ func Test_StandardIntLikeGetter_WrappedError(t *testing.T) {
assert.False(t, ok)
}

func Test_StandardBoolGetter(t *testing.T) {
tests := []struct {
name string
getter StandardBoolGetter[any]
want bool
valid bool
expectedErrorMsg string
}{
{
name: "primitive bool type",
getter: StandardBoolGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
return true, nil
},
},
want: true,
valid: true,
},
{
name: "ValueTypeBool type",
getter: StandardBoolGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
return pcommon.NewValueBool(true), nil
},
},
want: true,
valid: true,
},
{
name: "Incorrect type",
getter: StandardBoolGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
return 1, nil
},
},
valid: false,
expectedErrorMsg: "expected bool but got int",
},
{
name: "nil",
getter: StandardBoolGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
return nil, nil
},
},
valid: false,
expectedErrorMsg: "expected bool but got nil",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
val, err := tt.getter.Get(context.Background(), nil)
if tt.valid {
assert.NoError(t, err)
assert.Equal(t, tt.want, val)
} else {
assert.IsType(t, TypeError(""), err)
assert.EqualError(t, err, tt.expectedErrorMsg)
}
})
}
}

// nolint:errorlint
func Test_StandardBoolGetter_WrappedError(t *testing.T) {
getter := StandardBoolGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
return nil, TypeError("")
},
}
_, err := getter.Get(context.Background(), nil)
assert.Error(t, err)
_, ok := err.(TypeError)
assert.False(t, ok)
}

func Test_StandardBoolLikeGetter(t *testing.T) {
tests := []struct {
name string
getter BoolLikeGetter[any]
want any
valid bool
expectedErrorMsg string
}{
{
name: "string type true",
getter: StandardBoolLikeGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
return "true", nil
},
},
want: true,
valid: true,
},
{
name: "string type false",
getter: StandardBoolLikeGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
return "false", nil
},
},
want: false,
valid: true,
},
{
name: "int type",
getter: StandardBoolLikeGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
return 0, nil
},
},
want: false,
valid: true,
},
{
name: "float64 type",
getter: StandardBoolLikeGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
return float64(0.0), nil
},
},
want: false,
valid: true,
},
{
name: "pcommon.value type int",
getter: StandardBoolLikeGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
v := pcommon.NewValueInt(int64(0))
return v, nil
},
},
want: false,
valid: true,
},
{
name: "pcommon.value type string",
getter: StandardBoolLikeGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
v := pcommon.NewValueStr("false")
return v, nil
},
},
want: false,
valid: true,
},
{
name: "pcommon.value type bool",
getter: StandardBoolLikeGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
v := pcommon.NewValueBool(true)
return v, nil
},
},
want: true,
valid: true,
},
{
name: "pcommon.value type double",
getter: StandardBoolLikeGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
v := pcommon.NewValueDouble(float64(0.0))
return v, nil
},
},
want: false,
valid: true,
},
{
name: "nil",
getter: StandardBoolLikeGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
return nil, nil
},
},
want: nil,
valid: true,
},
{
name: "invalid type",
getter: StandardBoolLikeGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
return []byte{}, nil
},
},
valid: false,
expectedErrorMsg: "unsupported type: []uint8",
},
{
name: "invalid pcommon.value type",
getter: StandardBoolLikeGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
v := pcommon.NewValueMap()
return v, nil
},
},
valid: false,
expectedErrorMsg: "unsupported value type: Map",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
val, err := tt.getter.Get(context.Background(), nil)
if tt.valid {
assert.NoError(t, err)
if tt.want == nil {
assert.Nil(t, val)
} else {
assert.Equal(t, tt.want, *val)
}
} else {
assert.IsType(t, TypeError(""), err)
assert.EqualError(t, err, tt.expectedErrorMsg)
}
})
}
}

// nolint:errorlint
func Test_StandardBoolLikeGetter_WrappedError(t *testing.T) {
getter := StandardBoolLikeGetter[any]{
Getter: func(ctx context.Context, tCtx any) (any, error) {
return nil, TypeError("")
},
}
_, err := getter.Get(context.Background(), nil)
assert.Error(t, err)
_, ok := err.(TypeError)
assert.False(t, ok)
}

func Test_StandardPMapGetter(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading