Repository: go-sprout/sprout
Pull request #82 (2024-11-27 17:54:35)

feat: toIndentYAML -support indentation for YAML output
## Description
Resolves issue described in https://github.com/go-sprout/sprout/issues/79 where a yaml function that supports Indentation is needed.

## Changes
* Add new function toIndentYaml

## Fixes <!-- #issueNumber -->
Fixes https://github.com/go-sprout/sprout/issues/79
## Checklist
- [x] I have read the **CONTRIBUTING.md** document.
- [x] My code follows the code style of this project.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [x] I have updated the documentation accordingly.
- [x] This change requires a change to the documentation on the website.

## Additional Information
<!-- Any additional information regarding this pull request. -->



Patch:
diff --git a/docs/registries/encoding.md b/docs/registries/encoding.md
index 254f294..ea3bd67 100644
--- a/docs/registries/encoding.md
+++ b/docs/registries/encoding.md
@@ -171,3 +171,20 @@ The function serializes a Go data structure into a YAML string, converting the d
 {% endtab %}
 {% endtabs %}
 
+### <mark style="color:purple;">toIndentYaml</mark>
+
+The function serializes a Go data structure into a YAML string, converting the data into a format suitable for YAML representation. In addition to toYaml, toIndentYaml takes a parameter to define the indentation width in spaces.
+
+<table data-header-hidden><thead><tr><th width="164">Name</th><th>Value</th></tr></thead><tbody><tr><td>Signature</td><td><pre class="language-go"><code class="lang-go">ToIndentYAML(indent int, v any) (string, error)
+</code></pre></td></tr></tbody></table>
+
+{% tabs %}
+{% tab title="Template Example" %}
+```go
+{{ $person := dict "name" "John Doe" "age" 30 "location" (dict "country" "US" "planet" "Earth") }}
+{{ $person | toIndentYaml 2 }} // Output: \nage: 30\nlocation:\n  country: US\n  planet: Earth\nname: John Doe
+```
+{% endtab %}
+{% endtabs %}
+
+
diff --git a/registry/encoding/encoding.go b/registry/encoding/encoding.go
index 3712794..c15c425 100644
--- a/registry/encoding/encoding.go
+++ b/registry/encoding/encoding.go
@@ -33,6 +33,7 @@ func (er *EncodingRegistry) RegisterFunctions(funcsMap sprout.FunctionMap) error
 	sprout.AddFunction(funcsMap, "toRawJson", er.ToRawJson)
 	sprout.AddFunction(funcsMap, "fromYaml", er.FromYAML)
 	sprout.AddFunction(funcsMap, "toYaml", er.ToYAML)
+	sprout.AddFunction(funcsMap, "toIndentYaml", er.ToIndentYAML)
 	return nil
 }
 
