From 32e4c1e6bc4e7d0d8451aa6b75200d19e37a536a Mon Sep 17 00:00:00 2001
From: Unknwon <u@gogs.io>
Date: Sun, 19 Nov 2017 00:34:21 -0500
Subject: [PATCH] key: support nested values (#131)

Docs: http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#nested-values
---
 file.go     |  6 ++++++
 ini.go      |  5 ++++-
 key.go      | 24 ++++++++++++++++++++++++
 key_test.go | 31 +++++++++++++++++++++++++++++++
 parser.go   | 14 ++++++++++++++
 5 files changed, 79 insertions(+), 1 deletion(-)

diff --git a/file.go b/file.go
index 93ac508..ce26c3b 100644
--- a/file.go
+++ b/file.go
@@ -345,6 +345,12 @@ func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
 					return nil, err
 				}
 			}
+
+			for _, val := range key.nestedValues {
+				if _, err := buf.WriteString(indent + "  " + val + LineBreak); err != nil {
+					return nil, err
+				}
+			}
 		}
 
 		if PrettySection {
diff --git a/ini.go b/ini.go
index cd7c8a1..535d358 100644
--- a/ini.go
+++ b/ini.go
@@ -32,7 +32,7 @@ const (
 
 	// Maximum allowed depth when recursively substituing variable names.
 	_DEPTH_VALUES = 99
-	_VERSION      = "1.31.1"
+	_VERSION      = "1.32.0"
 )
 
 // Version returns current package version literal.
@@ -134,6 +134,9 @@ type LoadOptions struct {
 	AllowBooleanKeys bool
 	// AllowShadows indicates whether to keep track of keys with same name under same section.
 	AllowShadows bool
+	// AllowNestedValues indicates whether to allow AWS-like nested values.
+	// Docs: http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#nested-values
+	AllowNestedValues bool
 	// UnescapeValueDoubleQuotes indicates whether to unescape double quotes inside value to regular format
 	// when value is surrounded by double quotes, e.g. key="a \"value\"" => key=a "value"
 	UnescapeValueDoubleQuotes bool
diff --git a/key.go b/key.go
index d3eac47..7c8566a 100644
--- a/key.go
+++ b/key.go
@@ -34,6 +34,8 @@ type Key struct {
 
 	isShadow bool
 	shadows  []*Key
+
+	nestedValues []string
 }
 
 // newKey simply return a key object with given values.
@@ -66,6 +68,22 @@ func (k *Key) AddShadow(val string) error {
 	return k.addShadow(val)
 }
 
+func (k *Key) addNestedValue(val string) error {
+	if k.isAutoIncrement || k.isBooleanType {
+		return errors.New("cannot add nested value to auto-increment or boolean key")
+	}
+
+	k.nestedValues = append(k.nestedValues, val)
+	return nil
+}
+
+func (k *Key) AddNestedValue(val string) error {
+	if !k.s.f.options.AllowNestedValues {
+		return errors.New("nested value is not allowed")
+	}
+	return k.addNestedValue(val)
+}
+
 // ValueMapper represents a mapping function for values, e.g. os.ExpandEnv
 type ValueMapper func(string) string
 
@@ -92,6 +110,12 @@ func (k *Key) ValueWithShadows() []string {
 	return vals
 }
 
+// NestedValues returns nested values stored in the key.
+// It is possible returned value is nil if no nested values stored in the key.
+func (k *Key) NestedValues() []string {
+	return k.nestedValues
+}
+
 // transformValue takes a raw value and transforms to its final string.
 func (k *Key) transformValue(val string) string {
 	if k.s.f.ValueMapper != nil {
diff --git a/key_test.go b/key_test.go
index 69b3a97..a13ad95 100644
--- a/key_test.go
+++ b/key_test.go
@@ -15,6 +15,7 @@
 package ini_test
 
 import (
+	"bytes"
 	"fmt"
 	"strings"
 	"testing"
@@ -479,6 +480,36 @@ func TestKey_SetValue(t *testing.T) {
 	})
 }
 
+func TestKey_NestedValues(t *testing.T) {
+	Convey("Read and write nested values", t, func() {
+		f, err := ini.LoadSources(ini.LoadOptions{
+			AllowNestedValues: true,
+		}, []byte(`
+aws_access_key_id = foo
+aws_secret_access_key = bar
+region = us-west-2
+s3 =
+  max_concurrent_requests=10
+  max_queue_size=1000`))
+		So(err, ShouldBeNil)
+		So(f, ShouldNotBeNil)
+
+		So(f.Section("").Key("s3").NestedValues(), ShouldResemble, []string{"max_concurrent_requests=10", "max_queue_size=1000"})
+
+		var buf bytes.Buffer
+		_, err = f.WriteTo(&buf)
+		So(err, ShouldBeNil)
+		So(buf.String(), ShouldEqual, `aws_access_key_id     = foo
+aws_secret_access_key = bar
+region                = us-west-2
+s3                    = 
+  max_concurrent_requests=10
+  max_queue_size=1000
+
+`)
+	})
+}
+
 func TestRecursiveValues(t *testing.T) {
 	Convey("Recursive values should not reflect on same key", t, func() {
 		f, err := ini.Load([]byte(`
diff --git a/parser.go b/parser.go
index 6bd3cd3..db3af8f 100644
--- a/parser.go
+++ b/parser.go
@@ -270,6 +270,10 @@ func (f *File) parse(reader io.Reader) (err error) {
 	}
 	section, _ := f.NewSection(name)
 
+	// This "last" is not strictly equivalent to "previous one" if current key is not the first nested key
+	var isLastValueEmpty bool
+	var lastRegularKey *Key
+
 	var line []byte
 	var inUnparseableSection bool
 	for !p.isEOF {
@@ -278,6 +282,14 @@ func (f *File) parse(reader io.Reader) (err error) {
 			return err
 		}
 
+		if f.options.AllowNestedValues &&
+			isLastValueEmpty && len(line) > 0 {
+			if line[0] == ' ' || line[0] == '\t' {
+				lastRegularKey.addNestedValue(string(bytes.TrimSpace(line)))
+				continue
+			}
+		}
+
 		line = bytes.TrimLeftFunc(line, unicode.IsSpace)
 		if len(line) == 0 {
 			continue
@@ -374,6 +386,7 @@ func (f *File) parse(reader io.Reader) (err error) {
 		if err != nil {
 			return err
 		}
+		isLastValueEmpty = len(value) == 0
 
 		key, err := section.NewKey(kname, value)
 		if err != nil {
@@ -382,6 +395,7 @@ func (f *File) parse(reader io.Reader) (err error) {
 		key.isAutoIncrement = isAutoIncr
 		key.Comment = strings.TrimSpace(p.comment.String())
 		p.comment.Reset()
+		lastRegularKey = key
 	}
 	return nil
 }
-- 
GitLab