Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add detailed error information and detailed information about handled keys #273

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
113 changes: 98 additions & 15 deletions error.go
@@ -1,28 +1,116 @@
package mapstructure

import (
"errors"
"fmt"
"reflect"
"sort"
"strings"
)

// FieldError implements the error interface and provide access to
// field path where that error occurred.
type FieldError interface {
error
Path() FieldPath
}

// baseError default implementation of FieldError.
type baseError struct {
error
path FieldPath
}

func (e baseError) Path() FieldPath {
return e.path
}

func formatError(path FieldPath, format string, a ...interface{}) error {
return &baseError{
path: path,
error: fmt.Errorf(format, a...),
}
}

// SliceExpectedError implements the error interface. It provides access to
// field path where that error occurred and specific for this type of errors parameters.
type SliceExpectedError struct {
path FieldPath
Got reflect.Kind
}

func (e SliceExpectedError) Path() FieldPath {
return e.path
}

func (e *SliceExpectedError) Error() string {
return fmt.Sprintf(
"'%s': source data must be an array or slice, got %s", e.path, e.Got)
}

// UnexpectedUnconvertibleTypeError implements the error interface. It provides access to
// field path where that error occurred and specific for this type of errors parameters.
type UnexpectedUnconvertibleTypeError struct {
path FieldPath
Expected reflect.Type
Got reflect.Type
Data interface{}
}

func (e UnexpectedUnconvertibleTypeError) Path() FieldPath {
return e.path
}

func (e *UnexpectedUnconvertibleTypeError) Error() string {
return fmt.Sprintf(
"'%s' expected type '%s', got unconvertible type '%s', value: '%v'",
e.path, e.Expected, e.Got, e.Data)
}

// CanNotParseError implements the error interface. It provides access to
// field path where that error occurred and specific for this type of errors parameters.
type CanNotParseError struct {
path FieldPath
Reason error
Type string
}

func (e CanNotParseError) Path() FieldPath {
return e.path
}

func (e *CanNotParseError) Error() string {
return fmt.Sprintf("cannot parse '%s' as %s: %s", e.path, e.Type, e.Reason)
}

// Error implements the error interface and can represents multiple
// errors that occur in the course of a single decode.
type Error struct {
Errors []string
// Deprecated: left for backward compatibility.
Errors []string
realErrors []error
}

func newMultiError(errors []error) *Error {
stringErrors := make([]string, len(errors))
for i, err := range errors {
stringErrors[i] = err.Error()
}
return &Error{
Errors: stringErrors,
realErrors: errors,
}
}

func (e *Error) Error() string {
points := make([]string, len(e.Errors))
for i, err := range e.Errors {
points[i] = fmt.Sprintf("* %s", err)
points := make([]string, len(e.realErrors))
for i, err := range e.realErrors {
points[i] = fmt.Sprintf("* %s", err.Error())
}

sort.Strings(points)
return fmt.Sprintf(
"%d error(s) decoding:\n\n%s",
len(e.Errors), strings.Join(points, "\n"))
len(e.realErrors), strings.Join(points, "\n"))
}

// WrappedErrors implements the errwrap.Wrapper interface to make this
Expand All @@ -32,19 +120,14 @@ func (e *Error) WrappedErrors() []error {
return nil
}

result := make([]error, len(e.Errors))
for i, e := range e.Errors {
result[i] = errors.New(e)
}

return result
return e.realErrors
}

func appendErrors(errors []string, err error) []string {
func appendErrors(errors []error, err error) []error {
switch e := err.(type) {
case *Error:
return append(errors, e.Errors...)
return append(errors, e.WrappedErrors()...)
default:
return append(errors, e.Error())
return append(errors, e)
}
}
122 changes: 122 additions & 0 deletions field_path.go
@@ -0,0 +1,122 @@
package mapstructure

import (
"strconv"
)

//PathPart is interface for different kinds of FieldPath elements.
type PathPart interface {
getDelimiter() string
String() string
}

//InStructPathPart is FieldPath element that represents field name in structure.
type InStructPathPart struct {
val string
}

func (p InStructPathPart) getDelimiter() string {
return "."
}

func (p InStructPathPart) String() string {
return p.val
}

func (p InStructPathPart) Value() string {
return p.val
}

//InMapPathPart is FieldPath element that represents key in map.
type InMapPathPart struct {
val string
}

func (p InMapPathPart) getDelimiter() string {
return ""
}

func (p InMapPathPart) String() string {
return "[" + p.val + "]"
}

func (p InMapPathPart) Value() string {
return p.val
}

//InSlicePathPart is FieldPath element that represents index in slice or array.
type InSlicePathPart struct {
val int
}

func (p InSlicePathPart) getDelimiter() string {
return ""
}

func (p InSlicePathPart) String() string {
return "[" + strconv.Itoa(p.val) + "]"
}

func (p InSlicePathPart) Value() int {
return p.val
}

//FieldPath represents path to a field in nested structure.
type FieldPath struct {
parts []PathPart
}

func (f FieldPath) addStruct(part string) FieldPath {
return FieldPath{
parts: appendPart(f.parts, InStructPathPart{val: part}),
}
}

func (f FieldPath) addMap(part string) FieldPath {
return FieldPath{
parts: appendPart(f.parts, InMapPathPart{val: part}),
}
}

func (f FieldPath) addSlice(part int) FieldPath {
return FieldPath{
parts: appendPart(f.parts, InSlicePathPart{val: part}),
}
}

func (f FieldPath) notEmpty() bool {
return len(f.parts) > 0
}

func newFieldPath() FieldPath {
return FieldPath{
parts: make([]PathPart, 0),
}
}

func (f FieldPath) Parts() []PathPart {
return f.parts
}

func (f FieldPath) String() string {
result := ""

for i, part := range f.parts {
delimiter := ""

if i > 0 { //there is no delimiter before first element
delimiter = part.getDelimiter()
}

result += delimiter + part.String()
}

return result
}

//appendPart appends PathPart to a PathPart slice with guarantee of slice immutability.
func appendPart(parts []PathPart, part PathPart) []PathPart {
p := make([]PathPart, len(parts))
copy(p, parts)
return append(p, part)
}