Repository: goccy/go-yaml (go)

Issue:
API to change error behavior
ドキュメントを読んでたら以下を見たのですが

```
errors.ColoredErr = false
```

yamlパッケージで色々処理を書いているのにerrorsのグローバル変数をいじってエラー表示方法を変えるのがちょっと微妙だなー、と思います。

1. ユーザー的には errorsがpublicである必要性がない
2. 必要ならerrorsじゃなくてyamlパッケージに対して処理を依頼するほうがユーザー的には自然
3. そもそも、エラーを表示する方法を変えるだけなので、yaml側で変更するより、ユーザー側のコードで制御するほうが自然なのでは

と感じました。

今のAPIだとこうですが、

```go
import (
   "github.com/goccy/go-yaml"
   "github.com/goccy/go-yaml/errors" // errors.ColoredErrのためだけに必要
)

if err := yaml.Unmarshal(...); err != nil {
   errors.ColoredErr = false
   fmt.Println(err)
}
```

こんな感じにしてしまえばよいかと

```go
import (
   "github.com/goccy/go-yaml"
    // go-yaml/errorsは go-yaml/internal/errors に隠蔽してしまう
)

if err := yaml.Unmarshal(...); err != nil {
   yaml.PrettyPrintError(err) // or yaml.PrettyPrintError(err, boolColoredErr, boolIncludeSource)
}
```

とかにするとユーザー側としては

1. そもそもerrorsパッケージを外部から利用する必要もなければ、internalだから利用もできない
2. errorsではなくyamlパッケージにPrettyPrintを依頼する形になり自然
3. 明示的に yaml.PrettyPrintError()を呼ぶことによって、ユーザー側で出力を変更できるし、グローバル変数にも依存しない

という形にできるかと思います。

よければPR書きます


Pull request:
streamline error generation
fixes #4 

Patch:
diff --git a/README.md b/README.md
index 6e239ed..bae19f6 100644
--- a/README.md
+++ b/README.md
@@ -218,11 +218,19 @@ people:
 
 # 4. Pretty Formatted Errors
 
-<img src="https://user-images.githubusercontent.com/209884/67358124-587f0980-f59a-11e9-96fc-7205aab77695.png"></img>
+Error values produced during parsing has two extra features over regular
+error values.
+
+First by default they contain extra information on the location of the error
+from the source YAML document, to make it easier finding the error location.
 
-If parser receives invalid YAML, the error is printed along with highlighted source code.
+Second, the error messages can optionally be colorized.
 
-※ If you do not want to source code in your error message (colored or otherwise), set `errors.ColoredErr = false` or `errors.WithSourceCode = false` .
+If you would like to control exactly how the output looks like, consider
+using  `yaml.FormatError`, which accepts two boolean values to
+control turning on/off these features
+
+<img src="https://user-images.githubusercontent.com/209884/67358124-587f0980-f59a-11e9-96fc-7205aab77695.png"></img>
 
 # Installation
 
