expr-lang / expr

Expression language and expression evaluation for Go
https://expr-lang.org
MIT License
5.85k stars 378 forks source link

interface conversion: interface {} is nil, not string (1:9) #652

Closed mbardelmeijer closed 1 month ago

mbardelmeijer commented 1 month ago

When running the following expression, it returns in the error interface conversion: interface {} is nil, not string (1:9)

We would like to handle this gracefully, as the query parameter are optional that we're matching on. We prefer not to wrap it in string(), as our customer may create their own expression, and prefer to have this expression as clean as possible.

Is there any way to combat this, with for instance AllowUndefinedVariables? Or can expr-lang be modified to allow contains queries etc. on possible nil types?

env := map[string]interface{}{
    "query": map[string]interface{}{
        "another_query": "test",
    },
}

expression := "query.b contains 'test'"
prg, _ := expr.Compile(expression, expr.Env(env))
result, err := expr.Run(prg, env)
fmt.Println(result)
fmt.Println(err)

Output:

interface conversion: interface {} is nil, not string (1:9)
 | query.b contains 'test'
antonmedv commented 1 month ago

I think we should make it possible to compare to nil anything.

I will update == operator to support this.

antonmedv commented 1 month ago

Actually, the problem is not with ==, but with contains.

The == already supports this:

func TestExpr_string_nil_equals_string(t *testing.T) {
    var str *string = nil

    env := map[string]any{
        "nilString": str,
    }

    program, err := expr.Compile(`nilString == "hello, world"`, expr.Env(env))
    require.NoError(t, err) // Pass

    output, err := expr.Run(program, env)
    require.NoError(t, err)
    require.Equal(t, false, output) // Pass
}
antonmedv commented 1 month ago

Here is the test, only first is pasing:


func TestExpr_nil_op_str(t *testing.T) {
    // Let's test operators, which do `.(string)` in VM, also check for nil.

    var str *string = nil
    env := map[string]any{
        "nilString": str,
    }

    tests := []struct{ code string }{
        {`nilString == "str"`},             // ok
        {`nilString contains "str"`},
        {`nilString matches "str"`},
        {`nilString startsWith "str"`},
        {`nilString endsWith "str"`},
    }

    for _, tt := range tests {
        t.Run(tt.code, func(t *testing.T) {
            program, err := expr.Compile(tt.code)
            require.NoError(t, err)

            output, err := expr.Run(program, env)
            require.NoError(t, err)
            require.Equal(t, false, output)
        })
    }
}
antonmedv commented 1 month ago

Fixed.

mbardelmeijer commented 1 month ago

Awesome, thanks a lot! 🙏 -- Can confirm the fix works.