diff --git a/registry/encoding/functions.go b/registry/encoding/functions.go
index bb5a792..b131680 100644
--- a/registry/encoding/functions.go
+++ b/registry/encoding/functions.go
@@ -230,16 +230,42 @@ func (er *EncodingRegistry) FromYAML(str string) (any, error) {
 //	{{ $d := dict "name" "John Doe" "age" 30 }}
 //	{{ $d | toYaml }} // Output: name: John Doe\nage: 30
 func (er *EncodingRegistry) ToYAML(v any) (out string, err error) {
+	return er.ToIndentYAML(4, v)
+}
+
+// ToIndentYAML serializes a Go data structure to a YAML string and returns any error
+// that occurs during the serialization. It allows to set an indentation width.
+//
+// Parameters:
+//
+//      v any      - the data structure to serialize.
+//      indent int - the indentation
+//      omitempty bool - omit empty fields (default: false)
+//
+// Returns:
+//
+//      string - the YAML string representation of the data structure.
+//      error - error if the serialization fails.
+//
+// Example:
+//
+//      {{ $person := dict "name" "John Doe" "age" 30 "location" (dict "country" "US" "planet" "Earth") }}
+//      {{ $person | toIndentYaml 2 }} // Output: name: John Doe\nage: 30\nlocation:\n  country: US\n  planet: Earth
+
+func (er *EncodingRegistry) ToIndentYAML(indent int, v any) (out string, err error) {
 	// recover panic from yaml package
 	defer sprout.ErrRecoverPanic(&err, "yaml encode error")
 
-	data, err := yaml.Marshal(v)
-	if err != nil {
+	buf := bytes.Buffer{}
+	enc := yaml.NewEncoder(&buf)
+	enc.SetIndent(indent)
+
+	if err = enc.Encode(&v); err != nil {
 		// code unreachable because yaml.Marshal always panic on error and never
 		// returns an error, but we still need to handle the error for the sake of
 		// consistency. The error message is set by ErrRecoverPanic.
-		return "", fmt.Errorf("yaml encode error: %w", err)
+		return "", fmt.Errorf("YAML encode error: %w", err)
 	}
 
-	return strings.TrimSuffix(string(data), "\n"), nil
+	return strings.TrimSuffix(buf.String(), "\n"), nil
 }


Test patch:
diff --git a/registry/encoding/functions_test.go b/registry/encoding/functions_test.go
index f291bfc..1bcaffa 100644
--- a/registry/encoding/functions_test.go
+++ b/registry/encoding/functions_test.go
@@ -61,7 +61,7 @@ func TestFromJson(t *testing.T) {
 
 func TestToJson(t *testing.T) {
 	tc := []pesticide.TestCase{
-		{Name: "TestEmptyInput", Input: `{{ "" | toJson }}`, ExpectedOutput: "\"\""},
+		{Name: "TestEmptyInput", Input: `{{ "" | toJson }}`, ExpectedOutput: `""`},
 		{Name: "TestVariableInput", Input: `{{ .V | toJson }}`, ExpectedOutput: "{\"bar\":\"baz\",\"foo\":55}", Data: map[string]any{"V": map[string]any{"foo": 55, "bar": "baz"}}},
 		{Name: "TestInvalidInput", Input: `{{ .V | toJson }}`, ExpectedErr: "json encode error", Data: map[string]any{"V": make(chan int)}},
 	}
@@ -71,7 +71,7 @@ func TestToJson(t *testing.T) {
 
 func TestToPrettyJson(t *testing.T) {
 	tc := []pesticide.TestCase{
-		{Name: "TestEmptyInput", Input: `{{ "" | toPrettyJson }}`, ExpectedOutput: "\"\""},
+		{Name: "TestEmptyInput", Input: `{{ "" | toPrettyJson }}`, ExpectedOutput: `""`},
 		{Name: "TestVariableInput", Input: `{{ .V | toPrettyJson }}`, ExpectedOutput: "{\n  \"bar\": \"baz\",\n  \"foo\": 55\n}", Data: map[string]any{"V": map[string]any{"foo": 55, "bar": "baz"}}},
 	}
 
@@ -80,7 +80,7 @@ func TestToPrettyJson(t *testing.T) {
 
 func TestToRawJson(t *testing.T) {
 	tc := []pesticide.TestCase{
-		{Name: "TestEmptyInput", Input: `{{ "" | toRawJson }}`, ExpectedOutput: "\"\""},
+		{Name: "TestEmptyInput", Input: `{{ "" | toRawJson }}`, ExpectedOutput: `""`},
 		{Name: "TestVariableInput", Input: `{{ .V | toRawJson }}`, ExpectedOutput: "{\"bar\":\"baz\",\"foo\":55}", Data: map[string]any{"V": map[string]any{"foo": 55, "bar": "baz"}}},
 	}
 
@@ -100,7 +100,7 @@ func TestFromYAML(t *testing.T) {
 
 func TestToYAML(t *testing.T) {
 	tc := []pesticide.TestCase{
-		{Name: "TestEmptyInput", Input: `{{ "" | toYaml }}`, ExpectedOutput: "\"\""},
+		{Name: "TestEmptyInput", Input: `{{ "" | toYaml }}`, ExpectedOutput: `""`},
 		{Name: "TestVariableInput", Input: `{{ .V | toYaml }}`, ExpectedOutput: "bar: baz\nfoo: 55", Data: map[string]any{"V": map[string]any{"foo": 55, "bar": "baz"}}},
 		{Name: "TestInvalidInput", Input: `{{ .V | toYaml }}`, ExpectedErr: "yaml encode error", Data: map[string]any{"V": make(chan int)}},
 	}
@@ -108,6 +108,16 @@ func TestToYAML(t *testing.T) {
 	pesticide.RunTestCases(t, encoding.NewRegistry(), tc)
 }
 
+func TestToIndentYAML(t *testing.T) {
+	tc := []pesticide.TestCase{
+		{Name: "TestEmptyInput", Input: `{{ "" | toIndentYaml 8 }}`, ExpectedOutput: `""`},
+		{Name: "TestVariableInput", Input: `{{ .V | toIndentYaml 8 }}`, ExpectedOutput: "bar: baz\nfoo:\n        bar: baz\n        baz: bar", Data: map[string]any{"V": map[string]any{"foo": map[string]any{"baz": "bar", "bar": "baz"}, "bar": "baz"}}},
+		{Name: "TestInvalidInput", Input: `{{ .V | toIndentYaml 8 }}`, ExpectedErr: "yaml encode error", Data: map[string]any{"V": make(chan int)}},
+	}
+
+	pesticide.RunTestCases(t, encoding.NewRegistry(), tc)
+}
+
 func TestMustFromJson(t *testing.T) {
 	tc := []pesticide.TestCase{
 		{
@@ -141,13 +151,13 @@ func TestMustToJson(t *testing.T) {
 		{
 			Name:           "TestEmptyInput",
 			Input:          `{{ "" | mustToJson }}`,
-			ExpectedOutput: "\"\"",
+			ExpectedOutput: `""`,
 			ExpectedErr:    "",
 		},
 		{
 			Name:           "TestVariableInput",
 			Input:          `{{ .V | mustToJson }}`,
-			ExpectedOutput: "{\"bar\":\"baz\",\"foo\":55}",
+			ExpectedOutput: `{"bar":"baz","foo":55}`,
 			Data:           map[string]any{"V": map[string]any{"foo": 55, "bar": "baz"}},
 			ExpectedErr:    "",
 		},
@@ -168,7 +178,7 @@ func TestMustToPrettyJson(t *testing.T) {
 		{
 			Name:           "TestEmptyInput",
 			Input:          `{{ "" | mustToPrettyJson }}`,
-			ExpectedOutput: "\"\"",
+			ExpectedOutput: `""`,
 			ExpectedErr:    "",
 		},
 		{
@@ -195,13 +205,13 @@ func TestMustToRawJson(t *testing.T) {
 		{
 			Name:           "TestEmptyInput",
 			Input:          `{{ "" | mustToRawJson }}`,
-			ExpectedOutput: "\"\"",
+			ExpectedOutput: `""`,
 			ExpectedErr:    "",
 		},
 		{
 			Name:           "TestVariableInput",
 			Input:          `{{ .V | mustToRawJson }}`,
-			ExpectedOutput: "{\"bar\":\"baz\",\"foo\":55}",
+			ExpectedOutput: `{"bar":"baz","foo":55}`,
 			Data:           map[string]any{"V": map[string]any{"foo": 55, "bar": "baz"}},
 			ExpectedErr:    "",
 		},
@@ -250,7 +260,7 @@ func TestMustToYAML(t *testing.T) {
 		{
 			Name:           "TestEmptyInput",
 			Input:          `{{ "" | mustToYaml }}`,
-			ExpectedOutput: "\"\"",
+			ExpectedOutput: `""`,
 			ExpectedErr:    "",
 		},
 		{