diff --git a/cmd/ycat/ycat.go b/cmd/ycat/ycat.go
index df7be48..c70cb3b 100644
--- a/cmd/ycat/ycat.go
+++ b/cmd/ycat/ycat.go
@@ -7,6 +7,7 @@ import (
 	"os"
 
 	"github.com/fatih/color"
+	"github.com/goccy/go-yaml"
 	"github.com/goccy/go-yaml/lexer"
 	"github.com/goccy/go-yaml/printer"
 	"github.com/mattn/go-colorable"
@@ -77,6 +78,6 @@ func _main(args []string) error {
 
 func main() {
 	if err := _main(os.Args); err != nil {
-		fmt.Printf("%v\n", err)
+		fmt.Printf("%v\n", yaml.FormatError(err, true, true))
 	}
 }
diff --git a/decode.go b/decode.go
index 8790c6b..9f42f46 100644
--- a/decode.go
+++ b/decode.go
@@ -10,7 +10,7 @@ import (
 	"reflect"
 
 	"github.com/goccy/go-yaml/ast"
-	"github.com/goccy/go-yaml/errors"
+	"github.com/goccy/go-yaml/internal/errors"
 	"github.com/goccy/go-yaml/lexer"
 	"github.com/goccy/go-yaml/parser"
 	"github.com/goccy/go-yaml/token"
diff --git a/encode.go b/encode.go
index edd40cd..250a0d0 100644
--- a/encode.go
+++ b/encode.go
@@ -10,7 +10,7 @@ import (
 	"strings"
 
 	"github.com/goccy/go-yaml/ast"
-	"github.com/goccy/go-yaml/errors"
+	"github.com/goccy/go-yaml/internal/errors"
 	"github.com/goccy/go-yaml/printer"
 	"github.com/goccy/go-yaml/token"
 	"golang.org/x/xerrors"
diff --git a/errors/error.go b/internal/errors/error.go
similarity index 65%
rename from errors/error.go
rename to internal/errors/error.go
index ed12ab0..3bf3c15 100644
--- a/errors/error.go
+++ b/internal/errors/error.go
@@ -1,6 +1,7 @@
 package errors
 
 import (
+	"bytes"
 	"fmt"
 
 	"github.com/goccy/go-yaml/printer"
@@ -8,12 +9,12 @@ import (
 	"golang.org/x/xerrors"
 )
 
+const (
+	defaultColorize      = false
+	defaultIncludeSource = true
+)
+
 var (
-	// ColoredErr error with syntax highlight
-	ColoredErr = true
-	// WithSourceCode error with source code
-	WithSourceCode = true
-	// ErrDecodeRequiredPointerType error instance for decoding
 	ErrDecodeRequiredPointerType = xerrors.New("required pointer type value")
 )
 
@@ -66,7 +67,24 @@ type wrapError struct {
 	frame   xerrors.Frame
 }
 
+type myprinter struct {
+	xerrors.Printer
+	colored    bool
+	inclSource bool
+}
+
+func (e *wrapError) PrettyPrint(p xerrors.Printer, colored, inclSource bool) error {
+	return e.FormatError(&myprinter{Printer: p, colored: colored, inclSource: inclSource})
+}
+
 func (e *wrapError) FormatError(p xerrors.Printer) error {
+	if _, ok := p.(*myprinter); !ok {
+		p = &myprinter{
+			Printer:    p,
+			colored:    defaultColorize,
+			inclSource: defaultIncludeSource,
+		}
+	}
 	if e.verb == 'v' && e.state.Flag('+') {
 		// print stack trace for debugging
 		p.Print(e.err, "\n")
@@ -124,7 +142,9 @@ func (e *wrapError) Format(state fmt.State, verb rune) {
 }
 
 func (e *wrapError) Error() string {
-	return e.err.Error()
+	var buf bytes.Buffer
+	e.PrettyPrint(&Sink{&buf}, defaultColorize, defaultIncludeSource)
+	return buf.String()
 }
 
 type syntaxError struct {
@@ -134,25 +154,53 @@ type syntaxError struct {
 	frame xerrors.Frame
 }
 
+func (e *syntaxError) PrettyPrint(p xerrors.Printer, colored, inclSource bool) error {
+	return e.FormatError(&myprinter{Printer: p, colored: colored, inclSource: inclSource})
+}
+
 func (e *syntaxError) FormatError(p xerrors.Printer) error {
+	var pp printer.Printer
+
+	var colored, inclSource bool
+	if mp, ok := p.(*myprinter); ok {
+		colored = mp.colored
+		inclSource = mp.inclSource
+	}
+
+	pos := fmt.Sprintf("[%d:%d] ", e.token.Position.Line, e.token.Position.Column)
+	msg := pp.PrintErrorMessage(fmt.Sprintf("%s%s", pos, e.msg), colored)
+	if inclSource {
+		msg += "\n" + pp.PrintErrorToken(e.token, colored)
+	}
+	p.Print(msg)
+
 	if e.verb == 'v' && e.state.Flag('+') {
 		// %+v
 		// print stack trace for debugging
-		p.Print(e.Error())
 		e.frame.Format(p)
-	} else {
-		p.Print(e.Error())
 	}
 	return nil
 }
 
+type PrettyPrinter interface {
+	PrettyPrint(xerrors.Printer, bool, bool) error
+}
+type Sink struct{ *bytes.Buffer }
+
+func (es *Sink) Print(args ...interface{}) {
+	fmt.Fprint(es.Buffer, args...)
+}
+
+func (es *Sink) Printf(f string, args ...interface{}) {
+	fmt.Fprintf(es.Buffer, f, args...)
+}
+
+func (es *Sink) Detail() bool {
+	return false
+}
+
 func (e *syntaxError) Error() string {
-	var p printer.Printer
-	pos := fmt.Sprintf("[%d:%d] ", e.token.Position.Line, e.token.Position.Column)
-	msg := p.PrintErrorMessage(fmt.Sprintf("syntax error: %s%s", pos, e.msg), ColoredErr)
-	if WithSourceCode {
-		err := p.PrintErrorToken(e.token, ColoredErr)
-		return fmt.Sprintf("%s\n%s", msg, err)
-	}
-	return msg
+	var buf bytes.Buffer
+	e.PrettyPrint(&Sink{&buf}, defaultColorize, defaultIncludeSource)
+	return buf.String()
 }
diff --git a/parser/parser.go b/parser/parser.go
index fa5cf59..06bdf9e 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -4,7 +4,7 @@ import (
 	"strings"
 
 	"github.com/goccy/go-yaml/ast"
-	"github.com/goccy/go-yaml/errors"
+	"github.com/goccy/go-yaml/internal/errors"
 	"github.com/goccy/go-yaml/token"
 )
 
@@ -179,6 +179,52 @@ func (p *Parser) parseMappingValue(ctx *Context) (ast.Node, error) {
 	}
 	keyColumn := key.GetToken().Position.Column
 	valueColumn := value.GetToken().Position.Column
+	if keyColumn < valueColumn && p.isMapNode(value) {
+		// sub mapping
+		node := &ast.MappingCollectionNode{
+			Start:  tk,
+			Values: []ast.Node{value},
+		}
+		mvnode := &ast.MappingValueNode{
+			Start: tk,
+			Key:   key,
+			Value: node,
+		}
+		ntk := ctx.nextToken()
+		if ntk == nil {
+			return mvnode, nil
+		}
+		if ntk.Position.Column != valueColumn {
+			return mvnode, nil
+		}
+		tk := ctx.afterNextToken()
+		for tk != nil && tk.Type == token.MappingValueType {
+			ctx.progress(1)
+			value, err := p.parseToken(ctx, ctx.currentToken())
+			if err != nil {
+				return nil, errors.Wrapf(err, "failed to parse mapping value node")
+			}
+			if !p.isMapNode(value) {
+				return nil, errors.Wrapf(err, "failed to parse mapping value node")
+			}
+			node.Values = append(node.Values, value)
+			nextKeyToken := ctx.nextToken()
+			if nextKeyToken == nil {
+				break
+			}
+			if nextKeyToken.Position.Column != valueColumn {
+				break
+			}
+			tk = ctx.afterNextToken()
+			if tk == nil {
+				break
+			}
+			if tk.Type != token.MappingValueType {
+				break
+			}
+		}
+		return mvnode, nil
+	}
 	if keyColumn == valueColumn {
 		if value.Type() == ast.StringType {
 			ntk := ctx.nextToken()
diff --git a/yaml.go b/yaml.go
index c306273..7331300 100644
--- a/yaml.go
+++ b/yaml.go
@@ -3,7 +3,8 @@ package yaml
 import (
 	"bytes"
 
-	"github.com/goccy/go-yaml/errors"
+	"github.com/goccy/go-yaml/internal/errors"
+	"golang.org/x/xerrors"
 )
 
 // Marshaler interface may be implemented by types to customize their
@@ -111,3 +112,21 @@ func Unmarshal(data []byte, v interface{}) error {
 	}
 	return nil
 }
+
+// FormatError is a utility function that takes advantage of the metadata
+// stored in the errors returned by this package's parser.
+//
+// If the second argument `colored` is true, the error message is colorized.
+// If the third argument `inclSource` is true, the error message will
+// contain snippets of the YAML source that was used.
+func FormatError(e error, colored, inclSource bool) string {
+	var pp errors.PrettyPrinter
+	if xerrors.As(e, &pp) {
+		var buf bytes.Buffer
+		pp.PrettyPrint(&errors.Sink{&buf}, colored, inclSource)
+		return buf.String()
+	}
+
+	return e.Error()
+
+}


Test patch:
diff --git a/decode_test.go b/decode_test.go
index b0e3209..c8a2ae8 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -343,3 +343,31 @@ items:
 		t.Fatal("failed to decode with merge key")
 	}
 }
+
+func TestDecoder_InvalidCases(t *testing.T) {
+	const src = `---
+a:
+- b
+  c: d
+`
+	var v struct {
+		A []string
+	}
+	err := yaml.NewDecoder(strings.NewReader(src)).Decode(&v)
+	if err == nil {
+		t.Fatalf("expected error")
+	}
+
+	if err.Error() != yaml.FormatError(err, false, true) {
+		t.Logf("err.Error() = %s", err.Error())
+		t.Logf("yaml.FormatError(err, false, true) = %s", yaml.FormatError(err, false, true))
+		t.Fatal(`err.Error() should match yaml.FormatError(err, false, true)`)
+	}
+
+	//TODO: properly check if errors are colored/have source
+	t.Logf("%s", err)
+	t.Logf("%s", yaml.FormatError(err, true, false))
+	t.Logf("%s", yaml.FormatError(err, false, true))
+	t.Logf("%s", yaml.FormatError(err, true, true))
+}
+
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 9fe8064..a4c89fd 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -335,26 +335,6 @@ a:
 - - b
 - - c
   - d
-`,
-		},
-		{
-			`
-a:
-  b:
-    c: d
-  e:
-    f: g
-    h: i
-j: k
-`,
-			`
-a:
-  b:
-    c: d
-  e:
-    f: g
-    h: i
-j: k
 `,
 		},
 	}