Disallow non-supported nodes in pattern and replacement
diff --git a/pkg/mapper/mapper.go b/pkg/mapper/mapper.go
index f540525..8350823 100644
--- a/pkg/mapper/mapper.go
+++ b/pkg/mapper/mapper.go
@@ -70,7 +70,7 @@
}
// ApplyMappings applies the specified mapping rules to a JSON object
-func (m *Mapper) ApplyMappings(mappingID string, opts MappingOptions, jsonData interface{}) (interface{}, error) {
+func (m *Mapper) ApplyMappings(mappingID string, opts MappingOptions, jsonData any) (any, error) {
// Validate mapping ID
if _, exists := m.mappingLists[mappingID]; !exists {
return nil, fmt.Errorf("mapping list with ID %s not found", mappingID)
@@ -95,9 +95,13 @@
return nil, fmt.Errorf("failed to parse JSON into AST: %w", err)
}
- // Extract the inner node if it's a token
+ // Store whether the input was a Token
+ isToken := false
+ var tokenWrap ast.Node
if token, ok := node.(*ast.Token); ok {
- node = token.Wrap
+ isToken = true
+ tokenWrap = token.Wrap
+ node = tokenWrap
}
// Apply each rule to the AST
@@ -130,12 +134,20 @@
}
// Create matcher and apply replacement
- m := matcher.NewMatcher(ast.Pattern{Root: pattern}, ast.Replacement{Root: replacement})
+ m, err := matcher.NewMatcher(ast.Pattern{Root: pattern}, ast.Replacement{Root: replacement})
+ if err != nil {
+ return nil, fmt.Errorf("failed to create matcher: %w", err)
+ }
node = m.Replace(node)
}
- // Wrap the result in a token
- result := &ast.Token{Wrap: node}
+ // Wrap the result in a token if the input was a token
+ var result ast.Node
+ if isToken {
+ result = &ast.Token{Wrap: node}
+ } else {
+ result = node
+ }
// Convert AST back to JSON
resultBytes, err := parser.SerializeToJSON(result)
diff --git a/pkg/mapper/mapper_test.go b/pkg/mapper/mapper_test.go
index b5a8950..40ec88e 100644
--- a/pkg/mapper/mapper_test.go
+++ b/pkg/mapper/mapper_test.go
@@ -6,6 +6,8 @@
"path/filepath"
"testing"
+ "github.com/KorAP/KoralPipe-TermMapper2/pkg/ast"
+ "github.com/KorAP/KoralPipe-TermMapper2/pkg/matcher"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -226,3 +228,151 @@
})
}
}
+
+func TestMatchComplexPatterns(t *testing.T) {
+ tests := []struct {
+ name string
+ pattern ast.Pattern
+ replacement ast.Replacement
+ input ast.Node
+ expected ast.Node
+ }{
+ {
+ name: "Deep nested pattern with mixed operators",
+ pattern: ast.Pattern{
+ Root: &ast.TermGroup{
+ Operands: []ast.Node{
+ &ast.Term{
+ Key: "A",
+ Match: ast.MatchEqual,
+ },
+ &ast.TermGroup{
+ Operands: []ast.Node{
+ &ast.Term{
+ Key: "B",
+ Match: ast.MatchEqual,
+ },
+ &ast.TermGroup{
+ Operands: []ast.Node{
+ &ast.Term{
+ Key: "C",
+ Match: ast.MatchEqual,
+ },
+ &ast.Term{
+ Key: "D",
+ Match: ast.MatchEqual,
+ },
+ },
+ Relation: ast.AndRelation,
+ },
+ },
+ Relation: ast.OrRelation,
+ },
+ },
+ Relation: ast.AndRelation,
+ },
+ },
+ replacement: ast.Replacement{
+ Root: &ast.Term{
+ Key: "RESULT",
+ Match: ast.MatchEqual,
+ },
+ },
+ input: &ast.TermGroup{
+ Operands: []ast.Node{
+ &ast.Term{
+ Key: "A",
+ Match: ast.MatchEqual,
+ },
+ &ast.TermGroup{
+ Operands: []ast.Node{
+ &ast.Term{
+ Key: "C",
+ Match: ast.MatchEqual,
+ },
+ &ast.Term{
+ Key: "D",
+ Match: ast.MatchEqual,
+ },
+ },
+ Relation: ast.AndRelation,
+ },
+ },
+ Relation: ast.AndRelation,
+ },
+ expected: &ast.Term{
+ Key: "RESULT",
+ Match: ast.MatchEqual,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m, err := matcher.NewMatcher(tt.pattern, tt.replacement)
+ require.NoError(t, err)
+ result := m.Replace(tt.input)
+ assert.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+func TestInvalidPatternReplacement(t *testing.T) {
+ // Create a temporary config file
+ tmpDir := t.TempDir()
+ configFile := filepath.Join(tmpDir, "test-config.yaml")
+
+ configContent := `- id: test-mapper
+ foundryA: opennlp
+ layerA: p
+ foundryB: upos
+ layerB: p
+ mappings:
+ - "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]"`
+
+ err := os.WriteFile(configFile, []byte(configContent), 0644)
+ require.NoError(t, err)
+
+ // Create a new mapper
+ m, err := NewMapper(configFile)
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input string
+ expectError bool
+ errorMsg string
+ }{
+ {
+ name: "Invalid input - empty term group",
+ input: `{
+ "@type": "koral:token",
+ "wrap": {
+ "@type": "koral:termGroup",
+ "operands": [],
+ "relation": "relation:and"
+ }
+ }`,
+ expectError: true,
+ errorMsg: "failed to parse JSON into AST: error parsing wrapped node: term group must have at least one operand",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var inputData any
+ err := json.Unmarshal([]byte(tt.input), &inputData)
+ require.NoError(t, err)
+
+ result, err := m.ApplyMappings("test-mapper", MappingOptions{Direction: AtoB}, inputData)
+ if tt.expectError {
+ assert.Error(t, err)
+ assert.Equal(t, tt.errorMsg, err.Error())
+ assert.Nil(t, result)
+ } else {
+ assert.NoError(t, err)
+ assert.NotNil(t, result)
+ }
+ })
+ }
+}