Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions examples/interface_field_access_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package examples

import (
"testing"

"github.com/hyperjumptech/grule-rule-engine/ast"
"github.com/hyperjumptech/grule-rule-engine/builder"
"github.com/hyperjumptech/grule-rule-engine/engine"
"github.com/hyperjumptech/grule-rule-engine/pkg"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// PayloadContainer represents a struct with an interface{} field
type PayloadContainer struct {
Type string
Payload interface{}
}

// NestedData represents the concrete type stored in the interface
type NestedData struct {
Status string
Value int
}

func TestInterfaceFieldAccess(t *testing.T) {
t.Parallel()
t.Run("should not fail when accessing interface fields directly", func(t *testing.T) {
// Rule that tries to access interface field directly
ruleText := `
rule InterfaceFieldRule "test interface field access" {
when
Data.Type == "test" &&
Data.Payload.Status == "active"
then
Data.Type = "processed";
Log("Success");
}`

// Test data with interface{} field
testData := &PayloadContainer{
Type: "test",
Payload: NestedData{
Status: "active",
Value: 42,
},
}

dataCtx := ast.NewDataContext()
require.NoError(t, dataCtx.Add("Data", testData))

knowledgeLibrary := ast.NewKnowledgeLibrary()
ruleBuilder := builder.NewRuleBuilder(knowledgeLibrary)

byteArr := pkg.NewBytesResource([]byte(ruleText))
err := ruleBuilder.BuildRuleFromResource("Test", "1.0.0", byteArr)
require.NoError(t, err)

knowledgeBase, err := knowledgeLibrary.NewKnowledgeBaseInstance("Test", "1.0.0")
require.NoError(t, err)

gruleEngine := engine.NewGruleEngine()
gruleEngine.ReturnErrOnFailedRuleEvaluation = true
err = gruleEngine.Execute(dataCtx, knowledgeBase)

assert.NoError(t, err, "should not fail to access interface field")
assert.Equal(t, "processed", testData.Type, "rule should have executed and modified the data")

// Verify the interface field value remains unchanged since we're only reading it
payload, ok := testData.Payload.(NestedData)
require.True(t, ok, "type assertion should succeed")
assert.Equal(t, "active", payload.Status, "interface field should remain unchanged")
})

t.Run("should allow modifying interface fields when using pointer to struct", func(t *testing.T) {
// Rule that modifies interface field
ruleText := `
rule InterfaceFieldModifyRule "test interface field modification" {
when
Data.Type == "test" &&
Data.Payload.Status == "active"
then
Data.Type = "processed";
Data.Payload.Status = "handled";
Log("Modified interface field");
}`

// Test data with interface{} field containing a pointer to struct (addressable)
testData := &PayloadContainer{
Type: "test",
Payload: &NestedData{
Status: "active",
Value: 42,
},
}

dataCtx := ast.NewDataContext()
require.NoError(t, dataCtx.Add("Data", testData))

knowledgeLibrary := ast.NewKnowledgeLibrary()
ruleBuilder := builder.NewRuleBuilder(knowledgeLibrary)

byteArr := pkg.NewBytesResource([]byte(ruleText))
err := ruleBuilder.BuildRuleFromResource("Test", "1.0.0", byteArr)
require.NoError(t, err)

knowledgeBase, err := knowledgeLibrary.NewKnowledgeBaseInstance("Test", "1.0.0")
require.NoError(t, err)

gruleEngine := engine.NewGruleEngine()
gruleEngine.ReturnErrOnFailedRuleEvaluation = true
err = gruleEngine.Execute(dataCtx, knowledgeBase)

assert.NoError(t, err, "should not fail to modify interface field when using pointer")
assert.Equal(t, "processed", testData.Type, "rule should have executed and modified the data")

// Verify the interface field was modified
payload, ok := testData.Payload.(*NestedData)
require.True(t, ok, "type assertion should succeed")
assert.Equal(t, "handled", payload.Status, "interface field should have been modified")
})
}
61 changes: 52 additions & 9 deletions model/GoDataAccessLayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,19 @@ func (node *GoValueNode) IsObject() bool {

return typ.Elem().Kind() == reflect.Struct
}
if typ.Kind() == reflect.Interface {
// Check if the interface contains a struct
if !node.thisValue.IsNil() {
elem := node.thisValue.Elem()
if elem.IsValid() {
elemType := elem.Type()
if elemType.Kind() == reflect.Ptr {
return elemType.Elem().Kind() == reflect.Struct
}
return elemType.Kind() == reflect.Struct
}
}
}

return typ.Kind() == reflect.Struct
}
Expand All @@ -267,14 +280,24 @@ func (node *GoValueNode) IsObject() bool {
func (node *GoValueNode) GetObjectValueByField(field string) (reflect.Value, error) {
if node.IsObject() {
var val reflect.Value
if node.thisValue.Kind() == reflect.Ptr {

// Handle interface types by extracting the concrete value
if node.thisValue.Kind() == reflect.Interface {
if !node.thisValue.IsNil() {
elem := node.thisValue.Elem()
if elem.Kind() == reflect.Ptr {
val = elem.Elem().FieldByName(field)
} else if elem.Kind() == reflect.Struct {
val = elem.FieldByName(field)
}
}
} else if node.thisValue.Kind() == reflect.Ptr {
val = node.thisValue.Elem().FieldByName(field)
}
if node.thisValue.Kind() == reflect.Struct {
} else if node.thisValue.Kind() == reflect.Struct {
val = node.thisValue.FieldByName(field)
}
if val.IsValid() {

if val.IsValid() {
return val, nil
}

Expand All @@ -293,12 +316,20 @@ func (node *GoValueNode) GetObjectTypeByField(field string) (typ reflect.Type, e
typ = nil
}
}()
if node.thisValue.Kind() == reflect.Ptr {

// Handle interface types by extracting the concrete type
if node.thisValue.Kind() == reflect.Interface {
if !node.thisValue.IsNil() {
elem := node.thisValue.Elem()
if elem.Kind() == reflect.Ptr {
return elem.Elem().FieldByName(field).Type(), nil
} else if elem.Kind() == reflect.Struct {
return elem.FieldByName(field).Type(), nil
}
}
} else if node.thisValue.Kind() == reflect.Ptr {
return node.thisValue.Elem().FieldByName(field).Type(), nil
}
if node.thisValue.Kind() == reflect.Struct {

} else if node.thisValue.Kind() == reflect.Struct {
return node.thisValue.FieldByName(field).Type(), nil
}
}
Expand Down Expand Up @@ -353,7 +384,19 @@ func SetNumberValue(target, newvalue reflect.Value) error {

// SetObjectValueByField will set the underlying value's field with new value.
func (node *GoValueNode) SetObjectValueByField(field string, newValue reflect.Value) (err error) {
fieldVal := node.thisValue.Elem().FieldByName(field)
var objValue reflect.Value = node.thisValue

// If it's an interface, extract the concrete value
if node.thisValue.Kind() == reflect.Interface && !node.thisValue.IsNil() {
objValue = node.thisValue.Elem()
}

// Handle pointer to struct
if objValue.Kind() == reflect.Ptr {
objValue = objValue.Elem()
}

fieldVal := objValue.FieldByName(field)
if fieldVal.IsValid() && fieldVal.CanAddr() && fieldVal.CanSet() {
defer func() {
if r := recover(); r != nil {
Expand Down
82 changes: 81 additions & 1 deletion model/GoDataAccessLayer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
package model

import (
"github.com/stretchr/testify/assert"
"reflect"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

type Person struct {
Expand Down Expand Up @@ -358,3 +359,82 @@ func TestConstantFunctionCalls(t *testing.T) {
assert.Equal(t, "string", retVal.Type().String())
assert.Equal(t, "SomeWithSpace", retVal.String())
}

// TestStructWithInterface represents a struct with an interface{} field for testing
type TestStructWithInterface struct {
Name string
Payload interface{}
}

// TestPayload represents the concrete type stored in the interface
type TestPayload struct {
Status string
Count int
}

func TestGoValueNode_Interface(t *testing.T) {
// Test with interface containing struct value
testData := &TestStructWithInterface{
Name: "test",
Payload: TestPayload{
Status: "active",
Count: 42,
},
}

rootNode := NewGoValueNode(reflect.ValueOf(testData), "testData")
payloadNode, err := rootNode.GetChildNodeByField("Payload")
assert.NoError(t, err)
assert.True(t, payloadNode.IsInterface())
assert.True(t, payloadNode.IsObject()) // Should return true for interface containing struct

// Test accessing fields within interface
statusNode, err := payloadNode.GetChildNodeByField("Status")
assert.NoError(t, err)
assert.True(t, statusNode.IsString())
assert.Equal(t, "testData.Payload.Status", statusNode.IdentifiedAs())

statusValue, err := statusNode.GetValue()
assert.NoError(t, err)
assert.Equal(t, "active", statusValue.String())

countNode, err := payloadNode.GetChildNodeByField("Count")
assert.NoError(t, err)
assert.True(t, countNode.IsInteger())

countValue, err := countNode.GetValue()
assert.NoError(t, err)
assert.Equal(t, 42, int(countValue.Int()))
}

func TestGoValueNode_InterfaceWithPointer(t *testing.T) {
// Test with interface containing pointer to struct (addressable)
testData := &TestStructWithInterface{
Name: "test",
Payload: &TestPayload{
Status: "active",
Count: 42,
},
}

rootNode := NewGoValueNode(reflect.ValueOf(testData), "testData")
payloadNode, err := rootNode.GetChildNodeByField("Payload")
assert.NoError(t, err)
assert.True(t, payloadNode.IsInterface())
assert.True(t, payloadNode.IsObject()) // Should return true for interface containing pointer to struct

// Test setting fields within interface (should work with pointer)
err = payloadNode.SetObjectValueByField("Status", reflect.ValueOf("modified"))
assert.NoError(t, err)

// Verify the change
statusNode, err := payloadNode.GetChildNodeByField("Status")
assert.NoError(t, err)
statusValue, err := statusNode.GetValue()
assert.NoError(t, err)
assert.Equal(t, "modified", statusValue.String())

// Also verify through direct access to the struct
payload := testData.Payload.(*TestPayload)
assert.Equal(t, "modified", payload.Status)
}
Loading