File size: 5,427 Bytes
04f1444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
package apierrors

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"github.com/openmeterio/openmeter/api/v3/render"
)

// BaseAPIError is the schema for all API apierrors.
type BaseAPIError struct {
	// A unique identifier for this error. When dereferenced it must provide
	// human-readable documentation for the problem. The URL must follow
	// (#122 - Resource Names) and must not contain URI fragments type.
	Type string `json:"type"`

	// The HTTP status code of the error. Useful when passing the response body
	// to child properties in a frontend UI. Must be returned as an integer.
	Status int `json:"status"`

	// A short, human-readable summary of the problem. It should not change
	// between occurrences of a problem, except for localization. Should be
	// provided as "Sentence case" for direct use in the UI.
	Title string `json:"title"`

	// Used to return the correlation ID back to the user, in the format
	// {product}:trace:<trace_id>. This helps us find the relevant logs when a
	// customer reports an issue.
	Instance string `json:"instance"`

	// A human-readable explanation specific to this occurrence of the problem.
	// This field may contain request/entity data to help the user understand
	// what went wrong. Enclose variable values in square brackets. Should be
	// provided as "Sentence case" for direct use in the UI.
	Detail string `json:"detail"`

	// Used to indicate which fields have invalid values when validated. Both a
	// human-readable value (reason) and a type that can be used for localized
	// results (rule) are provided.
	InvalidParameters InvalidParameters `json:"invalid_parameters,omitempty"`

	// UnderlyingError is the underlying error stack to be logged.
	// NOTE: this should not be returned to callers.
	UnderlyingError error `json:"-"`

	// The context used to extract a logger.
	ctx context.Context
}

// InvalidParameters is a collection of fields that failed input validation.
type InvalidParameters []InvalidParameter

type InvalidParameterSource uint8

const (
	InvalidParamSourcePath InvalidParameterSource = iota + 1
	InvalidParamSourceQuery
	InvalidParamSourceBody
	InvalidParamSourceHeader
)

func (i InvalidParameterSource) String() string {
	switch i {
	case InvalidParamSourceBody:
		return "body"
	case InvalidParamSourcePath:
		return "path"
	case InvalidParamSourceHeader:
		return "header"
	case InvalidParamSourceQuery:
		return "query"
	}
	return ""
}

func ToInvalid(s string) InvalidParameterSource {
	switch s {
	case "query":
		return InvalidParamSourceQuery
	case "path":
		return InvalidParamSourcePath
	case "body":
		return InvalidParamSourceBody
	case "header":
		return InvalidParamSourceHeader
	}
	return InvalidParameterSource(0)
}

func (i InvalidParameterSource) MarshalJSON() ([]byte, error) {
	return json.Marshal(i.String())
}

func (i *InvalidParameterSource) UnmarshalJSON(data []byte) error {
	var source string
	if err := json.Unmarshal(data, &source); err != nil {
		return err
	}
	*i = ToInvalid(source)
	return nil
}

// InvalidParameter is a single field that failed input validation.
type InvalidParameter struct {
	// Field concerned by the error.
	Field string `json:"field"`

	// Rule represents the rule that has triggered the error.
	Rule string `json:"rule,omitempty"`

	// Reason describes why the error has been triggered.
	Reason string `json:"reason"`

	// Source describes where the error has been triggered: body, header, path.
	Source InvalidParameterSource `json:"source"`

	// Choices represents the available choices for value in a case of an enum.
	Choices []string `json:"choices,omitempty"`

	// Minimum is an optional field for setting the minimum required value for
	// an attribute.
	Minimum *int `json:"minimum,omitempty"`

	// Maximum is an optional field for setting the maximum required value for
	// an attribute.
	Maximum *int `json:"maximum,omitempty"`

	// Dependents is an optional field for when the rule "dependent_fields" is
	// applied.
	Dependents []string `json:"dependents,omitempty"`
}

// Stringer method for a collection of InvalidParameter entities.
func (ips InvalidParameters) String() string {
	out := new(strings.Builder)
	for i, param := range ips {
		out.WriteString(param.Field)
		if param.Rule != "" {
			_, _ = fmt.Fprintf(out, " [%s]", param.Rule)
		}
		_, _ = fmt.Fprintf(out, ": %s", param.Reason)
		if i != len(ips)-1 {
			_, _ = fmt.Fprintf(out, ", ")
		}
	}
	return out.String()
}

// Error satisfies the error interface.
func (bae *BaseAPIError) Error() string {
	switch {
	case bae.InvalidParameters != nil:
		return fmt.Sprintf("%s: %s", bae.UnderlyingError, bae.InvalidParameters.String())
	case bae.Detail != "" && bae.UnderlyingError != nil:
		return fmt.Sprintf("%s: %s", bae.Detail, bae.UnderlyingError)
	case bae.Detail != "":
		return bae.Detail
	}
	if bae.UnderlyingError != nil {
		return bae.UnderlyingError.Error()
	}
	return bae.Title
}

// Unwrap returns the underlying error
func (bae *BaseAPIError) Unwrap() error {
	return bae.UnderlyingError
}

// Context is the context that created the error
func (bae *BaseAPIError) Context() context.Context {
	return bae.ctx
}

// HandleAPIError is a helper function that accepts an error
func (bae *BaseAPIError) HandleAPIError(
	w http.ResponseWriter,
	r *http.Request,
) {
	_ = render.RenderJSON(w, bae, render.WithContentType(ContentTypeProblemValue), render.WithStatus(bae.Status))
}