diff --git a/decode.go b/decode.go index b6e22a5e..f144e78e 100644 --- a/decode.go +++ b/decode.go @@ -534,6 +534,10 @@ func (e *unknownFieldError) Error() string { return e.err.Error() } +func (e *unknownFieldError) Unwrap() error { + return e.err +} + func errUnknownField(msg string, tk *token.Token) *unknownFieldError { return &unknownFieldError{err: errors.ErrSyntax(msg, tk)} } @@ -550,6 +554,10 @@ func (e *duplicateKeyError) Error() string { return e.err.Error() } +func (e *duplicateKeyError) Unwrap() error { + return e.err +} + func errDuplicateKey(msg string, tk *token.Token) *duplicateKeyError { return &duplicateKeyError{err: errors.ErrSyntax(msg, tk)} } diff --git a/error.go b/error.go index 163dcc55..3c24fea9 100644 --- a/error.go +++ b/error.go @@ -2,6 +2,8 @@ package yaml import ( "github.com/goccy/go-yaml/ast" + "github.com/goccy/go-yaml/internal/errors" + "github.com/goccy/go-yaml/token" "golang.org/x/xerrors" ) @@ -60,3 +62,42 @@ func IsInvalidAnchorNameError(err error) bool { func IsInvalidAliasNameError(err error) bool { return xerrors.Is(err, ast.ErrInvalidAliasName) } + +// TokenScopedError represents an error associated with a specific [token.Token]. +type TokenScopedError struct { + // Msg is the underlying error message. + Msg string + // Token is the [token.Token] associated with this error. + Token *token.Token + // err is the underlying, unwraped error. + err error +} + +// Error implements the error interface. +// It returns the unwraped error returned by go-yaml. +func (s TokenScopedError) Error() string { + return s.err.Error() +} + +// AsTokenScopedError checks if the error is associated with a specific token. +// If so, it returns +// Otherwise, it returns nil. +func AsTokenScopedError(err error) *TokenScopedError { + var syntaxError *errors.SyntaxError + if xerrors.As(err, &syntaxError) { + return &TokenScopedError{ + Msg: syntaxError.GetMessage(), + Token: syntaxError.GetToken(), + err: err, + } + } + var typeError *errors.TypeError + if xerrors.As(err, &typeError) { + return &TokenScopedError{ + Msg: typeError.Error(), + Token: typeError.Token, + err: err, + } + } + return nil +} diff --git a/error_test.go b/error_test.go new file mode 100644 index 00000000..35abcb1e --- /dev/null +++ b/error_test.go @@ -0,0 +1,78 @@ +package yaml + +import ( + "reflect" + "testing" + + "github.com/goccy/go-yaml/internal/errors" + "github.com/goccy/go-yaml/token" + "golang.org/x/xerrors" +) + +func TestAsSyntaxError(t *testing.T) { + tests := []struct { + input error + expected *TokenScopedError + }{ + { + input: nil, + expected: nil, + }, + { + input: xerrors.New("dummy test"), + expected: nil, + }, + { + input: errors.ErrSyntax("some error", &token.Token{Value: "123"}), + expected: &TokenScopedError{ + Msg: "some error", + Token: &token.Token{Value: "123"}, + }, + }, + { + input: xerrors.Errorf( + "something went wrong: %w", + errors.ErrSyntax("some error", &token.Token{Value: "123"})), + expected: &TokenScopedError{ + Msg: "some error", + Token: &token.Token{Value: "123"}, + }, + }, + { + input: errUnknownField("unknown field", &token.Token{Value: "123"}), + expected: &TokenScopedError{ + Msg: "unknown field", + Token: &token.Token{Value: "123"}, + }, + }, + { + input: errDuplicateKey("duplicate key", &token.Token{Value: "123"}), + expected: &TokenScopedError{ + Msg: "duplicate key", + Token: &token.Token{Value: "123"}, + }, + }, + { + input: errTypeMismatch(reflect.TypeOf("string"), reflect.TypeOf(0), &token.Token{Value: "123"}), + expected: &TokenScopedError{ + Msg: "cannot unmarshal int into Go value of type string", + Token: &token.Token{Value: "123"}, + }, + }, + } + for _, test := range tests { + syntaxErr := AsTokenScopedError(test.input) + if test.expected == nil { + if syntaxErr != nil { + t.Fatalf("wanted nil, but go %v", syntaxErr) + } + continue + } + if syntaxErr == nil { + t.Fatalf("must not be nil") + } + if *test.expected.Token != *syntaxErr.Token || test.expected.Msg != syntaxErr.Msg { + t.Fatalf("unexpected output.\nexpect:\n[%v]\nactual:\n[%v]", test.expected, syntaxErr) + } + } +} diff --git a/internal/errors/error.go b/internal/errors/error.go index 7f1ea9af..67d36086 100644 --- a/internal/errors/error.go +++ b/internal/errors/error.go @@ -30,8 +30,8 @@ func Wrapf(err error, msg string, args ...interface{}) error { } // ErrSyntax create syntax error instance with message and token -func ErrSyntax(msg string, tk *token.Token) *syntaxError { - return &syntaxError{ +func ErrSyntax(msg string, tk *token.Token) *SyntaxError { + return &SyntaxError{ baseError: &baseError{}, msg: msg, token: tk, @@ -54,7 +54,7 @@ func (e *baseError) chainStateAndVerb(err error) { wrapErr.state = e.state wrapErr.verb = e.verb } - syntaxErr, ok := err.(*syntaxError) + syntaxErr, ok := err.(*SyntaxError) if ok { syntaxErr.state = e.state syntaxErr.verb = e.verb @@ -164,18 +164,18 @@ func (e *wrapError) Error() string { return buf.String() } -type syntaxError struct { +type SyntaxError struct { *baseError msg string token *token.Token frame xerrors.Frame } -func (e *syntaxError) PrettyPrint(p xerrors.Printer, colored, inclSource bool) error { +func (e *SyntaxError) PrettyPrint(p xerrors.Printer, colored, inclSource bool) error { return e.FormatError(&FormatErrorPrinter{Printer: p, Colored: colored, InclSource: inclSource}) } -func (e *syntaxError) FormatError(p xerrors.Printer) error { +func (e *SyntaxError) FormatError(p xerrors.Printer) error { var pp printer.Printer var colored, inclSource bool @@ -199,6 +199,14 @@ func (e *syntaxError) FormatError(p xerrors.Printer) error { return nil } +func (e *SyntaxError) GetToken() *token.Token { + return e.token +} + +func (e *SyntaxError) GetMessage() string { + return e.msg +} + type PrettyPrinter interface { PrettyPrint(xerrors.Printer, bool, bool) error } @@ -216,7 +224,7 @@ func (es *Sink) Detail() bool { return false } -func (e *syntaxError) Error() string { +func (e *SyntaxError) Error() string { var buf bytes.Buffer e.PrettyPrint(&Sink{&buf}, defaultColorize, defaultIncludeSource) return buf.String()