Optimize mappings
diff --git a/ast/ast.go b/ast/ast.go
index dc7d7f3..e47a78e 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -29,6 +29,7 @@
// Node represents a node in the AST
type Node interface {
Type() NodeType
+ Clone() Node
}
// Rewrite represents a koral:rewrite
@@ -88,6 +89,18 @@
return RewriteNode
}
+// Clone creates a deep copy of the Rewrite node
+func (r *Rewrite) Clone() Node {
+ return &Rewrite{
+ Editor: r.Editor,
+ Operation: r.Operation,
+ Scope: r.Scope,
+ Src: r.Src,
+ Comment: r.Comment,
+ Original: r.Original, // Note: this is a shallow copy of the Original field
+ }
+}
+
// MarshalJSON implements custom JSON marshaling to ensure clean output
func (r *Rewrite) MarshalJSON() ([]byte, error) {
// Create a map with only the modern field names
@@ -128,6 +141,27 @@
return TokenNode
}
+// Clone creates a deep copy of the Token node
+func (t *Token) Clone() Node {
+ var clonedWrap Node
+ if t.Wrap != nil {
+ clonedWrap = t.Wrap.Clone()
+ }
+ tc := &Token{
+ Wrap: clonedWrap,
+ }
+
+ if t.Rewrites != nil {
+ clonedRewrites := make([]Rewrite, len(t.Rewrites))
+ for i, rewrite := range t.Rewrites {
+ clonedRewrites[i] = *rewrite.Clone().(*Rewrite)
+ }
+ tc.Rewrites = clonedRewrites
+ }
+
+ return tc
+}
+
// TermGroup represents a koral:termGroup
type TermGroup struct {
Operands []Node `json:"operands"`
@@ -139,6 +173,27 @@
return TermGroupNode
}
+// Clone creates a deep copy of the TermGroup node
+func (tg *TermGroup) Clone() Node {
+ clonedOperands := make([]Node, len(tg.Operands))
+ for i, operand := range tg.Operands {
+ clonedOperands[i] = operand.Clone()
+ }
+ tgc := &TermGroup{
+ Operands: clonedOperands,
+ Relation: tg.Relation,
+ }
+ if tg.Rewrites != nil {
+ clonedRewrites := make([]Rewrite, len(tg.Rewrites))
+ for i, rewrite := range tg.Rewrites {
+ clonedRewrites[i] = *rewrite.Clone().(*Rewrite)
+ }
+ tgc.Rewrites = clonedRewrites
+ }
+
+ return tgc
+}
+
// Term represents a koral:term
type Term struct {
Foundry string `json:"foundry"`
@@ -153,6 +208,27 @@
return TermNode
}
+// Clone creates a deep copy of the Term node
+func (t *Term) Clone() Node {
+
+ tc := &Term{
+ Foundry: t.Foundry,
+ Key: t.Key,
+ Layer: t.Layer,
+ Match: t.Match,
+ Value: t.Value,
+ }
+
+ if t.Rewrites != nil {
+ clonedRewrites := make([]Rewrite, len(t.Rewrites))
+ for i, rewrite := range t.Rewrites {
+ clonedRewrites[i] = *rewrite.Clone().(*Rewrite)
+ }
+ tc.Rewrites = clonedRewrites
+ }
+ return tc
+}
+
// Pattern represents a pattern to match in the AST
type Pattern struct {
Root Node
@@ -174,3 +250,61 @@
func (c *CatchallNode) Type() NodeType {
return NodeType(c.NodeType)
}
+
+// Clone creates a deep copy of the CatchallNode
+func (c *CatchallNode) Clone() Node {
+ newNode := &CatchallNode{
+ NodeType: c.NodeType,
+ }
+
+ // Handle RawContent properly - preserve nil if it's nil
+ if c.RawContent != nil {
+ newNode.RawContent = make(json.RawMessage, len(c.RawContent))
+ copy(newNode.RawContent, c.RawContent)
+ }
+
+ if c.Wrap != nil {
+ newNode.Wrap = c.Wrap.Clone()
+ }
+
+ if len(c.Operands) > 0 {
+ newNode.Operands = make([]Node, len(c.Operands))
+ for i, operand := range c.Operands {
+ newNode.Operands[i] = operand.Clone()
+ }
+ }
+
+ return newNode
+}
+
+// ApplyFoundryAndLayerOverrides recursively applies foundry and layer overrides to terms
+func ApplyFoundryAndLayerOverrides(node Node, foundry, layer string) {
+ if node == nil {
+ return
+ }
+
+ switch n := node.(type) {
+ case *Term:
+ if foundry != "" {
+ n.Foundry = foundry
+ }
+ if layer != "" {
+ n.Layer = layer
+ }
+ case *TermGroup:
+ for _, op := range n.Operands {
+ ApplyFoundryAndLayerOverrides(op, foundry, layer)
+ }
+ case *Token:
+ if n.Wrap != nil {
+ ApplyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
+ }
+ case *CatchallNode:
+ if n.Wrap != nil {
+ ApplyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
+ }
+ for _, op := range n.Operands {
+ ApplyFoundryAndLayerOverrides(op, foundry, layer)
+ }
+ }
+}
diff --git a/ast/ast_test.go b/ast/ast_test.go
index 0daaf7c..49fdbe3 100644
--- a/ast/ast_test.go
+++ b/ast/ast_test.go
@@ -343,49 +343,39 @@
}
func TestComplexNestedStructures(t *testing.T) {
- // Create a complex nested structure
- innerGroup1 := &TermGroup{
+ // Test nested tokens and term groups
+ termGroup := &TermGroup{
Operands: []Node{
- &Term{Foundry: "f1", Key: "k1", Layer: "l1", Match: MatchEqual},
- &Term{Foundry: "f2", Key: "k2", Layer: "l2", Match: MatchNotEqual},
+ &Term{
+ Foundry: "opennlp",
+ Key: "DET",
+ Layer: "p",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "opennlp",
+ Key: "AdjType",
+ Layer: "m",
+ Match: MatchEqual,
+ Value: "Pdt",
+ },
},
Relation: AndRelation,
}
- innerGroup2 := &TermGroup{
- Operands: []Node{
- &Term{Foundry: "f3", Key: "k3", Layer: "l3", Match: MatchEqual},
- &Term{Foundry: "f4", Key: "k4", Layer: "l4", Match: MatchEqual, Value: "test"},
- },
- Relation: OrRelation,
+ token := &Token{
+ Wrap: termGroup,
}
- topGroup := &TermGroup{
- Operands: []Node{
- innerGroup1,
- innerGroup2,
- &Token{Wrap: &Term{Foundry: "f5", Key: "k5", Layer: "l5", Match: MatchEqual}},
- },
- Relation: AndRelation,
- }
-
- assert.Equal(t, TermGroupNode, topGroup.Type())
- assert.Len(t, topGroup.Operands, 3)
- assert.Equal(t, AndRelation, topGroup.Relation)
-
- // Test inner groups
- group1 := topGroup.Operands[0].(*TermGroup)
- assert.Len(t, group1.Operands, 2)
- assert.Equal(t, AndRelation, group1.Relation)
-
- group2 := topGroup.Operands[1].(*TermGroup)
- assert.Len(t, group2.Operands, 2)
- assert.Equal(t, OrRelation, group2.Relation)
-
- // Test token wrapping
- token := topGroup.Operands[2].(*Token)
+ assert.Equal(t, TokenNode, token.Type())
assert.NotNil(t, token.Wrap)
- assert.Equal(t, TermNode, token.Wrap.Type())
+ assert.Equal(t, TermGroupNode, token.Wrap.Type())
+
+ // Test that the nested structure is correct
+ if tg, ok := token.Wrap.(*TermGroup); ok {
+ assert.Equal(t, 2, len(tg.Operands))
+ assert.Equal(t, AndRelation, tg.Relation)
+ }
}
func TestEdgeCases(t *testing.T) {
@@ -462,3 +452,402 @@
t.Run(tt.name, tt.test)
}
}
+
+func TestCloneMethod(t *testing.T) {
+ tests := []struct {
+ name string
+ node Node
+ }{
+ {
+ name: "Clone Term",
+ node: &Term{
+ Foundry: "opennlp",
+ Key: "DET",
+ Layer: "p",
+ Match: MatchEqual,
+ Value: "test",
+ Rewrites: []Rewrite{
+ {
+ Editor: "test",
+ Scope: "foundry",
+ },
+ },
+ },
+ },
+ {
+ name: "Clone Token",
+ node: &Token{
+ Wrap: &Term{
+ Foundry: "opennlp",
+ Key: "DET",
+ Layer: "p",
+ Match: MatchEqual,
+ },
+ Rewrites: []Rewrite{
+ {
+ Editor: "test",
+ Scope: "layer",
+ },
+ },
+ },
+ },
+ {
+ name: "Clone TermGroup",
+ node: &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "opennlp",
+ Key: "DET",
+ Layer: "p",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "opennlp",
+ Key: "AdjType",
+ Layer: "m",
+ Match: MatchEqual,
+ Value: "Pdt",
+ },
+ },
+ Relation: AndRelation,
+ Rewrites: []Rewrite{
+ {
+ Editor: "test",
+ Scope: "foundry",
+ },
+ },
+ },
+ },
+ {
+ name: "Clone CatchallNode",
+ node: &CatchallNode{
+ NodeType: "koral:unknown",
+ RawContent: []byte(`{"@type":"koral:unknown","test":"value"}`),
+ Wrap: &Term{
+ Foundry: "opennlp",
+ Key: "DET",
+ Layer: "p",
+ Match: MatchEqual,
+ },
+ Operands: []Node{
+ &Term{
+ Foundry: "opennlp",
+ Key: "AdjType",
+ Layer: "m",
+ Match: MatchEqual,
+ Value: "Pdt",
+ },
+ },
+ },
+ },
+ {
+ name: "Clone Rewrite",
+ node: &Rewrite{
+ Editor: "termMapper",
+ Operation: "injection",
+ Scope: "foundry",
+ Src: "test",
+ Comment: "test comment",
+ Original: "original_value",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cloned := tt.node.Clone()
+
+ // Check that the clone is not the same instance
+ assert.NotSame(t, tt.node, cloned)
+
+ // Check that the clone has the same type
+ assert.Equal(t, tt.node.Type(), cloned.Type())
+
+ // Check that nodes are equal (deep comparison)
+ assert.True(t, NodesEqual(tt.node, cloned))
+
+ // Test that modifying the clone doesn't affect the original
+ switch original := tt.node.(type) {
+ case *Term:
+ clonedTerm := cloned.(*Term)
+ clonedTerm.Foundry = "modified"
+ assert.NotEqual(t, original.Foundry, clonedTerm.Foundry)
+
+ case *Token:
+ clonedToken := cloned.(*Token)
+ if clonedToken.Wrap != nil {
+ if termWrap, ok := clonedToken.Wrap.(*Term); ok {
+ termWrap.Foundry = "modified"
+ if originalWrap, ok := original.Wrap.(*Term); ok {
+ assert.NotEqual(t, originalWrap.Foundry, termWrap.Foundry)
+ }
+ }
+ }
+
+ case *TermGroup:
+ clonedGroup := cloned.(*TermGroup)
+ clonedGroup.Relation = OrRelation
+ assert.NotEqual(t, original.Relation, clonedGroup.Relation)
+
+ case *CatchallNode:
+ clonedCatchall := cloned.(*CatchallNode)
+ clonedCatchall.NodeType = "modified"
+ assert.NotEqual(t, original.NodeType, clonedCatchall.NodeType)
+
+ case *Rewrite:
+ clonedRewrite := cloned.(*Rewrite)
+ clonedRewrite.Editor = "modified"
+ assert.NotEqual(t, original.Editor, clonedRewrite.Editor)
+ }
+ })
+ }
+}
+
+func TestCloneNilNodes(t *testing.T) {
+ // Test cloning nodes with nil fields
+ tests := []struct {
+ name string
+ node Node
+ }{
+ {
+ name: "Token with nil wrap",
+ node: &Token{Wrap: nil},
+ },
+ {
+ name: "TermGroup with empty operands",
+ node: &TermGroup{
+ Operands: []Node{},
+ Relation: AndRelation,
+ },
+ },
+ {
+ name: "CatchallNode with nil wrap and operands",
+ node: &CatchallNode{
+ NodeType: "koral:unknown",
+ RawContent: nil,
+ Wrap: nil,
+ Operands: nil,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cloned := tt.node.Clone()
+ assert.NotSame(t, tt.node, cloned)
+ assert.Equal(t, tt.node.Type(), cloned.Type())
+ assert.True(t, NodesEqual(tt.node, cloned))
+ })
+ }
+}
+
+func TestApplyFoundryAndLayerOverrides(t *testing.T) {
+ tests := []struct {
+ name string
+ node Node
+ foundry string
+ layer string
+ expectedChanges func(t *testing.T, node Node)
+ }{
+ {
+ name: "Apply foundry and layer to Term",
+ node: &Term{
+ Foundry: "original",
+ Key: "DET",
+ Layer: "original",
+ Match: MatchEqual,
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expectedChanges: func(t *testing.T, node Node) {
+ term := node.(*Term)
+ assert.Equal(t, "new_foundry", term.Foundry)
+ assert.Equal(t, "new_layer", term.Layer)
+ },
+ },
+ {
+ name: "Apply only foundry to Term",
+ node: &Term{
+ Foundry: "original",
+ Key: "DET",
+ Layer: "original",
+ Match: MatchEqual,
+ },
+ foundry: "new_foundry",
+ layer: "",
+ expectedChanges: func(t *testing.T, node Node) {
+ term := node.(*Term)
+ assert.Equal(t, "new_foundry", term.Foundry)
+ assert.Equal(t, "original", term.Layer) // Should remain unchanged
+ },
+ },
+ {
+ name: "Apply to TermGroup",
+ node: &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "original1",
+ Key: "DET",
+ Layer: "original1",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "original2",
+ Key: "AdjType",
+ Layer: "original2",
+ Match: MatchEqual,
+ Value: "Pdt",
+ },
+ },
+ Relation: AndRelation,
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expectedChanges: func(t *testing.T, node Node) {
+ termGroup := node.(*TermGroup)
+ for _, operand := range termGroup.Operands {
+ if term, ok := operand.(*Term); ok {
+ assert.Equal(t, "new_foundry", term.Foundry)
+ assert.Equal(t, "new_layer", term.Layer)
+ }
+ }
+ },
+ },
+ {
+ name: "Apply to Token with wrapped Term",
+ node: &Token{
+ Wrap: &Term{
+ Foundry: "original",
+ Key: "DET",
+ Layer: "original",
+ Match: MatchEqual,
+ },
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expectedChanges: func(t *testing.T, node Node) {
+ token := node.(*Token)
+ if term, ok := token.Wrap.(*Term); ok {
+ assert.Equal(t, "new_foundry", term.Foundry)
+ assert.Equal(t, "new_layer", term.Layer)
+ }
+ },
+ },
+ {
+ name: "Apply to CatchallNode",
+ node: &CatchallNode{
+ NodeType: "koral:unknown",
+ Wrap: &Term{
+ Foundry: "original",
+ Key: "DET",
+ Layer: "original",
+ Match: MatchEqual,
+ },
+ Operands: []Node{
+ &Term{
+ Foundry: "original2",
+ Key: "AdjType",
+ Layer: "original2",
+ Match: MatchEqual,
+ Value: "Pdt",
+ },
+ },
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expectedChanges: func(t *testing.T, node Node) {
+ catchall := node.(*CatchallNode)
+ if term, ok := catchall.Wrap.(*Term); ok {
+ assert.Equal(t, "new_foundry", term.Foundry)
+ assert.Equal(t, "new_layer", term.Layer)
+ }
+ for _, operand := range catchall.Operands {
+ if term, ok := operand.(*Term); ok {
+ assert.Equal(t, "new_foundry", term.Foundry)
+ assert.Equal(t, "new_layer", term.Layer)
+ }
+ }
+ },
+ },
+ {
+ name: "Apply to nested structure",
+ node: &Token{
+ Wrap: &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "original1",
+ Key: "DET",
+ Layer: "original1",
+ Match: MatchEqual,
+ },
+ &Token{
+ Wrap: &Term{
+ Foundry: "original2",
+ Key: "AdjType",
+ Layer: "original2",
+ Match: MatchEqual,
+ Value: "Pdt",
+ },
+ },
+ },
+ Relation: AndRelation,
+ },
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expectedChanges: func(t *testing.T, node Node) {
+ token := node.(*Token)
+ if termGroup, ok := token.Wrap.(*TermGroup); ok {
+ for _, operand := range termGroup.Operands {
+ switch op := operand.(type) {
+ case *Term:
+ assert.Equal(t, "new_foundry", op.Foundry)
+ assert.Equal(t, "new_layer", op.Layer)
+ case *Token:
+ if innerTerm, ok := op.Wrap.(*Term); ok {
+ assert.Equal(t, "new_foundry", innerTerm.Foundry)
+ assert.Equal(t, "new_layer", innerTerm.Layer)
+ }
+ }
+ }
+ }
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Clone the node to avoid modifying the original test data
+ cloned := tt.node.Clone()
+
+ // Apply the overrides
+ ApplyFoundryAndLayerOverrides(cloned, tt.foundry, tt.layer)
+
+ // Check the expected changes
+ tt.expectedChanges(t, cloned)
+ })
+ }
+}
+
+func TestApplyFoundryAndLayerOverridesNilNode(t *testing.T) {
+ // Test that applying overrides to a nil node doesn't panic
+ assert.NotPanics(t, func() {
+ ApplyFoundryAndLayerOverrides(nil, "foundry", "layer")
+ })
+}
+
+func TestApplyFoundryAndLayerOverridesEmptyValues(t *testing.T) {
+ // Test applying empty foundry and layer values
+ term := &Term{
+ Foundry: "original_foundry",
+ Key: "DET",
+ Layer: "original_layer",
+ Match: MatchEqual,
+ }
+
+ ApplyFoundryAndLayerOverrides(term, "", "")
+
+ // Values should remain unchanged
+ assert.Equal(t, "original_foundry", term.Foundry)
+ assert.Equal(t, "original_layer", term.Layer)
+}
diff --git a/ast/compare.go b/ast/compare.go
index fec22b1..617e8f0 100644
--- a/ast/compare.go
+++ b/ast/compare.go
@@ -21,7 +21,8 @@
n1.Key == n2.Key &&
n1.Layer == n2.Layer &&
n1.Match == n2.Match &&
- n1.Value == n2.Value
+ n1.Value == n2.Value &&
+ rewritesEqual(n1.Rewrites, n2.Rewrites)
}
case *TermGroup:
if n2, ok := b.(*TermGroup); ok {
@@ -33,22 +34,62 @@
return false
}
}
- return true
+ return rewritesEqual(n1.Rewrites, n2.Rewrites)
}
case *Token:
if n2, ok := b.(*Token); ok {
- return NodesEqual(n1.Wrap, n2.Wrap)
+ return NodesEqual(n1.Wrap, n2.Wrap) &&
+ rewritesEqual(n1.Rewrites, n2.Rewrites)
}
case *CatchallNode:
if n2, ok := b.(*CatchallNode); ok {
- return n1.NodeType == n2.NodeType &&
- reflect.DeepEqual(n1.RawContent, n2.RawContent) &&
- NodesEqual(n1.Wrap, n2.Wrap)
+ if n1.NodeType != n2.NodeType ||
+ !reflect.DeepEqual(n1.RawContent, n2.RawContent) ||
+ !NodesEqual(n1.Wrap, n2.Wrap) {
+ return false
+ }
+ // Compare operands
+ if 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 *Rewrite:
+ if n2, ok := b.(*Rewrite); ok {
+ return n1.Editor == n2.Editor &&
+ n1.Operation == n2.Operation &&
+ n1.Scope == n2.Scope &&
+ n1.Src == n2.Src &&
+ n1.Comment == n2.Comment &&
+ reflect.DeepEqual(n1.Original, n2.Original)
}
}
return false
}
+// rewritesEqual compares two slices of Rewrite structs for equality
+func rewritesEqual(a, b []Rewrite) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ if a[i].Editor != b[i].Editor ||
+ a[i].Operation != b[i].Operation ||
+ a[i].Scope != b[i].Scope ||
+ a[i].Src != b[i].Src ||
+ a[i].Comment != b[i].Comment ||
+ !reflect.DeepEqual(a[i].Original, b[i].Original) {
+ return false
+ }
+ }
+ return true
+}
+
// IsTermNode checks if a node is a Term node
func IsTermNode(node Node) bool {
_, ok := node.(*Term)