Support legacy fields
diff --git a/ast/ast.go b/ast/ast.go
index 07a6df1..dc7d7f3 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -38,12 +38,86 @@
Scope string `json:"scope,omitempty"`
Src string `json:"src,omitempty"`
Comment string `json:"_comment,omitempty"`
+ Original any `json:"original,omitempty"`
+}
+
+// UnmarshalJSON implements custom JSON unmarshaling for backward compatibility
+func (r *Rewrite) UnmarshalJSON(data []byte) error {
+ // Create a temporary struct to hold all possible fields
+ var temp struct {
+ Type string `json:"@type,omitempty"`
+ Editor string `json:"editor,omitempty"`
+ Source string `json:"source,omitempty"` // legacy field
+ Operation string `json:"operation,omitempty"` // legacy field
+ Scope string `json:"scope,omitempty"`
+ Src string `json:"src,omitempty"`
+ Origin string `json:"origin,omitempty"` // legacy field
+ Original any `json:"original,omitempty"`
+ Comment string `json:"_comment,omitempty"`
+ }
+
+ if err := json.Unmarshal(data, &temp); err != nil {
+ return err
+ }
+
+ // Apply precedence for editor field: editor >> source
+ if temp.Editor != "" {
+ r.Editor = temp.Editor
+ } else if temp.Source != "" {
+ r.Editor = temp.Source
+ }
+
+ // Apply precedence for original/src/origin: original >> src >> origin
+ if temp.Original != nil {
+ r.Original = temp.Original
+ } else if temp.Src != "" {
+ r.Src = temp.Src
+ } else if temp.Origin != "" {
+ r.Src = temp.Origin
+ }
+
+ // Copy other fields
+ r.Operation = temp.Operation
+ r.Scope = temp.Scope
+ r.Comment = temp.Comment
+
+ return nil
}
func (r *Rewrite) Type() NodeType {
return RewriteNode
}
+// MarshalJSON implements custom JSON marshaling to ensure clean output
+func (r *Rewrite) MarshalJSON() ([]byte, error) {
+ // Create a map with only the modern field names
+ result := make(map[string]any)
+
+ // Always include @type if this is a rewrite
+ result["@type"] = "koral:rewrite"
+
+ if r.Editor != "" {
+ result["editor"] = r.Editor
+ }
+ if r.Operation != "" {
+ result["operation"] = r.Operation
+ }
+ if r.Scope != "" {
+ result["scope"] = r.Scope
+ }
+ if r.Src != "" {
+ result["src"] = r.Src
+ }
+ if r.Comment != "" {
+ result["_comment"] = r.Comment
+ }
+ if r.Original != nil {
+ result["original"] = r.Original
+ }
+
+ return json.Marshal(result)
+}
+
// Token represents a koral:token
type Token struct {
Wrap Node `json:"wrap"`
diff --git a/ast/rewrite_test.go b/ast/rewrite_test.go
new file mode 100644
index 0000000..63f5e53
--- /dev/null
+++ b/ast/rewrite_test.go
@@ -0,0 +1,229 @@
+package ast
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRewriteUnmarshalJSON(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected Rewrite
+ }{
+ {
+ name: "Standard rewrite with editor and original",
+ input: `{
+ "@type": "koral:rewrite",
+ "editor": "termMapper",
+ "operation": "operation:mapping",
+ "scope": "foundry",
+ "original": {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }`,
+ expected: Rewrite{
+ Editor: "termMapper",
+ Operation: "operation:mapping",
+ Scope: "foundry",
+ Original: map[string]any{
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq",
+ },
+ },
+ },
+ {
+ name: "Legacy rewrite with source instead of editor",
+ input: `{
+ "@type": "koral:rewrite",
+ "source": "legacy-mapper",
+ "operation": "operation:mapping",
+ "scope": "foundry",
+ "src": "legacy-source"
+ }`,
+ expected: Rewrite{
+ Editor: "legacy-mapper",
+ Operation: "operation:mapping",
+ Scope: "foundry",
+ Src: "legacy-source",
+ },
+ },
+ {
+ name: "Legacy rewrite with origin instead of original/src",
+ input: `{
+ "@type": "koral:rewrite",
+ "editor": "termMapper",
+ "operation": "operation:mapping",
+ "scope": "foundry",
+ "origin": "legacy-origin"
+ }`,
+ expected: Rewrite{
+ Editor: "termMapper",
+ Operation: "operation:mapping",
+ Scope: "foundry",
+ Src: "legacy-origin",
+ },
+ },
+ {
+ name: "Precedence test: editor over source",
+ input: `{
+ "@type": "koral:rewrite",
+ "editor": "preferred-editor",
+ "source": "legacy-source",
+ "operation": "operation:mapping"
+ }`,
+ expected: Rewrite{
+ Editor: "preferred-editor",
+ Operation: "operation:mapping",
+ },
+ },
+ {
+ name: "Precedence test: original over src over origin",
+ input: `{
+ "@type": "koral:rewrite",
+ "editor": "termMapper",
+ "operation": "operation:mapping",
+ "original": "preferred-original",
+ "src": "middle-src",
+ "origin": "lowest-origin"
+ }`,
+ expected: Rewrite{
+ Editor: "termMapper",
+ Operation: "operation:mapping",
+ Original: "preferred-original",
+ },
+ },
+ {
+ name: "Precedence test: src over origin when no original",
+ input: `{
+ "@type": "koral:rewrite",
+ "editor": "termMapper",
+ "operation": "operation:mapping",
+ "src": "preferred-src",
+ "origin": "lowest-origin"
+ }`,
+ expected: Rewrite{
+ Editor: "termMapper",
+ Operation: "operation:mapping",
+ Src: "preferred-src",
+ },
+ },
+ {
+ name: "Only legacy fields",
+ input: `{
+ "@type": "koral:rewrite",
+ "source": "legacy-editor",
+ "operation": "operation:mapping",
+ "origin": "legacy-origin",
+ "_comment": "Legacy rewrite"
+ }`,
+ expected: Rewrite{
+ Editor: "legacy-editor",
+ Operation: "operation:mapping",
+ Src: "legacy-origin",
+ Comment: "Legacy rewrite",
+ },
+ },
+ {
+ name: "Mixed with comment",
+ input: `{
+ "@type": "koral:rewrite",
+ "editor": "termMapper",
+ "operation": "operation:mapping",
+ "scope": "foundry",
+ "src": "original-source",
+ "_comment": "This is a comment"
+ }`,
+ expected: Rewrite{
+ Editor: "termMapper",
+ Operation: "operation:mapping",
+ Scope: "foundry",
+ Src: "original-source",
+ Comment: "This is a comment",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var rewrite Rewrite
+ err := json.Unmarshal([]byte(tt.input), &rewrite)
+ require.NoError(t, err)
+ assert.Equal(t, tt.expected, rewrite)
+ })
+ }
+}
+
+func TestRewriteArrayUnmarshal(t *testing.T) {
+ // Test unmarshaling an array of rewrites with mixed legacy and modern fields
+ input := `[
+ {
+ "@type": "koral:rewrite",
+ "editor": "termMapper",
+ "operation": "operation:mapping",
+ "original": "modern-original"
+ },
+ {
+ "@type": "koral:rewrite",
+ "source": "legacy-editor",
+ "operation": "operation:legacy",
+ "origin": "legacy-origin"
+ }
+ ]`
+
+ var rewrites []Rewrite
+ err := json.Unmarshal([]byte(input), &rewrites)
+ require.NoError(t, err)
+ require.Len(t, rewrites, 2)
+
+ // Check first rewrite (modern)
+ assert.Equal(t, "termMapper", rewrites[0].Editor)
+ assert.Equal(t, "operation:mapping", rewrites[0].Operation)
+ assert.Equal(t, "modern-original", rewrites[0].Original)
+
+ // Check second rewrite (legacy)
+ assert.Equal(t, "legacy-editor", rewrites[1].Editor)
+ assert.Equal(t, "operation:legacy", rewrites[1].Operation)
+ assert.Equal(t, "legacy-origin", rewrites[1].Src)
+}
+
+func TestRewriteMarshalJSON(t *testing.T) {
+ // Test that marshaling works correctly and maintains the modern field names
+ rewrite := Rewrite{
+ Editor: "termMapper",
+ Operation: "operation:mapping",
+ Scope: "foundry",
+ Src: "source-value",
+ Comment: "Test comment",
+ Original: "original-value",
+ }
+
+ data, err := json.Marshal(rewrite)
+ require.NoError(t, err)
+
+ // Parse back to verify structure
+ var result map[string]any
+ err = json.Unmarshal(data, &result)
+ require.NoError(t, err)
+
+ assert.Equal(t, "termMapper", result["editor"])
+ assert.Equal(t, "operation:mapping", result["operation"])
+ assert.Equal(t, "foundry", result["scope"])
+ assert.Equal(t, "source-value", result["src"])
+ assert.Equal(t, "Test comment", result["_comment"])
+ assert.Equal(t, "original-value", result["original"])
+
+ // Ensure legacy fields are not present in output
+ assert.NotContains(t, result, "source")
+ assert.NotContains(t, result, "origin")
+}