Add RestrictObligatory filter for response rewriting
Change-Id: I30a386ac48fa8dcbd0635b77fa6449c755f0cd59
diff --git a/ast/ast.go b/ast/ast.go
index e47a78e..2da55b8 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -308,3 +308,92 @@
}
}
}
+
+// RestrictToObligatory takes a replacement node from a mapping rule and reduces the boolean structure
+// to only obligatory operations by removing optional OR-relations and keeping required AND-relations.
+// It also applies foundry and layer overrides like ApplyFoundryAndLayerOverrides().
+// Note: This function is designed for mapping rule replacement nodes and does not handle CatchallNodes.
+// For efficiency, restriction is performed first, then foundry/layer overrides are applied to the smaller result.
+//
+// Examples:
+// - (a & b & c) -> (a & b & c) (kept as is)
+// - (a & b & (c | d) & e) -> (a & b & e) (OR-relation removed)
+// - (a | b) -> nil (completely optional)
+func RestrictToObligatory(node Node, foundry, layer string) Node {
+ if node == nil {
+ return nil
+ }
+
+ // First, clone and restrict to obligatory operations
+ cloned := node.Clone()
+ restricted := restrictToObligatoryRecursive(cloned)
+
+ // Then apply foundry and layer overrides to the smaller, restricted tree
+ if restricted != nil {
+ ApplyFoundryAndLayerOverrides(restricted, foundry, layer)
+ }
+
+ return restricted
+}
+
+// restrictToObligatoryRecursive performs the actual restriction logic
+func restrictToObligatoryRecursive(node Node) Node {
+ if node == nil {
+ return nil
+ }
+
+ switch n := node.(type) {
+ case *Term:
+ // Terms are always obligatory
+ return n
+
+ case *Token:
+ // Process the wrapped node
+ if n.Wrap != nil {
+ restricted := restrictToObligatoryRecursive(n.Wrap)
+ if restricted == nil {
+ return nil
+ }
+ return &Token{
+ Wrap: restricted,
+ Rewrites: n.Rewrites,
+ }
+ }
+ return n
+
+ case *TermGroup:
+ if n.Relation == OrRelation {
+ // OR-relations are optional, so remove them
+ return nil
+ } else if n.Relation == AndRelation {
+ // AND-relations are obligatory, but we need to process operands
+ var obligatoryOperands []Node
+ for _, operand := range n.Operands {
+ restricted := restrictToObligatoryRecursive(operand)
+ if restricted != nil {
+ obligatoryOperands = append(obligatoryOperands, restricted)
+ }
+ }
+
+ // If no operands remain, return nil
+ if len(obligatoryOperands) == 0 {
+ return nil
+ }
+
+ // If only one operand remains, return it directly
+ if len(obligatoryOperands) == 1 {
+ return obligatoryOperands[0]
+ }
+
+ // Return the group with obligatory operands
+ return &TermGroup{
+ Operands: obligatoryOperands,
+ Relation: AndRelation,
+ Rewrites: n.Rewrites,
+ }
+ }
+ }
+
+ // For unknown node types, return as is
+ return node
+}
diff --git a/ast/ast_test.go b/ast/ast_test.go
index 49fdbe3..3965f1d 100644
--- a/ast/ast_test.go
+++ b/ast/ast_test.go
@@ -851,3 +851,324 @@
assert.Equal(t, "original_foundry", term.Foundry)
assert.Equal(t, "original_layer", term.Layer)
}
+
+func TestRestrictToObligatory(t *testing.T) {
+ tests := []struct {
+ name string
+ node Node
+ foundry string
+ layer string
+ expected Node
+ }{
+ {
+ name: "Simple term - kept as is",
+ node: &Term{
+ Foundry: "old_foundry",
+ Key: "DET",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expected: &Term{
+ Foundry: "new_foundry",
+ Key: "DET",
+ Layer: "new_layer",
+ Match: MatchEqual,
+ },
+ },
+ {
+ name: "AND group - kept as is",
+ node: &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "old_foundry",
+ Key: "A",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "old_foundry",
+ Key: "B",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "old_foundry",
+ Key: "C",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ },
+ Relation: AndRelation,
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expected: &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "new_foundry",
+ Key: "A",
+ Layer: "new_layer",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "new_foundry",
+ Key: "B",
+ Layer: "new_layer",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "new_foundry",
+ Key: "C",
+ Layer: "new_layer",
+ Match: MatchEqual,
+ },
+ },
+ Relation: AndRelation,
+ },
+ },
+ {
+ name: "OR group - becomes nil",
+ node: &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "old_foundry",
+ Key: "A",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "old_foundry",
+ Key: "B",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ },
+ Relation: OrRelation,
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expected: nil,
+ },
+ {
+ name: "Mixed AND with nested OR - OR removed",
+ node: &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "old_foundry",
+ Key: "A",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "old_foundry",
+ Key: "B",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "old_foundry",
+ Key: "C",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "old_foundry",
+ Key: "D",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ },
+ Relation: OrRelation,
+ },
+ &Term{
+ Foundry: "old_foundry",
+ Key: "E",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ },
+ Relation: AndRelation,
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expected: &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "new_foundry",
+ Key: "A",
+ Layer: "new_layer",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "new_foundry",
+ Key: "B",
+ Layer: "new_layer",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "new_foundry",
+ Key: "E",
+ Layer: "new_layer",
+ Match: MatchEqual,
+ },
+ },
+ Relation: AndRelation,
+ },
+ },
+ {
+ name: "AND group with all OR operands - becomes nil",
+ node: &TermGroup{
+ Operands: []Node{
+ &TermGroup{
+ Operands: []Node{
+ &Term{Key: "A", Match: MatchEqual},
+ &Term{Key: "B", Match: MatchEqual},
+ },
+ Relation: OrRelation,
+ },
+ &TermGroup{
+ Operands: []Node{
+ &Term{Key: "C", Match: MatchEqual},
+ &Term{Key: "D", Match: MatchEqual},
+ },
+ Relation: OrRelation,
+ },
+ },
+ Relation: AndRelation,
+ },
+ foundry: "",
+ layer: "",
+ expected: nil,
+ },
+ {
+ name: "AND group with one operand after restriction - returns single operand",
+ node: &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "old_foundry",
+ Key: "A",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ &TermGroup{
+ Operands: []Node{
+ &Term{Key: "B", Match: MatchEqual},
+ &Term{Key: "C", Match: MatchEqual},
+ },
+ Relation: OrRelation,
+ },
+ },
+ Relation: AndRelation,
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expected: &Term{
+ Foundry: "new_foundry",
+ Key: "A",
+ Layer: "new_layer",
+ Match: MatchEqual,
+ },
+ },
+ {
+ name: "Token with wrapped term",
+ node: &Token{
+ Wrap: &Term{
+ Foundry: "old_foundry",
+ Key: "DET",
+ Layer: "old_layer",
+ Match: MatchEqual,
+ },
+ Rewrites: []Rewrite{
+ {Editor: "test"},
+ },
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expected: &Token{
+ Wrap: &Term{
+ Foundry: "new_foundry",
+ Key: "DET",
+ Layer: "new_layer",
+ Match: MatchEqual,
+ },
+ Rewrites: []Rewrite{
+ {Editor: "test"},
+ },
+ },
+ },
+ {
+ name: "Token with wrapped OR group - becomes nil",
+ node: &Token{
+ Wrap: &TermGroup{
+ Operands: []Node{
+ &Term{Key: "A", Match: MatchEqual},
+ &Term{Key: "B", Match: MatchEqual},
+ },
+ Relation: OrRelation,
+ },
+ },
+ foundry: "",
+ layer: "",
+ expected: nil,
+ },
+ {
+ name: "Nil node",
+ node: nil,
+ foundry: "foundry",
+ layer: "layer",
+ expected: nil,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := RestrictToObligatory(tt.node, tt.foundry, tt.layer)
+ assert.True(t, NodesEqual(tt.expected, result),
+ "Expected: %+v\nGot: %+v", tt.expected, result)
+ })
+ }
+}
+
+func TestRestrictToObligatoryDoesNotModifyOriginal(t *testing.T) {
+ // Test that the original node is not modified
+ original := &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "original",
+ Key: "A",
+ Layer: "original",
+ Match: MatchEqual,
+ },
+ &TermGroup{
+ Operands: []Node{
+ &Term{Key: "B", Match: MatchEqual},
+ &Term{Key: "C", Match: MatchEqual},
+ },
+ Relation: OrRelation,
+ },
+ },
+ Relation: AndRelation,
+ }
+
+ // Clone to check that original remains unchanged
+ originalClone := original.Clone()
+
+ // Apply restriction
+ result := RestrictToObligatory(original, "new_foundry", "new_layer")
+
+ // Original should be unchanged
+ assert.True(t, NodesEqual(originalClone, original))
+
+ // Result should be different
+ expected := &Term{
+ Foundry: "new_foundry",
+ Key: "A",
+ Layer: "new_layer",
+ Match: MatchEqual,
+ }
+ assert.True(t, NodesEqual(expected, result))
+}