Repository navigation
Expand file tree
/
Copy pathcheck_errors.go
More file actions
167 lines (135 loc) · 5.02 KB
/
Copy pathcheck_errors.go
File metadata and controls
167 lines (135 loc) · 5.02 KB
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
// Copyright (c) 2023-2026 Onur Cinar.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://github.com/cinar/checker
package v2
import (
"encoding/json"
"net/http"
"sort"
"strings"
)
// CheckErrors is a map of field names to their corresponding check errors,
// as returned by CheckStruct.
type CheckErrors map[string]error
// FieldError is the JSON representation of a single field's check error. Code
// is the machine-readable check error code, and Message is the localized
// human-readable error message.
type FieldError struct {
// Code is the check error code, such as "REQUIRED". It is empty when the
// field's error is not a *CheckError.
Code string `json:"code"`
// Message is the localized human-readable error message.
Message string `json:"message"`
}
// sortedNames returns the field names of errs, sorted for a deterministic
// iteration order.
func (errs CheckErrors) sortedNames() []string {
names := make([]string, 0, len(errs))
for name := range errs {
names = append(names, name)
}
sort.Strings(names)
return names
}
// Error joins the errors of all the fields into a single human-readable message,
// sorted by field name for a deterministic result. It allows CheckErrors to be
// used and propagated as a regular error.
func (errs CheckErrors) Error() string {
names := errs.sortedNames()
var message strings.Builder
for i, name := range names {
if i > 0 {
message.WriteString("; ")
}
message.WriteString(name)
message.WriteString(": ")
message.WriteString(errs[name].Error())
}
return message.String()
}
// JSON marshals the errors as a JSON object of field name to FieldError,
// localized using the default locale. It is suitable for use directly as
// an HTTP API error response body.
func (errs CheckErrors) JSON() ([]byte, error) {
return errs.JSONWithLocale(DefaultLocale)
}
// JSONWithLocale marshals the errors as a JSON object of field name to
// FieldError, localized using the given locale.
func (errs CheckErrors) JSONWithLocale(locale string) ([]byte, error) {
fields := make(map[string]FieldError, len(errs))
for name, err := range errs {
fields[name] = newFieldError(err, locale)
}
return json.Marshal(fields)
}
// ProblemDetail is the RFC 9457 (Problem Details for HTTP APIs)
// representation of a single field's check error, listed under a
// ProblemDetails' InvalidParams.
type ProblemDetail struct {
// Name is the field name.
Name string `json:"name"`
// Reason is the localized human-readable error message.
Reason string `json:"reason"`
// Code is the check error code, such as "REQUIRED". It is empty when
// the field's error is not a *CheckError.
Code string `json:"code,omitempty"`
}
// ProblemDetails is the RFC 9457 (Problem Details for HTTP APIs)
// representation of a CheckErrors value, suitable for use as the body of
// an "application/problem+json" HTTP response. RFC 9457 leaves Type,
// Title, and Status to the API producer; ProblemDetails/
// ProblemDetailsWithLocale fill in reasonable defaults, and both are
// plain exported fields so callers can override them directly on the
// returned value before serializing it.
type ProblemDetails struct {
// Type is a URI reference identifying the problem type. Defaults to
// "about:blank", the RFC 9457 default for a problem with no more
// specific type of its own.
Type string `json:"type"`
// Title is a short, human-readable summary of the problem type.
Title string `json:"title"`
// Status is the HTTP status code for this occurrence of the problem.
// Defaults to 400 Bad Request.
Status int `json:"status"`
// InvalidParams lists each failing field, sorted by name for a
// deterministic result.
InvalidParams []ProblemDetail `json:"invalid-params"`
}
// ProblemDetails builds an RFC 9457 Problem Details value from errs,
// localized using the default locale.
func (errs CheckErrors) ProblemDetails() *ProblemDetails {
return errs.ProblemDetailsWithLocale(DefaultLocale)
}
// ProblemDetailsWithLocale builds an RFC 9457 Problem Details value from
// errs, localized using the given locale.
func (errs CheckErrors) ProblemDetailsWithLocale(locale string) *ProblemDetails {
names := errs.sortedNames()
invalidParams := make([]ProblemDetail, 0, len(names))
for _, name := range names {
field := newFieldError(errs[name], locale)
invalidParams = append(invalidParams, ProblemDetail{
Name: name,
Reason: field.Message,
Code: field.Code,
})
}
return &ProblemDetails{
Type: "about:blank",
Title: "Your request parameters failed validation.",
Status: http.StatusBadRequest,
InvalidParams: invalidParams,
}
}
// newFieldError converts an error into its JSON field error representation.
// An error that is not a *CheckError only carries a message, since it has no code.
func newFieldError(err error, locale string) FieldError {
checkErr, ok := err.(*CheckError)
if !ok {
return FieldError{Message: err.Error()}
}
return FieldError{
Code: checkErr.Code,
Message: checkErr.ErrorWithLocale(locale),
}
}