forked from invopop/gobl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
errors.go
231 lines (202 loc) · 5.66 KB
/
errors.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
package gobl
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/schema"
"github.com/invopop/validation"
)
// An Error provides a structure to better be able to make error comparisons.
// The contents can also be serialised as JSON ready to send to a client
// if needed, see [MarshalJSON] method.
type Error struct {
key cbc.Key
cause error // the underlying error
}
// FieldErrors is a map of field names to errors, which is provided when it
// was possible to determine that the error was related to a specific field.
type FieldErrors map[string]error // nolint:errname
var (
// ErrNoDocument is provided when the envelope does not contain a
// document payload.
ErrNoDocument = NewError("no-document")
// ErrValidation is used when a document fails a validation request.
ErrValidation = NewError("validation")
// ErrCalculation wraps around errors that we're generated during a
// call to perform calculations on a document.
ErrCalculation = NewError("calculation")
// ErrMarshal is provided when there has been a problem attempting to encode
// or marshal an object, usually into JSON.
ErrMarshal = NewError("marshal")
// ErrUnmarshal is used when that has been a problem attempting to read the
// source data.
ErrUnmarshal = NewError("unmarshal")
// ErrSignature identifies an issue related to signatures.
ErrSignature = NewError("signature")
// ErrDigest identifies an issue related to the digest.
ErrDigest = NewError("digest")
// ErrInternal is a "catch-all" for errors that are not expected.
ErrInternal = NewError("internal")
// ErrUnknownSchema is provided when we attempt to determine the schema for an object
// or from an ID and cannot find a match.
ErrUnknownSchema = NewError("unknown-schema")
)
// NewError provides a new error with a code that is meant to provide
// a context.
func NewError(key cbc.Key) *Error {
return &Error{key: key}
}
// wrapError is used to ensure that errors are wrapped around the GOBL standard
// error so they can be output in a consistent manner.
func wrapError(err error) error {
if err == nil {
return nil
}
if _, ok := err.(*Error); ok {
return err // nothing to do
}
if errors.Is(err, schema.ErrUnknownSchema) {
return ErrUnknownSchema
}
switch err.(type) {
case validation.Errors:
return ErrValidation.WithCause(err)
}
return ErrInternal.WithCause(err)
}
// Error provides a string representation of the error.
func (e *Error) Error() string {
if e.cause != nil {
msg := e.cause.Error()
if e.Fields() != nil {
msg = fmt.Sprintf("(%s).", msg)
}
return fmt.Sprintf("%s: %s", e.key, msg)
}
return e.key.String()
}
// WithCause is used to copy and add an underlying error to this one,
// unless the errors is already of type [*Error], in which case it will
// be returned as is.
func (e *Error) WithCause(err error) *Error {
ne := e.copy()
switch te := err.(type) {
case *Error:
return te
case validation.Errors:
ne.cause = fieldErrorsFromValidation(te)
default:
ne.cause = err
}
return ne
}
// WithReason returns the error with a specific reason.
func (e *Error) WithReason(msg string, a ...interface{}) *Error {
ne := e.copy()
ne.cause = fmt.Errorf(msg, a...)
return ne
}
// Key provides the error's key.
func (e *Error) Key() cbc.Key {
return e.key
}
// Fields returns the errors that are associated with specific fields
// or nil if there are no field errors available.
func (e *Error) Fields() FieldErrors {
if fe, ok := e.cause.(FieldErrors); ok {
return fe
}
return nil
}
// Message returns a string representation of the error if it cannot
// be serialised as a map of field names to sub-errors, see also the
// [Fields] method.
func (e *Error) Message() string {
if e.cause == nil {
return ""
}
if e.Fields() != nil {
return ""
}
return e.cause.Error()
}
func (e *Error) copy() *Error {
ne := new(Error)
*ne = *e
return ne
}
// Is checks to see if the target error matches the current error or
// part of the chain.
func (e *Error) Is(target error) bool {
t, ok := target.(*Error)
if !ok {
return errors.Is(e.cause, target)
}
return e.key == t.key
}
// MarshalJSON converts the Error into a valid JSON, correctly
// handling mashaling of cause objects that might not have a
// valid MarhsalJSON method.
func (e *Error) MarshalJSON() ([]byte, error) {
err := struct {
Key cbc.Key `json:"key"`
Fields FieldErrors `json:"fields,omitempty"`
Message string `json:"message,omitempty"`
}{
Key: e.key,
Fields: e.Fields(),
Message: e.Message(),
}
return json.Marshal(err)
}
// Error returns the error string of FieldErrors. This is based on the
// implementation of [validation.Errors].
func (fe FieldErrors) Error() string {
if len(fe) == 0 {
return ""
}
keys := make([]string, len(fe))
i := 0
for key := range fe {
keys[i] = key
i++
}
sort.Strings(keys)
var s strings.Builder
for i, key := range keys {
if i > 0 {
s.WriteString("; ")
}
switch errs := fe[key].(type) {
case FieldErrors, validation.Errors:
_, _ = fmt.Fprintf(&s, "%v: (%v)", key, errs)
default:
_, _ = fmt.Fprintf(&s, "%v: %v", key, fe[key].Error())
}
}
s.WriteString(".")
return s.String()
}
// MarshalJSON converts the FieldErrors into a valid JSON. Based on the
// implementation of [validation.Errors].
func (fe FieldErrors) MarshalJSON() ([]byte, error) {
errs := map[string]any{}
for key, err := range fe {
if ms, ok := err.(json.Marshaler); ok {
errs[key] = ms
} else {
errs[key] = err.Error()
}
}
return json.Marshal(errs)
}
func fieldErrorsFromValidation(errs validation.Errors) FieldErrors {
fe := make(FieldErrors)
for key, err := range errs {
fe[key] = err
}
return fe
}