Files
holos/internal/errors/errors.go
Jeff McCune 0c01c9177d render: switch platform on api version (#268)
The holos cli does not use an interface to handle different Platform api
versions.  This makes it difficult to evolve the API in a backwards
compatible way.

This patch adds a top level switch statement to the `holos render
platform` command.  The switch discriminates on the Platform API
version.  v1alpha3 and earlier are classified as legacy versions and
will use the existing strict types.  v1alpha4 and later versions will
use an interface to render the platform, allowing for multiple types to
implement the platform rendering interface.
2024-10-07 16:08:59 -07:00

132 lines
2.9 KiB
Go

// Package errors provides error wrapping with location information
package errors
import (
"context"
"errors"
"fmt"
"log/slog"
"path/filepath"
"runtime"
)
// ErrUnsupported is errors.ErrUnsupported
var ErrUnsupported = errors.ErrUnsupported
func NotImplemented() error {
return wrap(New("not implemented"), 2)
}
// Format calls fmt.Format(format, a...) and wraps the error with the caller
// source location.
func Format(format string, a ...any) error {
return wrap(fmt.Errorf(format, a...), 2)
}
// As calls errors.As
func As(err error, target any) bool {
return errors.As(err, target)
}
// Is calls errors.Is
func Is(err error, target error) bool {
return errors.Is(err, target)
}
// Join calls errors.Join
func Join(errs ...error) error {
return errors.Join(errs...)
}
// New calls errors.New
func New(text string) error {
return errors.New(text)
}
// Unwrap calls errors.Unwrap
func Unwrap(err error) error {
return errors.Unwrap(err)
}
// Source represents the Source file and line where an error was encountered
type Source struct {
File string `json:"file"`
Line int `json:"line"`
}
func (s *Source) Loc() string {
gp := filepath.Base(filepath.Dir(filepath.Dir(s.File)))
p := filepath.Base(filepath.Dir(s.File))
return fmt.Sprintf("%s/%s/%s:%d", gp, p, filepath.Base(s.File), s.Line)
}
// ErrorAt wraps an error with the Source location the error was encountered at
// for tracing from a top level error handler.
type ErrorAt struct {
Err error
Source Source
}
// Unwrap implements error wrapping.
func (e *ErrorAt) Unwrap() error {
return e.Err
}
// Error returns the error string with Source location.
func (e *ErrorAt) Error() string {
return e.Source.Loc() + ": " + e.Err.Error()
}
func wrap(err error, skip int) error {
// Nothing to do
if err == nil {
return nil
}
// Already a holos error no need to do anything.
var errAt *ErrorAt
if errors.As(err, &errAt) {
return err
}
// Try to wrap err with caller info
if _, file, line, ok := runtime.Caller(skip); ok {
return &ErrorAt{
Err: err,
Source: Source{
File: file,
Line: line,
},
}
}
return err
}
// Wrap wraps err in a ErrorAt or returns err if err is nil, already a
// ErrorAt, or caller info is not available.
//
// XXX: Refactor to Err(error, ...slog.Attr). Often want to add attributes for
// the top level logger.
func Wrap(err error) error {
return wrap(err, 2)
}
// Log logs err with Source location if Err is a ErrorAt
func Log(log *slog.Logger, ctx context.Context, level slog.Level, err error, msg string, args ...any) {
var errAt *ErrorAt
if ok := errors.As(err, &errAt); ok {
args = append(args,
slog.String("err", errAt.Unwrap().Error()),
slog.String("loc", errAt.Source.Loc()),
)
} else {
if _, file, line, ok := runtime.Caller(1); ok {
source := Source{file, line}
args = append(args, slog.String("loc", source.Loc()))
}
args = append(args, slog.String("err", err.Error()))
}
log.Log(ctx, level, msg, args...)
}