Emit per-field scoped rewrites for all changed term attributes to ensure reversibility
Change-Id: Ifd3dd025e217fb1ffee47b7aa571934a02870778
diff --git a/mapper/query.go b/mapper/query.go
index 9aed2db..57de912 100644
--- a/mapper/query.go
+++ b/mapper/query.go
@@ -266,44 +266,67 @@
addRewriteToNode(newInner, oldInner)
}
-// addRewriteToNode creates and attaches a rewrite entry to a node,
+// addRewriteToNode creates and attaches rewrite entries to a node,
// recording what the node looked like before the change.
func addRewriteToNode(newNode, originalNode ast.Node) {
- rw := buildRewrite(originalNode, newNode)
- ast.AppendRewrite(newNode, rw)
+ for _, rw := range buildRewrites(originalNode, newNode) {
+ ast.AppendRewrite(newNode, rw)
+ }
}
-// buildRewrite creates a Rewrite describing what changed between
-// originalNode and newNode. For simple term-level changes (just foundry,
-// layer, key, or value), it uses a scoped rewrite. For structural changes,
-// it stores the full original as an object.
-func buildRewrite(originalNode, newNode ast.Node) ast.Rewrite {
+// buildRewrites creates Rewrite entries describing what changed between
+// originalNode and newNode. For term-level changes it emits one scoped
+// rewrite per changed field so the transformation is fully reversible.
+// For structural changes it stores the full original as an object.
+func buildRewrites(originalNode, newNode ast.Node) []ast.Rewrite {
if term, ok := originalNode.(*ast.Term); ok && ast.IsTermNode(newNode) && originalNode.Type() == newNode.Type() {
newTerm := newNode.(*ast.Term)
+ var rewrites []ast.Rewrite
+
if term.Foundry != newTerm.Foundry {
- return ast.Rewrite{Editor: RewriteEditor, Scope: "foundry", Original: term.Foundry}
+ rw := ast.Rewrite{Editor: RewriteEditor, Scope: "foundry"}
+ if term.Foundry != "" {
+ rw.Original = term.Foundry
+ }
+ rewrites = append(rewrites, rw)
}
if term.Layer != newTerm.Layer {
- return ast.Rewrite{Editor: RewriteEditor, Scope: "layer", Original: term.Layer}
+ rw := ast.Rewrite{Editor: RewriteEditor, Scope: "layer"}
+ if term.Layer != "" {
+ rw.Original = term.Layer
+ }
+ rewrites = append(rewrites, rw)
}
if term.Key != newTerm.Key {
- return ast.Rewrite{Editor: RewriteEditor, Scope: "key", Original: term.Key}
+ rw := ast.Rewrite{Editor: RewriteEditor, Scope: "key"}
+ if term.Key != "" {
+ rw.Original = term.Key
+ }
+ rewrites = append(rewrites, rw)
}
if term.Value != newTerm.Value {
- return ast.Rewrite{Editor: RewriteEditor, Scope: "value", Original: term.Value}
+ rw := ast.Rewrite{Editor: RewriteEditor, Scope: "value"}
+ if term.Value != "" {
+ rw.Original = term.Value
+ }
+ rewrites = append(rewrites, rw)
+ }
+
+ if len(rewrites) > 0 {
+ return rewrites
}
}
// Structural change: serialize the original as the rewrite value
originalBytes, err := parser.SerializeToJSON(originalNode)
if err != nil {
- return ast.Rewrite{Editor: RewriteEditor}
+ return []ast.Rewrite{{Editor: RewriteEditor}}
}
var originalJSON any
if err := json.Unmarshal(originalBytes, &originalJSON); err != nil {
- return ast.Rewrite{Editor: RewriteEditor}
+ return []ast.Rewrite{{Editor: RewriteEditor}}
}
- return ast.Rewrite{Editor: RewriteEditor, Original: originalJSON}
+ return []ast.Rewrite{{Editor: RewriteEditor, Original: originalJSON}}
}
// isValidQueryObject returns true if data is a JSON object with an @type field.