Add rewrites to mappings
diff --git a/ast/compare.go b/ast/compare.go
new file mode 100644
index 0000000..fec22b1
--- /dev/null
+++ b/ast/compare.go
@@ -0,0 +1,56 @@
+package ast
+
+import (
+ "reflect"
+)
+
+// NodesEqual compares two AST nodes for equality
+func NodesEqual(a, b Node) bool {
+ if a == nil || b == nil {
+ return a == b
+ }
+
+ if a.Type() != b.Type() {
+ return false
+ }
+
+ switch n1 := a.(type) {
+ case *Term:
+ if n2, ok := b.(*Term); ok {
+ return n1.Foundry == n2.Foundry &&
+ n1.Key == n2.Key &&
+ n1.Layer == n2.Layer &&
+ n1.Match == n2.Match &&
+ n1.Value == n2.Value
+ }
+ case *TermGroup:
+ if n2, ok := b.(*TermGroup); ok {
+ if n1.Relation != n2.Relation || len(n1.Operands) != len(n2.Operands) {
+ return false
+ }
+ for i := range n1.Operands {
+ if !NodesEqual(n1.Operands[i], n2.Operands[i]) {
+ return false
+ }
+ }
+ return true
+ }
+ case *Token:
+ if n2, ok := b.(*Token); ok {
+ return NodesEqual(n1.Wrap, n2.Wrap)
+ }
+ case *CatchallNode:
+ if n2, ok := b.(*CatchallNode); ok {
+ return n1.NodeType == n2.NodeType &&
+ reflect.DeepEqual(n1.RawContent, n2.RawContent) &&
+ NodesEqual(n1.Wrap, n2.Wrap)
+ }
+ }
+ return false
+}
+
+// IsTermNode checks if a node is a Term node
+func IsTermNode(node Node) bool {
+ _, ok := node.(*Term)
+ return ok
+}
diff --git a/mapper/mapper.go b/mapper/mapper.go
index fa75bd6..7d7b3db 100644
--- a/mapper/mapper.go
+++ b/mapper/mapper.go
@@ -74,11 +74,12 @@
// MappingOptions contains the options for applying mappings
type MappingOptions struct {
- FoundryA string
- LayerA string
- FoundryB string
- LayerB string
- Direction Direction
+ FoundryA string
+ LayerA string
+ FoundryB string
+ LayerB string
+ Direction Direction
+ AddRewrites bool
}
// ApplyQueryMappings applies the specified mapping rules to a JSON object
@@ -144,6 +145,19 @@
node = tokenWrap
}
+ // Store original node for rewrite if needed
+ var originalNode ast.Node
+ if opts.AddRewrites {
+ originalBytes, err := parser.SerializeToJSON(node)
+ if err != nil {
+ return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
+ }
+ originalNode, err = parser.ParseJSON(originalBytes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse original node for rewrite: %w", err)
+ }
+ }
+
// Apply each rule to the AST
for _, rule := range rules {
// Create pattern and replacement based on direction
@@ -201,6 +215,98 @@
return nil, fmt.Errorf("failed to parse result JSON: %w", err)
}
+ // Add rewrites if enabled and node was changed
+ if opts.AddRewrites && !ast.NodesEqual(node, originalNode) {
+ // Create rewrite object
+ rewrite := map[string]any{
+ "@type": "koral:rewrite",
+ "editor": "termMapper",
+ }
+
+ // Check if all terms in a group have their foundry changed
+ if term, ok := originalNode.(*ast.Term); ok {
+ if termGroup, ok := node.(*ast.TermGroup); ok {
+ // Check if all terms in the group have a different foundry
+ allFoundryChanged := true
+ for _, op := range termGroup.Operands {
+ if t, ok := op.(*ast.Term); ok {
+ if t.Foundry == term.Foundry {
+ allFoundryChanged = false
+ break
+ }
+ }
+ }
+ if allFoundryChanged {
+ rewrite["scope"] = "foundry"
+ rewrite["src"] = term.Foundry
+ } else {
+ // Full node replacement
+ originalBytes, err := parser.SerializeToJSON(originalNode)
+ if err != nil {
+ return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
+ }
+ var originalJSON any
+ if err := json.Unmarshal(originalBytes, &originalJSON); err != nil {
+ return nil, fmt.Errorf("failed to parse original node JSON for rewrite: %w", err)
+ }
+ rewrite["src"] = originalJSON
+ }
+ } else if newTerm, ok := node.(*ast.Term); ok {
+ // Single term changes
+ if term.Foundry != newTerm.Foundry {
+ rewrite["scope"] = "foundry"
+ rewrite["src"] = term.Foundry
+ } else if term.Layer != newTerm.Layer {
+ rewrite["scope"] = "layer"
+ rewrite["src"] = term.Layer
+ } else if term.Key != newTerm.Key {
+ rewrite["scope"] = "key"
+ rewrite["src"] = term.Key
+ } else if term.Value != newTerm.Value {
+ rewrite["scope"] = "value"
+ rewrite["src"] = term.Value
+ } else {
+ // No specific attribute changed, use full node replacement
+ originalBytes, err := parser.SerializeToJSON(originalNode)
+ if err != nil {
+ return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
+ }
+ var originalJSON any
+ if err := json.Unmarshal(originalBytes, &originalJSON); err != nil {
+ return nil, fmt.Errorf("failed to parse original node JSON for rewrite: %w", err)
+ }
+ rewrite["src"] = originalJSON
+ }
+ }
+ } else {
+ // Full node replacement
+ originalBytes, err := parser.SerializeToJSON(originalNode)
+ if err != nil {
+ return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
+ }
+ var originalJSON any
+ if err := json.Unmarshal(originalBytes, &originalJSON); err != nil {
+ return nil, fmt.Errorf("failed to parse original node JSON for rewrite: %w", err)
+ }
+ rewrite["src"] = originalJSON
+ }
+
+ // Add rewrite to the node
+ if resultMap, ok := resultData.(map[string]any); ok {
+ if wrapMap, ok := resultMap["wrap"].(map[string]any); ok {
+ rewrites, exists := wrapMap["rewrites"]
+ if !exists {
+ rewrites = []any{}
+ }
+ if rewritesList, ok := rewrites.([]any); ok {
+ wrapMap["rewrites"] = append(rewritesList, rewrite)
+ } else {
+ wrapMap["rewrites"] = []any{rewrite}
+ }
+ }
+ }
+ }
+
// Restore rewrites if they existed
if oldRewrites != nil {
if resultMap, ok := resultData.(map[string]any); ok {
diff --git a/mapper/mapper_test.go b/mapper/mapper_test.go
index 39389c9..c41f2b9 100644
--- a/mapper/mapper_test.go
+++ b/mapper/mapper_test.go
@@ -79,10 +79,11 @@
}`,
},
{
- name: "B to A direction",
+ name: "Simple A to B mapping with rewrites",
mappingID: "test-mapper",
opts: MappingOptions{
- Direction: BtoA,
+ Direction: AtoB,
+ AddRewrites: true,
},
input: `{
"@type": "koral:token",
@@ -97,21 +98,48 @@
expected: `{
"@type": "koral:token",
"wrap": {
- "@type": "koral:term",
- "foundry": "opennlp",
- "key": "PIDAT",
- "layer": "p",
- "match": "match:eq"
+ "@type": "koral:termGroup",
+ "operands": [
+ {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ },
+ {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "AdjType",
+ "layer": "p",
+ "match": "match:eq",
+ "value": "Pdt"
+ }
+ ],
+ "relation": "relation:and",
+ "rewrites": [
+ {
+ "@type": "koral:rewrite",
+ "editor": "termMapper",
+ "src": {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }
+ ]
}
}`,
- expectError: false,
},
{
- name: "Mapping with foundry override",
+ name: "Mapping with foundry override and rewrites",
mappingID: "test-mapper",
opts: MappingOptions{
- Direction: AtoB,
- FoundryB: "custom",
+ Direction: AtoB,
+ FoundryB: "custom",
+ AddRewrites: true,
},
input: `{
"@type": "koral:token",
@@ -144,11 +172,47 @@
"value": "Pdt"
}
],
- "relation": "relation:and"
+ "relation": "relation:and",
+ "rewrites": [
+ {
+ "@type": "koral:rewrite",
+ "editor": "termMapper",
+ "scope": "foundry",
+ "src": "opennlp"
+ }
+ ]
}
}`,
},
{
+ name: "B to A direction",
+ mappingID: "test-mapper",
+ opts: MappingOptions{
+ Direction: BtoA,
+ },
+ input: `{
+ "@type": "koral:token",
+ "wrap": {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }`,
+ expected: `{
+ "@type": "koral:token",
+ "wrap": {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }`,
+ expectError: false,
+ },
+ {
name: "Invalid mapping ID",
mappingID: "nonexistent",
opts: MappingOptions{