Errutil: Update documentation for Go 1.19 (#55807)

This commit is contained in:
Emil Tullstedt 2022-10-07 12:47:43 +02:00 committed by GitHub
parent c2d3a31772
commit 22756913ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 93 additions and 51 deletions

View File

@ -17,20 +17,20 @@
// naturally to experienced and beginner Go developers alike and is
// compatible with standard library features such as the ones in
// the errors package. To achieve this, Grafana's errors are divided
// into the Base and Error types, where the Base contains static
// into the [Base] and [Error] types, where the Base contains static
// information about a category of errors that may occur within a
// service and Error contains the combination of static and dynamic
// information for a particular instance of an error.
//
// A Base would typically be provided as a package-level variable for a
// service using the NewBase constructor with a CoreStatus and a unique
// static message ID that identifies the general structure of the public
// service using the [NewBase] constructor with a [CoreStatus] and a
// unique static message ID that identifies the structure of the public
// message attached to the specific error.
//
// var errNotFound = errutil.NewBase(errutil.StatusNotFound, "service.not-found")
// var errNotFound = errutil.NewBase(errutil.StatusNotFound, "service.notFound")
//
// This Base can now be used to construct a regular Go error with the
// Base.Errorf method using the same structure as fmt.Errorf:
// [Base.Errorf] method using the same structure as [fmt.Errorf]:
//
// return errNotFound.Errorf("looked for thing with ID %d, but it wasn't there: %w", id, err)
//
@ -39,11 +39,11 @@
// possible to override the message sent to the end user by using
// the WithPublicMessage functional option when creating a new Base
//
// var errNotFound = errutil.NewBase(errutil.StatusNotFound "service.not-found", WithPublicMessage("The thing is missing."))
// var errNotFound = errutil.NewBase(errutil.StatusNotFound "service.notFound", WithPublicMessage("The thing is missing."))
//
// If a dynamic message is needed, the Template type extends Base with a
// Go template using text/template from the standard library, refer to
// the documentation related to the Template type for usage examples.
// If a dynamic message is needed, the [Template] type extends Base with
// a Go template using [text/template], refer to the documentation
// related to the Template type for usage examples.
// It is also possible, but discouraged, to manually edit the fields of
// an Error.
package errutil

View File

@ -6,7 +6,7 @@ import (
)
// Base represents the static information about a specific error.
// Always use NewBase to create new instances of Base.
// Always use [NewBase] to create new instances of Base.
type Base struct {
// Because Base is typically instantiated as a package or global
// variable, having private members reduces the probability of a
@ -17,16 +17,16 @@ type Base struct {
logLevel LogLevel
}
// NewBase initializes a Base that is used to construct Error:s.
// NewBase initializes a [Base] that is used to construct [Error].
// The reason is used to determine the status code that should be
// returned for the error, and the msgID is passed to the caller
// to serve as the base for user facing error messages.
//
// msgID should be structured as component.error-brief, for example
// msgID should be structured as component.errorBrief, for example
//
// login.failed-authentication
// dashboards.validation-error
// dashboards.uid-already-exists
// login.failedAuthentication
// dashboards.validationError
// dashboards.uidAlreadyExists
func NewBase(reason StatusReason, msgID string, opts ...BaseOpt) Base {
b := Base{
reason: reason,
@ -44,9 +44,9 @@ func NewBase(reason StatusReason, msgID string, opts ...BaseOpt) Base {
type BaseOpt func(Base) Base
// WithLogLevel sets a custom log level for all errors instantiated from
// this Base.
// this [Base].
//
// Used as a functional option to NewBase.
// Used as a functional option to [NewBase].
func WithLogLevel(lvl LogLevel) BaseOpt {
return func(b Base) Base {
b.logLevel = lvl
@ -55,9 +55,9 @@ func WithLogLevel(lvl LogLevel) BaseOpt {
}
// WithPublicMessage sets the default public message that will be used
// for errors based on this Base.
// for errors based on this [Base].
//
// Used as a functional option to NewBase.
// Used as a functional option to [NewBase].
func WithPublicMessage(message string) BaseOpt {
return func(b Base) Base {
b.publicMessage = message
@ -65,9 +65,9 @@ func WithPublicMessage(message string) BaseOpt {
}
}
// Errorf creates a new Error with the Reason and MessageID from
// Base, and Message and Underlying will be populated using
// the rules of fmt.Errorf.
// Errorf creates a new [Error] with Reason and MessageID from [Base],
// and Message and Underlying will be populated using the rules of
// [fmt.Errorf].
func (b Base) Errorf(format string, args ...interface{}) Error {
err := fmt.Errorf(format, args...)
@ -95,8 +95,10 @@ func (b Base) Status() StatusReason {
return b.reason.Status()
}
// Is validates that an Error has the same reason and messageID as the
// Is validates that an [Error] has the same reason and messageID as the
// Base.
//
// Implements the interface used by [errors.Is].
func (b Base) Is(err error) bool {
// The linter complains that it wants to use errors.As because it
// handles unwrapping, we don't want to do that here since we want
@ -122,24 +124,61 @@ func (b Base) Is(err error) bool {
// boilerplate error handling for status codes and internationalization
// support.
//
// Use [Base.Errorf] or [Template.Build] to construct errors:
//
// // package-level
// var errMonthlyQuota = NewBase(errutil.StatusTooManyRequests, "service.monthlyQuotaReached")
// // in function
// err := errMonthlyQuota.Errorf("user '%s' reached their monthly quota for service", userUID)
//
// or
//
// // package-level
// var errRateLimited = NewBase(errutil.StatusTooManyRequests, "service.backoff").MustTemplate(
// "quota reached for user {{ .Private.user }}, rate limited until {{ .Public.time }}",
// errutil.WithPublic("Too many requests, try again after {{ .Public.time }}"),
// )
// // in function
// err := errRateLimited.Build(TemplateData{
// Private: map[string]interface{ "user": userUID },
// Public: map[string]interface{ "time": rateLimitUntil },
// })
//
// Error implements Unwrap and Is to natively support Go 1.13 style
// errors as described in https://go.dev/blog/go1.13-errors .
type Error struct {
Reason StatusReason
MessageID string
LogMessage string
Underlying error
// Reason provides the Grafana abstracted reason which can be turned
// into an upstream status code depending on the protocol. This
// allows us to use the same errors across HTTP, gRPC, and other
// protocols.
Reason StatusReason
// A MessageID together with PublicPayload should suffice to
// create the PublicMessage. This lets a localization aware client
// construct messages based on structured data.
MessageID string
// LogMessage will be displayed in the server logs or wherever
// [Error.Error] is called.
LogMessage string
// Underlying is the wrapped error returned by [Error.Unwrap].
Underlying error
// PublicMessage is constructed from the template uniquely
// identified by MessageID and the values in PublicPayload (if any)
// to provide the end-user with information that they can use to
// resolve the issue.
PublicMessage string
// PublicPayload provides fields for passing structured data to
// construct localized error messages in the client.
PublicPayload map[string]interface{}
LogLevel LogLevel
// LogLevel provides a suggested level of logging for the error.
LogLevel LogLevel
}
// MarshalJSON returns an error, we do not want raw Error:s being
// MarshalJSON returns an error, we do not want raw [Error]s being
// marshaled into JSON.
//
// Use Public to convert the Error into a PublicError which can be
// marshaled. This is not done automatically, as that conversion is
// lossy.
// Use [Error.Public] to convert the Error into a [PublicError] which
// can safely be marshaled into JSON. This is not done automatically,
// as that conversion is lossy.
func (e Error) MarshalJSON() ([]byte, error) {
return nil, fmt.Errorf("errutil.Error cannot be directly marshaled into JSON")
}
@ -155,8 +194,10 @@ func (e Error) Unwrap() error {
return e.Underlying
}
// Is is used by errors.Is to allow for custom definitions of equality
// between two errors.
// Is checks whether an error is derived from the error passed as an
// argument.
//
// Implements the interface used by [errors.Is].
func (e Error) Is(other error) bool {
// The linter complains that it wants to use errors.As because it
// handles unwrapping, we don't want to do that here since we want

View File

@ -15,8 +15,8 @@ var (
// same error message for the frontend statically within the
// package.
errAbsPath = errutil.NewBase(errutil.StatusBadRequest, "shorturl.absolute-path")
errInvalidPath = errutil.NewBase(errutil.StatusBadRequest, "shorturl.invalid-path")
errAbsPath = errutil.NewBase(errutil.StatusBadRequest, "shorturl.absolutePath")
errInvalidPath = errutil.NewBase(errutil.StatusBadRequest, "shorturl.invalidPath")
errUnexpected = errutil.NewBase(errutil.StatusInternal, "shorturl.unexpected")
)
@ -29,8 +29,8 @@ func Example() {
fmt.Println(e.Error())
// Output:
// 400 shorturl.invalid-path
// [shorturl.invalid-path] path mustn't contain '..': 'abc/../def'
// 400 shorturl.invalidPath
// [shorturl.invalidPath] path mustn't contain '..': 'abc/../def'
}
// CreateShortURL runs a few validations and returns

View File

@ -10,8 +10,8 @@ import (
)
func TestBase_Is(t *testing.T) {
baseNotFound := NewBase(StatusNotFound, "test:not-found")
baseInternal := NewBase(StatusInternal, "test:internal")
baseNotFound := NewBase(StatusNotFound, "test.notFound")
baseInternal := NewBase(StatusInternal, "test.internal")
tests := []struct {
Base Base

View File

@ -50,7 +50,7 @@ type TemplateOpt func(Template) (Template, error)
// MustTemplate panics if the template for Template cannot be compiled.
//
// Only useful for global or package level initialization of Template:s.
// Only useful for global or package level initialization of [Template].
func (b Base) MustTemplate(pattern string, opts ...TemplateOpt) Template {
res, err := b.Template(pattern, opts...)
if err != nil {
@ -63,7 +63,7 @@ func (b Base) MustTemplate(pattern string, opts ...TemplateOpt) Template {
// WithPublic provides templating for the user facing error message based
// on only the fields available in TemplateData.Public.
//
// Used as a functional option to Base.Template.
// Used as a functional option to [Base.Template].
func WithPublic(pattern string) TemplateOpt {
return func(t Template) (Template, error) {
var err error
@ -77,7 +77,7 @@ func WithPublic(pattern string) TemplateOpt {
// TemplateData.Error and TemplateData.Private will not be populated
// when rendering the public message.
//
// Used as a functional option to Base.Template.
// Used as a functional option to [Base.Template].
func WithPublicFromLog() TemplateOpt {
return func(t Template) (Template, error) {
t.publicTemplate = t.logTemplate
@ -85,8 +85,8 @@ func WithPublicFromLog() TemplateOpt {
}
}
// Build returns a new Error based on the base Template and the provided
// TemplateData, wrapping the error in TemplateData.Error if set.
// Build returns a new [Error] based on the base [Template] and the
// provided [TemplateData], wrapping the error in TemplateData.Error.
//
// Build can fail and return an error that is not of type Error.
func (t Template) Build(data TemplateData) error {

View File

@ -5,12 +5,13 @@ import (
"fmt"
"testing"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/util/errutil"
)
func TestTemplate(t *testing.T) {
tmpl := errutil.NewBase(errutil.StatusInternal, "template.sample-error").MustTemplate("[{{ .Public.user }}] got error: {{ .Error }}")
tmpl := errutil.NewBase(errutil.StatusInternal, "template.sampleError").MustTemplate("[{{ .Public.user }}] got error: {{ .Error }}")
err := tmpl.Build(errutil.TemplateData{
Public: map[string]interface{}{
"user": "grot the bot",
@ -30,7 +31,7 @@ func TestTemplate(t *testing.T) {
func ExampleTemplate() {
// Initialization, this is typically done on a package or global
// level.
var tmpl = errutil.NewBase(errutil.StatusInternal, "template.sample-error").MustTemplate("[{{ .Public.user }}] got error: {{ .Error }}")
var tmpl = errutil.NewBase(errutil.StatusInternal, "template.sampleError").MustTemplate("[{{ .Public.user }}] got error: {{ .Error }}")
// Construct an error based on the template.
err := tmpl.Build(errutil.TemplateData{
@ -43,14 +44,14 @@ func ExampleTemplate() {
fmt.Println(err.Error())
// Output:
// [template.sample-error] [grot the bot] got error: oh noes
// [template.sampleError] [grot the bot] got error: oh noes
}
func ExampleTemplate_public() {
// Initialization, this is typically done on a package or global
// level.
var tmpl = errutil.
NewBase(errutil.StatusInternal, "template.sample-error").
NewBase(errutil.StatusInternal, "template.sampleError").
MustTemplate(
"[{{ .Public.user }}] got error: {{ .Error }}",
errutil.WithPublic("Oh, no, error for {{ .Public.user }}"),
@ -69,6 +70,6 @@ func ExampleTemplate_public() {
fmt.Println(err.PublicMessage)
// Output:
// [template.sample-error] [grot the bot] got error: oh noes
// [template.sampleError] [grot the bot] got error: oh noes
// Oh, no, error for grot the bot
}