Skip to content

Commit bfe6d8f

Browse files
fix: Enable interface field access in rule expressions (#484)
- Enhanced IsObject() method to recognize interfaces containing structs - Updated GetObjectValueByField() to handle interface types - Updated SetObjectValueByField() to support interface field modification - Added comprehensive tests for interface field access - Enables direct field access like Data.Payload.Status in rules - Maintains backward compatibility with existing functionality Fixes issue where interface{} fields could not be accessed directly in rule expressions, requiring workaround accessor methods.
1 parent 8747b04 commit bfe6d8f

File tree

3 files changed

+255
-10
lines changed

3 files changed

+255
-10
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package examples
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hyperjumptech/grule-rule-engine/ast"
7+
"github.com/hyperjumptech/grule-rule-engine/builder"
8+
"github.com/hyperjumptech/grule-rule-engine/engine"
9+
"github.com/hyperjumptech/grule-rule-engine/pkg"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// PayloadContainer represents a struct with an interface{} field
15+
type PayloadContainer struct {
16+
Type string
17+
Payload interface{}
18+
}
19+
20+
// NestedData represents the concrete type stored in the interface
21+
type NestedData struct {
22+
Status string
23+
Value int
24+
}
25+
26+
func TestInterfaceFieldAccess(t *testing.T) {
27+
t.Parallel()
28+
t.Run("should not fail when accessing interface fields directly", func(t *testing.T) {
29+
// Rule that tries to access interface field directly
30+
ruleText := `
31+
rule InterfaceFieldRule "test interface field access" {
32+
when
33+
Data.Type == "test" &&
34+
Data.Payload.Status == "active"
35+
then
36+
Data.Type = "processed";
37+
Log("Success");
38+
}`
39+
40+
// Test data with interface{} field
41+
testData := &PayloadContainer{
42+
Type: "test",
43+
Payload: NestedData{
44+
Status: "active",
45+
Value: 42,
46+
},
47+
}
48+
49+
dataCtx := ast.NewDataContext()
50+
require.NoError(t, dataCtx.Add("Data", testData))
51+
52+
knowledgeLibrary := ast.NewKnowledgeLibrary()
53+
ruleBuilder := builder.NewRuleBuilder(knowledgeLibrary)
54+
55+
byteArr := pkg.NewBytesResource([]byte(ruleText))
56+
err := ruleBuilder.BuildRuleFromResource("Test", "1.0.0", byteArr)
57+
require.NoError(t, err)
58+
59+
knowledgeBase, err := knowledgeLibrary.NewKnowledgeBaseInstance("Test", "1.0.0")
60+
require.NoError(t, err)
61+
62+
gruleEngine := engine.NewGruleEngine()
63+
gruleEngine.ReturnErrOnFailedRuleEvaluation = true
64+
err = gruleEngine.Execute(dataCtx, knowledgeBase)
65+
66+
assert.NoError(t, err, "should not fail to access interface field")
67+
assert.Equal(t, "processed", testData.Type, "rule should have executed and modified the data")
68+
69+
// Verify the interface field value remains unchanged since we're only reading it
70+
payload, ok := testData.Payload.(NestedData)
71+
require.True(t, ok, "type assertion should succeed")
72+
assert.Equal(t, "active", payload.Status, "interface field should remain unchanged")
73+
})
74+
75+
t.Run("should allow modifying interface fields when using pointer to struct", func(t *testing.T) {
76+
// Rule that modifies interface field
77+
ruleText := `
78+
rule InterfaceFieldModifyRule "test interface field modification" {
79+
when
80+
Data.Type == "test" &&
81+
Data.Payload.Status == "active"
82+
then
83+
Data.Type = "processed";
84+
Data.Payload.Status = "handled";
85+
Log("Modified interface field");
86+
}`
87+
88+
// Test data with interface{} field containing a pointer to struct (addressable)
89+
testData := &PayloadContainer{
90+
Type: "test",
91+
Payload: &NestedData{
92+
Status: "active",
93+
Value: 42,
94+
},
95+
}
96+
97+
dataCtx := ast.NewDataContext()
98+
require.NoError(t, dataCtx.Add("Data", testData))
99+
100+
knowledgeLibrary := ast.NewKnowledgeLibrary()
101+
ruleBuilder := builder.NewRuleBuilder(knowledgeLibrary)
102+
103+
byteArr := pkg.NewBytesResource([]byte(ruleText))
104+
err := ruleBuilder.BuildRuleFromResource("Test", "1.0.0", byteArr)
105+
require.NoError(t, err)
106+
107+
knowledgeBase, err := knowledgeLibrary.NewKnowledgeBaseInstance("Test", "1.0.0")
108+
require.NoError(t, err)
109+
110+
gruleEngine := engine.NewGruleEngine()
111+
gruleEngine.ReturnErrOnFailedRuleEvaluation = true
112+
err = gruleEngine.Execute(dataCtx, knowledgeBase)
113+
114+
assert.NoError(t, err, "should not fail to modify interface field when using pointer")
115+
assert.Equal(t, "processed", testData.Type, "rule should have executed and modified the data")
116+
117+
// Verify the interface field was modified
118+
payload, ok := testData.Payload.(*NestedData)
119+
require.True(t, ok, "type assertion should succeed")
120+
assert.Equal(t, "handled", payload.Status, "interface field should have been modified")
121+
})
122+
}

model/GoDataAccessLayer.go

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,19 @@ func (node *GoValueNode) IsObject() bool {
256256

257257
return typ.Elem().Kind() == reflect.Struct
258258
}
259+
if typ.Kind() == reflect.Interface {
260+
// Check if the interface contains a struct
261+
if !node.thisValue.IsNil() {
262+
elem := node.thisValue.Elem()
263+
if elem.IsValid() {
264+
elemType := elem.Type()
265+
if elemType.Kind() == reflect.Ptr {
266+
return elemType.Elem().Kind() == reflect.Struct
267+
}
268+
return elemType.Kind() == reflect.Struct
269+
}
270+
}
271+
}
259272

260273
return typ.Kind() == reflect.Struct
261274
}
@@ -267,14 +280,24 @@ func (node *GoValueNode) IsObject() bool {
267280
func (node *GoValueNode) GetObjectValueByField(field string) (reflect.Value, error) {
268281
if node.IsObject() {
269282
var val reflect.Value
270-
if node.thisValue.Kind() == reflect.Ptr {
283+
284+
// Handle interface types by extracting the concrete value
285+
if node.thisValue.Kind() == reflect.Interface {
286+
if !node.thisValue.IsNil() {
287+
elem := node.thisValue.Elem()
288+
if elem.Kind() == reflect.Ptr {
289+
val = elem.Elem().FieldByName(field)
290+
} else if elem.Kind() == reflect.Struct {
291+
val = elem.FieldByName(field)
292+
}
293+
}
294+
} else if node.thisValue.Kind() == reflect.Ptr {
271295
val = node.thisValue.Elem().FieldByName(field)
272-
}
273-
if node.thisValue.Kind() == reflect.Struct {
296+
} else if node.thisValue.Kind() == reflect.Struct {
274297
val = node.thisValue.FieldByName(field)
275298
}
276-
if val.IsValid() {
277299

300+
if val.IsValid() {
278301
return val, nil
279302
}
280303

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

320+
// Handle interface types by extracting the concrete type
321+
if node.thisValue.Kind() == reflect.Interface {
322+
if !node.thisValue.IsNil() {
323+
elem := node.thisValue.Elem()
324+
if elem.Kind() == reflect.Ptr {
325+
return elem.Elem().FieldByName(field).Type(), nil
326+
} else if elem.Kind() == reflect.Struct {
327+
return elem.FieldByName(field).Type(), nil
328+
}
329+
}
330+
} else if node.thisValue.Kind() == reflect.Ptr {
298331
return node.thisValue.Elem().FieldByName(field).Type(), nil
299-
}
300-
if node.thisValue.Kind() == reflect.Struct {
301-
332+
} else if node.thisValue.Kind() == reflect.Struct {
302333
return node.thisValue.FieldByName(field).Type(), nil
303334
}
304335
}
@@ -353,7 +384,19 @@ func SetNumberValue(target, newvalue reflect.Value) error {
353384

354385
// SetObjectValueByField will set the underlying value's field with new value.
355386
func (node *GoValueNode) SetObjectValueByField(field string, newValue reflect.Value) (err error) {
356-
fieldVal := node.thisValue.Elem().FieldByName(field)
387+
var objValue reflect.Value = node.thisValue
388+
389+
// If it's an interface, extract the concrete value
390+
if node.thisValue.Kind() == reflect.Interface && !node.thisValue.IsNil() {
391+
objValue = node.thisValue.Elem()
392+
}
393+
394+
// Handle pointer to struct
395+
if objValue.Kind() == reflect.Ptr {
396+
objValue = objValue.Elem()
397+
}
398+
399+
fieldVal := objValue.FieldByName(field)
357400
if fieldVal.IsValid() && fieldVal.CanAddr() && fieldVal.CanSet() {
358401
defer func() {
359402
if r := recover(); r != nil {

model/GoDataAccessLayer_test.go

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
package model
1616

1717
import (
18-
"github.com/stretchr/testify/assert"
1918
"reflect"
2019
"testing"
2120
"time"
21+
22+
"github.com/stretchr/testify/assert"
2223
)
2324

2425
type Person struct {
@@ -358,3 +359,82 @@ func TestConstantFunctionCalls(t *testing.T) {
358359
assert.Equal(t, "string", retVal.Type().String())
359360
assert.Equal(t, "SomeWithSpace", retVal.String())
360361
}
362+
363+
// TestStructWithInterface represents a struct with an interface{} field for testing
364+
type TestStructWithInterface struct {
365+
Name string
366+
Payload interface{}
367+
}
368+
369+
// TestPayload represents the concrete type stored in the interface
370+
type TestPayload struct {
371+
Status string
372+
Count int
373+
}
374+
375+
func TestGoValueNode_Interface(t *testing.T) {
376+
// Test with interface containing struct value
377+
testData := &TestStructWithInterface{
378+
Name: "test",
379+
Payload: TestPayload{
380+
Status: "active",
381+
Count: 42,
382+
},
383+
}
384+
385+
rootNode := NewGoValueNode(reflect.ValueOf(testData), "testData")
386+
payloadNode, err := rootNode.GetChildNodeByField("Payload")
387+
assert.NoError(t, err)
388+
assert.True(t, payloadNode.IsInterface())
389+
assert.True(t, payloadNode.IsObject()) // Should return true for interface containing struct
390+
391+
// Test accessing fields within interface
392+
statusNode, err := payloadNode.GetChildNodeByField("Status")
393+
assert.NoError(t, err)
394+
assert.True(t, statusNode.IsString())
395+
assert.Equal(t, "testData.Payload.Status", statusNode.IdentifiedAs())
396+
397+
statusValue, err := statusNode.GetValue()
398+
assert.NoError(t, err)
399+
assert.Equal(t, "active", statusValue.String())
400+
401+
countNode, err := payloadNode.GetChildNodeByField("Count")
402+
assert.NoError(t, err)
403+
assert.True(t, countNode.IsInteger())
404+
405+
countValue, err := countNode.GetValue()
406+
assert.NoError(t, err)
407+
assert.Equal(t, 42, int(countValue.Int()))
408+
}
409+
410+
func TestGoValueNode_InterfaceWithPointer(t *testing.T) {
411+
// Test with interface containing pointer to struct (addressable)
412+
testData := &TestStructWithInterface{
413+
Name: "test",
414+
Payload: &TestPayload{
415+
Status: "active",
416+
Count: 42,
417+
},
418+
}
419+
420+
rootNode := NewGoValueNode(reflect.ValueOf(testData), "testData")
421+
payloadNode, err := rootNode.GetChildNodeByField("Payload")
422+
assert.NoError(t, err)
423+
assert.True(t, payloadNode.IsInterface())
424+
assert.True(t, payloadNode.IsObject()) // Should return true for interface containing pointer to struct
425+
426+
// Test setting fields within interface (should work with pointer)
427+
err = payloadNode.SetObjectValueByField("Status", reflect.ValueOf("modified"))
428+
assert.NoError(t, err)
429+
430+
// Verify the change
431+
statusNode, err := payloadNode.GetChildNodeByField("Status")
432+
assert.NoError(t, err)
433+
statusValue, err := statusNode.GetValue()
434+
assert.NoError(t, err)
435+
assert.Equal(t, "modified", statusValue.String())
436+
437+
// Also verify through direct access to the struct
438+
payload := testData.Payload.(*TestPayload)
439+
assert.Equal(t, "modified", payload.Status)
440+
}

0 commit comments

Comments
 (0)