Repository: TomWright/dasel
Pull request #415 (2024-06-28 14:03:05)

add support for resolving YAML aliases
Implements the read capability of #285.

@TomWright this is my first time writing Go. Meaning, I don't mind you nitpicking my code.

TomWright: Hey @pmeier, nice quick PR good job.

I've just added a couple of tests to verify things work on CI and it looks good.
TomWright: Thanks for the contribution 👏 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc8a3e0..2898d22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Fixed a bug that could cause a panic. 
 - `type()` now returns `null` instead of `unknown` for null values.
+- Added YAML support for merge tag/aliases. Thanks to [pmeier](https://github.com/pmeier). [Issue 285](https://github.com/TomWright/dasel/issues/285).
 
 ## [v2.7.0] - 2024-03-14
 
diff --git a/dencoding/yaml.go b/dencoding/yaml.go
index 7a62e51..cb0d46d 100644
--- a/dencoding/yaml.go
+++ b/dencoding/yaml.go
@@ -10,6 +10,7 @@ const (
 	yamlTagInt       = "!!int"
 	yamlTagFloat     = "!!float"
 	yamlTagTimestamp = "!!timestamp"
+	yamlTagMerge     = "!!merge"
 )
 
 // YAMLEncoderOption is identifies an option that can be applied to a YAML encoder.
diff --git a/dencoding/yaml_decoder.go b/dencoding/yaml_decoder.go
index 48e04c3..d868cf3 100644
--- a/dencoding/yaml_decoder.go
+++ b/dencoding/yaml_decoder.go
@@ -76,10 +76,18 @@ func (decoder *YAMLDecoder) getMappingNodeValue(node *yaml.Node) (any, error) {
 		keyNode := node.Content[i]
 		valueNode := node.Content[i+1]
 
-		keyValue, err := decoder.getNodeValue(keyNode)
-		if err != nil {
-			return nil, err
+		var keyValue any
+		if keyNode.ShortTag() == yamlTagMerge {
+			keyValue = valueNode.Value
+			valueNode = valueNode.Alias
+		} else {
+			var err error
+			keyValue, err = decoder.getNodeValue(keyNode)
+			if err != nil {
+				return nil, err
+			}
 		}
+
 		value, err := decoder.getNodeValue(valueNode)
 		if err != nil {
 			return nil, err


Test patch:
diff --git a/dencoding/yaml_decoder_test.go b/dencoding/yaml_decoder_test.go
index 3d4f29f..34679bb 100644
--- a/dencoding/yaml_decoder_test.go
+++ b/dencoding/yaml_decoder_test.go
@@ -2,10 +2,11 @@ package dencoding_test
 
 import (
 	"bytes"
-	"github.com/tomwright/dasel/v2/dencoding"
 	"io"
 	"reflect"
 	"testing"
+
+	"github.com/tomwright/dasel/v2/dencoding"
 )
 
 func TestYAMLDecoder_Decode(t *testing.T) {
@@ -96,4 +97,55 @@ key2: value6
 		}
 	})
 
+	t.Run("YamlAliases", func(t *testing.T) {
+		b := []byte(`foo: &foo
+  bar: 1
+  baz: "baz"
+spam:
+  ham: "eggs"
+  <<: *foo
+`)
+
+		dec := dencoding.NewYAMLDecoder(bytes.NewReader(b))
+
+		got := make([]any, 0)
+		for {
+			var v any
+			if err := dec.Decode(&v); err != nil {
+				if err == io.EOF {
+					break
+				}
+				t.Errorf("unexpected error: %v", err)
+				return
+			}
+			got = append(got, v)
+		}
+
+		fooMap := dencoding.NewMap().
+			Set("bar", int64(1)).
+			Set("baz", "baz")
+		spamMap := dencoding.NewMap().
+			Set("ham", "eggs").
+			Set("foo", fooMap)
+
+		exp := dencoding.NewMap().
+			Set("foo", fooMap).
+			Set("spam", spamMap)
+
+		if len(got) != 1 {
+			t.Errorf("expected result len of %d, got %d", 1, len(got))
+			return
+		}
+
+		gotMap, ok := got[0].(*dencoding.Map)
+		if !ok {
+			t.Errorf("expected result to be of type %T, got %T", exp, got[0])
+			return
+		}
+
+		if !reflect.DeepEqual(exp, gotMap) {
+			t.Errorf("expected %v, got %v", exp, gotMap)
+		}
+	})
+
 }
diff --git a/internal/command/select_test.go b/internal/command/select_test.go
index e7bfbc5..31a3955 100644
--- a/internal/command/select_test.go
+++ b/internal/command/select_test.go
@@ -252,6 +252,28 @@ octal: 8`)),
 		nil,
 	))
 
+	t.Run("Issue285 - YAML alias on read", runTest(
+		[]string{"-r", "yaml", "-w", "yaml"},
+		[]byte(`foo: &foo
+  bar: 1
+  baz: baz
+spam:
+  ham: eggs
+  <<: *foo
+`),
+		[]byte(`foo:
+  bar: 1
+  baz: baz
+spam:
+  ham: eggs
+  foo:
+    bar: 1
+    baz: baz
+`),
+		nil,
+		nil,
+	))
+
 	t.Run("OrDefaultString", runTest(
 		[]string{"-r", "json", "all().orDefault(locale,string(nope))"},
 		[]byte(`{
@@ -449,7 +471,7 @@ d.e.f`)),
 		nil,
 		nil,
 	))
-  
+
 	t.Run("Issue346", func(t *testing.T) {
 		t.Run("Select null or default string", runTest(
 			[]string{"-r", "json", "orDefault(foo,string(nope))"},