mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-19 20:32:35 +02:00
refactor: improve error handling and response formatting in API
This commit is contained in:
parent
82c829de18
commit
98e90d7a0b
31 changed files with 657 additions and 185 deletions
|
@ -1,6 +1,7 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -51,12 +52,12 @@ func (t FileType) GetPath(filename string) string {
|
|||
func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
|
||||
fileType = FileType(r.PathValue("type"))
|
||||
if !fileType.IsValid() {
|
||||
err = gphttp.ErrInvalidKey("type")
|
||||
err = fmt.Errorf("invalid file type: %s", fileType)
|
||||
return
|
||||
}
|
||||
filename = r.PathValue("filename")
|
||||
if filename == "" {
|
||||
err = gphttp.ErrMissingKey("filename")
|
||||
err = fmt.Errorf("missing filename")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
package favicon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
|
@ -21,11 +19,11 @@ import (
|
|||
func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||
url, alias := req.FormValue("url"), req.FormValue("alias")
|
||||
if url == "" && alias == "" {
|
||||
gphttp.ClientError(w, gphttp.ErrMissingKey("url or alias"), http.StatusBadRequest)
|
||||
gphttp.MissingKey(w, "url or alias")
|
||||
return
|
||||
}
|
||||
if url != "" && alias != "" {
|
||||
gphttp.ClientError(w, gperr.New("url and alias are mutually exclusive"), http.StatusBadRequest)
|
||||
gphttp.BadRequest(w, "url and alias are mutually exclusive")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -33,7 +31,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
|||
if url != "" {
|
||||
var iconURL homepage.IconURL
|
||||
if err := iconURL.Parse(url); err != nil {
|
||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
||||
gphttp.ClientError(w, req, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
fetchResult := homepage.FetchFavIconFromURL(req.Context(), &iconURL)
|
||||
|
@ -49,7 +47,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
|||
// try with route.Icon
|
||||
r, ok := routes.HTTP.Get(alias)
|
||||
if !ok {
|
||||
gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound)
|
||||
gphttp.ValueNotFound(w, "route", alias)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
icons, err := homepage.SearchIcons(r.FormValue("keyword"), limit)
|
||||
if err != nil {
|
||||
gphttp.ClientError(w, err)
|
||||
gphttp.ClientError(w, r, err)
|
||||
return
|
||||
}
|
||||
gphttp.RespondJSON(w, r, icons)
|
||||
|
|
|
@ -20,27 +20,27 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
|
|||
q := r.URL.Query()
|
||||
name := q.Get("name")
|
||||
if name == "" {
|
||||
gphttp.ClientError(w, gphttp.ErrMissingKey("name"))
|
||||
gphttp.MissingKey(w, "name")
|
||||
return
|
||||
}
|
||||
host := q.Get("host")
|
||||
if host == "" {
|
||||
gphttp.ClientError(w, gphttp.ErrMissingKey("host"))
|
||||
gphttp.MissingKey(w, "host")
|
||||
return
|
||||
}
|
||||
portStr := q.Get("port")
|
||||
if portStr == "" {
|
||||
gphttp.ClientError(w, gphttp.ErrMissingKey("port"))
|
||||
gphttp.MissingKey(w, "port")
|
||||
return
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
gphttp.ClientError(w, gphttp.ErrInvalidKey("port"))
|
||||
gphttp.InvalidKey(w, "port")
|
||||
return
|
||||
}
|
||||
hostport := fmt.Sprintf("%s:%d", host, port)
|
||||
if _, ok := config.GetInstance().GetAgent(hostport); ok {
|
||||
gphttp.ClientError(w, gphttp.ErrAlreadyExists("agent", hostport), http.StatusConflict)
|
||||
gphttp.KeyAlreadyExists(w, "agent", hostport)
|
||||
return
|
||||
}
|
||||
t := q.Get("type")
|
||||
|
@ -48,10 +48,10 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
|
|||
case "docker", "system":
|
||||
break
|
||||
case "":
|
||||
gphttp.ClientError(w, gphttp.ErrMissingKey("type"))
|
||||
gphttp.MissingKey(w, "type")
|
||||
return
|
||||
default:
|
||||
gphttp.ClientError(w, gphttp.ErrInvalidKey("type"))
|
||||
gphttp.InvalidKey(w, "type")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -109,13 +109,13 @@ func VerifyNewAgent(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if err := json.Unmarshal(clientPEMData, &data); err != nil {
|
||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
||||
gphttp.ClientError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(data.Host, data.CA, data.Client)
|
||||
if err != nil {
|
||||
gphttp.ClientError(w, err)
|
||||
gphttp.ClientError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -127,7 +127,7 @@ func VerifyNewAgent(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
filename, ok := certs.AgentCertsFilepath(data.Host)
|
||||
if !ok {
|
||||
gphttp.ClientError(w, gphttp.ErrInvalidKey("host"))
|
||||
gphttp.InvalidKey(w, "host")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
|||
r = r.WithContext(context.WithValue(r.Context(), nextHandlerContextKey, next))
|
||||
defaultAuth.LoginHandler(w, r)
|
||||
} else {
|
||||
gphttp.ClientError(w, err, http.StatusUnauthorized)
|
||||
gphttp.Unauthorized(w, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
|
@ -70,7 +69,7 @@ func (cfg *Config) Validate() gperr.Error {
|
|||
if !ok {
|
||||
b.Add(ErrUnknownProvider.
|
||||
Subject(cfg.Provider).
|
||||
Withf(strutils.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
|
||||
With(gperr.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
|
||||
} else {
|
||||
_, err := providerConstructor(cfg.Options)
|
||||
if err != nil {
|
||||
|
|
|
@ -37,11 +37,11 @@ func (err *baseError) Subjectf(format string, args ...any) Error {
|
|||
}
|
||||
|
||||
func (err baseError) With(extra error) Error {
|
||||
return &nestedError{&err, []error{extra}}
|
||||
return &nestedError{err.Err, []error{extra}}
|
||||
}
|
||||
|
||||
func (err baseError) Withf(format string, args ...any) Error {
|
||||
return &nestedError{&err, []error{fmt.Errorf(format, args...)}}
|
||||
return &nestedError{err.Err, []error{fmt.Errorf(format, args...)}}
|
||||
}
|
||||
|
||||
func (err *baseError) Error() string {
|
||||
|
@ -62,3 +62,11 @@ func (err *baseError) MarshalJSON() ([]byte, error) {
|
|||
return json.Marshal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (err *baseError) Plain() []byte {
|
||||
return Plain(err.Err)
|
||||
}
|
||||
|
||||
func (err *baseError) Markdown() []byte {
|
||||
return Markdown(err.Err)
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ func TestBuilderNested(t *testing.T) {
|
|||
• Inner: 1
|
||||
• Inner: 2
|
||||
• Action 2
|
||||
• Inner: 3`
|
||||
• Inner: 3
|
||||
`
|
||||
ExpectEqual(t, got, expected)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,16 @@ type Error interface {
|
|||
Subject(subject string) Error
|
||||
// Subjectf is a wrapper for Subject(fmt.Sprintf(format, args...)).
|
||||
Subjectf(format string, args ...any) Error
|
||||
PlainError
|
||||
MarkdownError
|
||||
}
|
||||
|
||||
type PlainError interface {
|
||||
Plain() []byte
|
||||
}
|
||||
|
||||
type MarkdownError interface {
|
||||
Markdown() []byte
|
||||
}
|
||||
|
||||
// this makes JSON marshaling work,
|
||||
|
|
|
@ -153,6 +153,7 @@ func TestErrorStringNested(t *testing.T) {
|
|||
• 2
|
||||
• action 3 > inner3: generic failure
|
||||
• 3
|
||||
• 3`
|
||||
• 3
|
||||
`
|
||||
expect.Equal(t, ansi.StripANSI(ne.Error()), want)
|
||||
}
|
||||
|
|
43
internal/gperr/hint.go
Normal file
43
internal/gperr/hint.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package gperr
|
||||
|
||||
import "github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||
|
||||
type Hint struct {
|
||||
Prefix string
|
||||
Message string
|
||||
Suffix string
|
||||
}
|
||||
|
||||
var _ PlainError = (*Hint)(nil)
|
||||
var _ MarkdownError = (*Hint)(nil)
|
||||
|
||||
func (h *Hint) Error() string {
|
||||
return h.Prefix + ansi.Info(h.Message) + h.Suffix
|
||||
}
|
||||
|
||||
func (h *Hint) Plain() []byte {
|
||||
return []byte(h.Prefix + h.Message + h.Suffix)
|
||||
}
|
||||
|
||||
func (h *Hint) Markdown() []byte {
|
||||
return []byte(h.Prefix + "**" + h.Message + "**" + h.Suffix)
|
||||
}
|
||||
|
||||
func (h *Hint) MarshalText() ([]byte, error) {
|
||||
return h.Plain(), nil
|
||||
}
|
||||
|
||||
func (h *Hint) String() string {
|
||||
return h.Error()
|
||||
}
|
||||
|
||||
func DoYouMean(s string) *Hint {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &Hint{
|
||||
Prefix: "Do you mean ",
|
||||
Message: s,
|
||||
Suffix: "?",
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ func log(msg string, err error, level zerolog.Level, logger ...*zerolog.Logger)
|
|||
} else {
|
||||
l = logging.GetLogger()
|
||||
}
|
||||
l.WithLevel(level).Msg(New(highlight(msg)).With(err).Error())
|
||||
l.WithLevel(level).Msg(New(highlightANSI(msg)).With(err).Error())
|
||||
switch level {
|
||||
case zerolog.FatalLevel:
|
||||
os.Exit(1)
|
||||
|
|
|
@ -3,8 +3,6 @@ package gperr
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
//nolint:recvcheck
|
||||
|
@ -67,48 +65,98 @@ func (err *nestedError) Is(other error) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
var nilError = newError("<nil>")
|
||||
var bulletPrefix = []byte("• ")
|
||||
var markdownBulletPrefix = []byte("- ")
|
||||
var spaces = []byte(" ")
|
||||
|
||||
type appendLineFunc func(buf []byte, err error, level int) []byte
|
||||
|
||||
func (err *nestedError) Error() string {
|
||||
if err == nil {
|
||||
return makeLine("<nil>", 0)
|
||||
return nilError.Error()
|
||||
}
|
||||
|
||||
if err.Err != nil {
|
||||
lines := make([]string, 0, 1+len(err.Extras))
|
||||
lines = append(lines, makeLine(err.Err.Error(), 0))
|
||||
lines = append(lines, makeLines(err.Extras, 1)...)
|
||||
return strutils.JoinLines(lines)
|
||||
buf := appendLineNormal(nil, err.Err, 0)
|
||||
if len(err.Extras) > 0 {
|
||||
buf = append(buf, '\n')
|
||||
buf = appendLines(buf, err.Extras, 1, appendLineNormal)
|
||||
}
|
||||
return strutils.JoinLines(makeLines(err.Extras, 0))
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func makeLine(err string, level int) string {
|
||||
const bulletPrefix = "• "
|
||||
const spaces = " "
|
||||
func (err *nestedError) Plain() []byte {
|
||||
if err == nil {
|
||||
return appendLinePlain(nil, nilError, 0)
|
||||
}
|
||||
buf := appendLinePlain(nil, err.Err, 0)
|
||||
if len(err.Extras) > 0 {
|
||||
buf = append(buf, '\n')
|
||||
buf = appendLines(buf, err.Extras, 1, appendLinePlain)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func (err *nestedError) Markdown() []byte {
|
||||
if err == nil {
|
||||
return appendLineMd(nil, nilError, 0)
|
||||
}
|
||||
|
||||
buf := appendLineMd(nil, err.Err, 0)
|
||||
if len(err.Extras) > 0 {
|
||||
buf = append(buf, '\n')
|
||||
buf = appendLines(buf, err.Extras, 1, appendLineMd)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func appendLineNormal(buf []byte, err error, level int) []byte {
|
||||
if level == 0 {
|
||||
return err
|
||||
return append(buf, err.Error()...)
|
||||
}
|
||||
return spaces[:2*level] + bulletPrefix + err
|
||||
buf = append(buf, spaces[:2*level]...)
|
||||
buf = append(buf, bulletPrefix...)
|
||||
buf = append(buf, err.Error()...)
|
||||
return buf
|
||||
}
|
||||
|
||||
func makeLines(errs []error, level int) []string {
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
func appendLinePlain(buf []byte, err error, level int) []byte {
|
||||
if level == 0 {
|
||||
return append(buf, Plain(err)...)
|
||||
}
|
||||
buf = append(buf, spaces[:2*level]...)
|
||||
buf = append(buf, bulletPrefix...)
|
||||
buf = append(buf, Plain(err)...)
|
||||
return buf
|
||||
}
|
||||
|
||||
func appendLineMd(buf []byte, err error, level int) []byte {
|
||||
if level == 0 {
|
||||
return append(buf, Markdown(err)...)
|
||||
}
|
||||
buf = append(buf, spaces[:2*level]...)
|
||||
buf = append(buf, markdownBulletPrefix...)
|
||||
buf = append(buf, Markdown(err)...)
|
||||
return buf
|
||||
}
|
||||
|
||||
func appendLines(buf []byte, errs []error, level int, appendLine appendLineFunc) []byte {
|
||||
if len(errs) == 0 {
|
||||
return buf
|
||||
}
|
||||
lines := make([]string, 0, len(errs))
|
||||
for _, err := range errs {
|
||||
switch err := wrap(err).(type) {
|
||||
case *nestedError:
|
||||
if err.Err != nil {
|
||||
lines = append(lines, makeLine(err.Err.Error(), level))
|
||||
lines = append(lines, makeLines(err.Extras, level+1)...)
|
||||
buf = appendLine(buf, err.Err, level)
|
||||
buf = append(buf, '\n')
|
||||
buf = appendLines(buf, err.Extras, level+1, appendLine)
|
||||
} else {
|
||||
lines = append(lines, makeLines(err.Extras, level)...)
|
||||
buf = appendLines(buf, err.Extras, level, appendLine)
|
||||
}
|
||||
default:
|
||||
lines = append(lines, makeLine(err.Error(), level))
|
||||
buf = appendLine(buf, err, level)
|
||||
buf = append(buf, '\n')
|
||||
}
|
||||
}
|
||||
return lines
|
||||
return buf
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package gperr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||
)
|
||||
|
@ -19,10 +19,23 @@ type withSubject struct {
|
|||
|
||||
const subjectSep = " > "
|
||||
|
||||
func highlight(subject string) string {
|
||||
type highlightFunc func(subject string) string
|
||||
|
||||
var _ PlainError = (*withSubject)(nil)
|
||||
var _ MarkdownError = (*withSubject)(nil)
|
||||
|
||||
func highlightANSI(subject string) string {
|
||||
return ansi.HighlightRed + subject + ansi.Reset
|
||||
}
|
||||
|
||||
func highlightMarkdown(subject string) string {
|
||||
return "**" + subject + "**"
|
||||
}
|
||||
|
||||
func noHighlight(subject string) string {
|
||||
return subject
|
||||
}
|
||||
|
||||
func PrependSubject(subject string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
|
@ -69,24 +82,38 @@ func (err *withSubject) Unwrap() error {
|
|||
}
|
||||
|
||||
func (err *withSubject) Error() string {
|
||||
return string(err.fmtError(highlightANSI))
|
||||
}
|
||||
|
||||
func (err *withSubject) Plain() []byte {
|
||||
return err.fmtError(noHighlight)
|
||||
}
|
||||
|
||||
func (err *withSubject) Markdown() []byte {
|
||||
return err.fmtError(highlightMarkdown)
|
||||
}
|
||||
|
||||
func (err *withSubject) fmtError(highlight highlightFunc) []byte {
|
||||
// subject is in reversed order
|
||||
n := len(err.Subjects)
|
||||
size := 0
|
||||
errStr := err.Err.Error()
|
||||
var sb strings.Builder
|
||||
var buf bytes.Buffer
|
||||
for _, s := range err.Subjects {
|
||||
size += len(s)
|
||||
}
|
||||
sb.Grow(size + 2 + n*len(subjectSep) + len(errStr) + len(highlight("")))
|
||||
buf.Grow(size + 2 + n*len(subjectSep) + len(errStr) + len(highlight("")))
|
||||
|
||||
for i := n - 1; i > 0; i-- {
|
||||
sb.WriteString(err.Subjects[i])
|
||||
sb.WriteString(subjectSep)
|
||||
buf.WriteString(err.Subjects[i])
|
||||
buf.WriteString(subjectSep)
|
||||
}
|
||||
sb.WriteString(highlight(err.Subjects[0]))
|
||||
sb.WriteString(": ")
|
||||
sb.WriteString(errStr)
|
||||
return sb.String()
|
||||
buf.WriteString(highlight(err.Subjects[0]))
|
||||
if errStr != "" {
|
||||
buf.WriteString(": ")
|
||||
buf.WriteString(errStr)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package gperr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
|
@ -29,16 +27,17 @@ func Wrap(err error, message ...string) Error {
|
|||
if len(message) == 0 || message[0] == "" {
|
||||
return wrap(err)
|
||||
}
|
||||
wrapped := &wrappedError{err, message[0]}
|
||||
//nolint:errorlint
|
||||
switch err := err.(type) {
|
||||
case *baseError:
|
||||
err.Err = fmt.Errorf("%s: %w", message[0], err.Err)
|
||||
err.Err = wrapped
|
||||
return err
|
||||
case *nestedError:
|
||||
err.Err = fmt.Errorf("%s: %w", message[0], err.Err)
|
||||
err.Err = wrapped
|
||||
return err
|
||||
}
|
||||
return &baseError{fmt.Errorf("%s: %w", message[0], err)}
|
||||
return &baseError{wrapped}
|
||||
}
|
||||
|
||||
func Unwrap(err error) Error {
|
||||
|
@ -65,18 +64,6 @@ func wrap(err error) Error {
|
|||
return &baseError{err}
|
||||
}
|
||||
|
||||
func IsJSONMarshallable(err error) bool {
|
||||
switch err := err.(type) {
|
||||
case *nestedError, *withSubject:
|
||||
return true
|
||||
case *baseError:
|
||||
return IsJSONMarshallable(err.Err)
|
||||
default:
|
||||
var v json.Marshaler
|
||||
return errors.As(err, &v)
|
||||
}
|
||||
}
|
||||
|
||||
func Join(errors ...error) Error {
|
||||
n := 0
|
||||
for _, err := range errors {
|
||||
|
@ -103,3 +90,27 @@ func Collect[T any, Err error, Arg any, Func func(Arg) (T, Err)](eb *Builder, fn
|
|||
eb.Add(err)
|
||||
return result
|
||||
}
|
||||
|
||||
func Plain(err error) []byte {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if p, ok := err.(PlainError); ok {
|
||||
return p.Plain()
|
||||
}
|
||||
return []byte(err.Error())
|
||||
}
|
||||
|
||||
func Markdown(err error) []byte {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
switch err := err.(type) {
|
||||
case MarkdownError:
|
||||
return err.Markdown()
|
||||
case interface{ Unwrap() []error }:
|
||||
return appendLines(nil, err.Unwrap(), 0, appendLineMd)
|
||||
default:
|
||||
return []byte(err.Error())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,63 +1,55 @@
|
|||
package gperr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testErr struct{}
|
||||
|
||||
func (e *testErr) Error() string {
|
||||
func (e testErr) Error() string {
|
||||
return "test error"
|
||||
}
|
||||
|
||||
func (e *testErr) MarshalJSON() ([]byte, error) {
|
||||
return nil, nil
|
||||
func (e testErr) Plain() []byte {
|
||||
return []byte("test error")
|
||||
}
|
||||
|
||||
func TestIsJSONMarshallable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "testErr",
|
||||
err: &testErr{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "baseError",
|
||||
err: &baseError{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "baseError with json marshallable error",
|
||||
err: &baseError{&testErr{}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "nestedError",
|
||||
err: &nestedError{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "withSubject",
|
||||
err: &withSubject{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "standard error",
|
||||
err: errors.New("test error"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
func (e testErr) Markdown() []byte {
|
||||
return []byte("**test error**")
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := IsJSONMarshallable(test.err); got != test.want {
|
||||
t.Errorf("IsJSONMarshallable(%v) = %v, want %v", test.err, got, test.want)
|
||||
}
|
||||
})
|
||||
type testMultiErr struct {
|
||||
errors []error
|
||||
}
|
||||
|
||||
func (e testMultiErr) Error() string {
|
||||
return Join(e.errors...).Error()
|
||||
}
|
||||
|
||||
func (e testMultiErr) Unwrap() []error {
|
||||
return e.errors
|
||||
}
|
||||
|
||||
func TestFormatting(t *testing.T) {
|
||||
err := testErr{}
|
||||
plain := Plain(err)
|
||||
if string(plain) != "test error" {
|
||||
t.Errorf("expected test error, got %s", string(plain))
|
||||
}
|
||||
md := Markdown(err)
|
||||
if string(md) != "**test error**" {
|
||||
t.Errorf("expected test error, got %s", string(md))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiError(t *testing.T) {
|
||||
err := testMultiErr{[]error{testErr{}, testErr{}}}
|
||||
plain := Plain(err)
|
||||
if string(plain) != "test error\ntest error" {
|
||||
t.Errorf("expected test error, got %s", string(plain))
|
||||
}
|
||||
md := Markdown(err)
|
||||
if string(md) != "**test error**\n**test error**" {
|
||||
t.Errorf("expected test error, got %s", string(md))
|
||||
}
|
||||
}
|
||||
|
|
34
internal/gperr/wrapped.go
Normal file
34
internal/gperr/wrapped.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package gperr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type wrappedError struct {
|
||||
Err error
|
||||
Message string
|
||||
}
|
||||
|
||||
var _ PlainError = (*wrappedError)(nil)
|
||||
var _ MarkdownError = (*wrappedError)(nil)
|
||||
|
||||
func (e *wrappedError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Message, e.Err.Error())
|
||||
}
|
||||
|
||||
func (e *wrappedError) Plain() []byte {
|
||||
return fmt.Appendf(nil, "%s: %s", e.Message, e.Err.Error())
|
||||
}
|
||||
|
||||
func (e *wrappedError) Markdown() []byte {
|
||||
return fmt.Appendf(nil, "**%s**: %s", e.Message, e.Err.Error())
|
||||
}
|
||||
|
||||
func (e *wrappedError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
func (e *wrappedError) Is(target error) bool {
|
||||
return errors.Is(e.Err, target)
|
||||
}
|
|
@ -2,9 +2,8 @@ package homepage
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/homepage/widgets"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
|
@ -13,20 +12,21 @@ type (
|
|||
Category []*Item
|
||||
|
||||
ItemConfig struct {
|
||||
Show bool `json:"show"`
|
||||
Name string `json:"name"` // display name
|
||||
Icon *IconURL `json:"icon"`
|
||||
Category string `json:"category"`
|
||||
Description string `json:"description" aliases:"desc"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
WidgetConfig map[string]any `json:"widget_config" aliases:"widget"`
|
||||
Show bool `json:"show"`
|
||||
Name string `json:"name"` // display name
|
||||
Icon *IconURL `json:"icon"`
|
||||
Category string `json:"category"`
|
||||
Description string `json:"description" aliases:"desc"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
Item struct {
|
||||
*ItemConfig
|
||||
WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget"`
|
||||
|
||||
Alias string
|
||||
Provider string
|
||||
Alias string
|
||||
Provider string
|
||||
OriginURL string
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -43,23 +43,10 @@ func (cfg *ItemConfig) GetOverride(alias string) *ItemConfig {
|
|||
}
|
||||
|
||||
func (item *Item) MarshalJSON() ([]byte, error) {
|
||||
var url *string
|
||||
if !strings.ContainsRune(item.Alias, '.') {
|
||||
godoxyCfg := config.GetInstance().Value()
|
||||
// use first domain as base domain
|
||||
domains := godoxyCfg.MatchDomains
|
||||
if len(domains) > 0 {
|
||||
url = new(string)
|
||||
*url = item.Alias + domains[0]
|
||||
}
|
||||
} else {
|
||||
url = &item.Alias
|
||||
}
|
||||
return json.Marshal(map[string]any{
|
||||
"show": item.Show,
|
||||
"alias": item.Alias,
|
||||
"provider": item.Provider,
|
||||
"url": url,
|
||||
"name": item.Name,
|
||||
"icon": item.Icon,
|
||||
"category": item.Category,
|
||||
|
|
68
internal/homepage/integrations/qbittorrent/client.go
Normal file
68
internal/homepage/integrations/qbittorrent/client.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/homepage/widgets"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
URL string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (c *Client) Initialize(ctx context.Context, url string, cfg map[string]any) error {
|
||||
c.URL = url
|
||||
c.Username = cfg["username"].(string)
|
||||
c.Password = cfg["password"].(string)
|
||||
|
||||
_, err := c.Version(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(ctx context.Context, method, endpoint string, query url.Values, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.URL+endpoint+query.Encode(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.Username != "" && c.Password != "" {
|
||||
req.SetBasicAuth(c.Username, c.Password)
|
||||
}
|
||||
|
||||
resp, err := widgets.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, gperr.Errorf("%w: %d %s", widgets.ErrHTTPStatus, resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func jsonRequest[T any](ctx context.Context, client *Client, endpoint string, query url.Values) (result T, err error) {
|
||||
resp, err := client.doRequest(ctx, http.MethodGet, endpoint, query, nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
102
internal/homepage/integrations/qbittorrent/logs.go
Normal file
102
internal/homepage/integrations/qbittorrent/logs.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const endpointLogs = "/api/v2/log/main"
|
||||
|
||||
type LogEntry struct {
|
||||
ID int `json:"id"`
|
||||
Timestamp int `json:"timestamp"`
|
||||
Type int `json:"type"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
const (
|
||||
LogSeverityNormal = 1 << iota
|
||||
LogSeverityInfo
|
||||
LogSeverityWarning
|
||||
LogSeverityCritical
|
||||
)
|
||||
|
||||
func (l *LogEntry) Time() time.Time {
|
||||
return time.Unix(int64(l.Timestamp), 0)
|
||||
}
|
||||
|
||||
func (l *LogEntry) Level() string {
|
||||
switch l.Type {
|
||||
case LogSeverityNormal:
|
||||
return "Normal"
|
||||
case LogSeverityInfo:
|
||||
return "Info"
|
||||
case LogSeverityWarning:
|
||||
return "Warning"
|
||||
case LogSeverityCritical:
|
||||
return "Critical"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LogEntry) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"id": l.ID,
|
||||
"timestamp": l.Timestamp,
|
||||
"level": l.Level(),
|
||||
"message": l.Message,
|
||||
})
|
||||
}
|
||||
|
||||
// params:
|
||||
//
|
||||
// normal: bool
|
||||
// info: bool
|
||||
// warning: bool
|
||||
// critical: bool
|
||||
// last_known_id: int
|
||||
func (c *Client) GetLogs(ctx context.Context, lastKnownID int) ([]*LogEntry, error) {
|
||||
return jsonRequest[[]*LogEntry](ctx, c, endpointLogs, url.Values{
|
||||
"last_known_id": {strconv.Itoa(lastKnownID)},
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) WatchLogs(ctx context.Context) (<-chan *LogEntry, <-chan error) {
|
||||
ch := make(chan *LogEntry)
|
||||
errCh := make(chan error)
|
||||
|
||||
lastKnownID := -1
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
defer close(errCh)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
logs, err := c.GetLogs(ctx, lastKnownID)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
|
||||
if len(logs) == 0 {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, log := range logs {
|
||||
ch <- log
|
||||
}
|
||||
lastKnownID = logs[len(logs)-1].ID
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, errCh
|
||||
}
|
32
internal/homepage/integrations/qbittorrent/transfer_info.go
Normal file
32
internal/homepage/integrations/qbittorrent/transfer_info.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/homepage/widgets"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
const endpointTransferInfo = "/api/v2/transfer/info"
|
||||
|
||||
type TransferInfo struct {
|
||||
ConnectionStatus string `json:"connection_status"`
|
||||
SessionDownloads uint64 `json:"dl_info_data"`
|
||||
SessionUploads uint64 `json:"up_info_data"`
|
||||
DownloadSpeed uint64 `json:"dl_info_speed"`
|
||||
UploadSpeed uint64 `json:"up_info_speed"`
|
||||
}
|
||||
|
||||
func (c *Client) Data(ctx context.Context) ([]widgets.NameValue, error) {
|
||||
info, err := jsonRequest[TransferInfo](ctx, c, endpointTransferInfo, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []widgets.NameValue{
|
||||
{Name: "Status", Value: info.ConnectionStatus},
|
||||
{Name: "Download", Value: strutils.FormatByteSize(info.SessionDownloads)},
|
||||
{Name: "Upload", Value: strutils.FormatByteSize(info.SessionUploads)},
|
||||
{Name: "Download Speed", Value: strutils.FormatByteSize(info.DownloadSpeed) + "/s"},
|
||||
{Name: "Upload Speed", Value: strutils.FormatByteSize(info.UploadSpeed) + "/s"},
|
||||
}, nil
|
||||
}
|
21
internal/homepage/integrations/qbittorrent/version.go
Normal file
21
internal/homepage/integrations/qbittorrent/version.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
func (c *Client) Version(ctx context.Context) (string, error) {
|
||||
resp, err := c.doRequest(ctx, "GET", "/api/v2/app/version", nil, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
14
internal/homepage/widgets/http.go
Normal file
14
internal/homepage/widgets/http.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package widgets
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
)
|
||||
|
||||
var HTTPClient = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
var ErrHTTPStatus = gperr.New("http status")
|
49
internal/homepage/widgets/widgets.go
Normal file
49
internal/homepage/widgets/widgets.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package widgets
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
Provider string `json:"provider"`
|
||||
Config Widget `json:"config"`
|
||||
}
|
||||
Widget interface {
|
||||
Initialize(ctx context.Context, url string, cfg map[string]any) error
|
||||
Data(ctx context.Context) ([]NameValue, error)
|
||||
}
|
||||
NameValue struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
WidgetProviderQbittorrent = "qbittorrent"
|
||||
)
|
||||
|
||||
var widgetProviders = map[string]struct{}{
|
||||
WidgetProviderQbittorrent: {},
|
||||
}
|
||||
|
||||
var ErrInvalidProvider = gperr.New("invalid provider")
|
||||
|
||||
func (cfg *Config) UnmarshalMap(m map[string]any) error {
|
||||
cfg.Provider = m["provider"].(string)
|
||||
if _, ok := widgetProviders[cfg.Provider]; !ok {
|
||||
return ErrInvalidProvider.Subject(cfg.Provider)
|
||||
}
|
||||
delete(m, "provider")
|
||||
m, ok := m["config"].(map[string]any)
|
||||
if !ok {
|
||||
return gperr.New("invalid config")
|
||||
}
|
||||
if err := utils.MapUnmarshalValidate(m, &cfg.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -10,6 +10,15 @@ type (
|
|||
AcceptContentType []ContentType
|
||||
)
|
||||
|
||||
const (
|
||||
ContentTypeJSON = ContentType("application/json")
|
||||
ContentTypeTextPlain = ContentType("text/plain")
|
||||
ContentTypeTextHTML = ContentType("text/html")
|
||||
ContentTypeTextMarkdown = ContentType("text/markdown")
|
||||
ContentTypeTextXML = ContentType("text/xml")
|
||||
ContentTypeXHTML = ContentType("application/xhtml+xml")
|
||||
)
|
||||
|
||||
func GetContentType(h http.Header) ContentType {
|
||||
ct := h.Get("Content-Type")
|
||||
if ct == "" {
|
||||
|
@ -35,15 +44,15 @@ func GetAccept(h http.Header) AcceptContentType {
|
|||
}
|
||||
|
||||
func (ct ContentType) IsHTML() bool {
|
||||
return ct == "text/html" || ct == "application/xhtml+xml"
|
||||
return ct == ContentTypeTextHTML || ct == ContentTypeXHTML
|
||||
}
|
||||
|
||||
func (ct ContentType) IsJSON() bool {
|
||||
return ct == "application/json"
|
||||
return ct == ContentTypeJSON
|
||||
}
|
||||
|
||||
func (ct ContentType) IsPlainText() bool {
|
||||
return ct == "text/plain"
|
||||
return ct == ContentTypeTextPlain
|
||||
}
|
||||
|
||||
func (act AcceptContentType) IsEmpty() bool {
|
||||
|
@ -68,6 +77,15 @@ func (act AcceptContentType) AcceptJSON() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (act AcceptContentType) AcceptMarkdown() bool {
|
||||
for _, v := range act {
|
||||
if v == ContentTypeTextMarkdown || v == "*/*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (act AcceptContentType) AcceptPlainText() bool {
|
||||
for _, v := range act {
|
||||
if v.IsPlainText() || v == "text/*" || v == "*/*" {
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"syscall"
|
||||
|
||||
|
@ -40,15 +41,22 @@ func ServerError(w http.ResponseWriter, r *http.Request, err error, code ...int)
|
|||
//
|
||||
// For JSON marshallable errors (e.g. gperr.Error), it returns the error details as JSON.
|
||||
// Otherwise, it returns the error details as plain text.
|
||||
func ClientError(w http.ResponseWriter, err error, code ...int) {
|
||||
func ClientError(w http.ResponseWriter, r *http.Request, err error, code ...int) {
|
||||
if len(code) == 0 {
|
||||
code = []int{http.StatusBadRequest}
|
||||
}
|
||||
if gperr.IsJSONMarshallable(err) {
|
||||
w.WriteHeader(code[0])
|
||||
accept := GetAccept(r.Header)
|
||||
switch {
|
||||
case accept.AcceptJSON():
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(err)
|
||||
} else {
|
||||
http.Error(w, err.Error(), code[0])
|
||||
case accept.AcceptMarkdown():
|
||||
w.Header().Set("Content-Type", "text/markdown")
|
||||
w.Write(gperr.Markdown(err))
|
||||
default:
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(gperr.Plain(err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,18 +91,18 @@ func NotFound(w http.ResponseWriter, err string) {
|
|||
BadRequest(w, err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func ErrMissingKey(k string) error {
|
||||
return gperr.New(k + " is required")
|
||||
func MissingKey(w http.ResponseWriter, k string) {
|
||||
BadRequest(w, k+" is required", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func ErrInvalidKey(k string) error {
|
||||
return gperr.New(k + " is invalid")
|
||||
func InvalidKey(w http.ResponseWriter, k string) {
|
||||
BadRequest(w, k+" is invalid", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func ErrAlreadyExists(k, v string) error {
|
||||
return gperr.Errorf("%s %q already exists", k, v)
|
||||
func KeyAlreadyExists(w http.ResponseWriter, k, v string) {
|
||||
BadRequest(w, fmt.Sprintf("%s %q already exists", k, v), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func ErrNotFound(k, v string) error {
|
||||
return gperr.Errorf("%s %q not found", k, v)
|
||||
func ValueNotFound(w http.ResponseWriter, k, v string) {
|
||||
BadRequest(w, fmt.Sprintf("%s %q not found", k, v), http.StatusNotFound)
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ func Get(name string) (*Middleware, Error) {
|
|||
if !ok {
|
||||
return nil, ErrUnknownMiddleware.
|
||||
Subject(name).
|
||||
Withf(strutils.DoYouMean(utils.NearestField(name, allMiddlewares)))
|
||||
With(gperr.DoYouMean(utils.NearestField(name, allMiddlewares)))
|
||||
}
|
||||
return middleware, nil
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@ type (
|
|||
FieldsBody []LogField
|
||||
ListBody []string
|
||||
MessageBody string
|
||||
ErrorBody struct {
|
||||
Error error
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -113,3 +116,15 @@ func (m MessageBody) Format(format *LogFormat) ([]byte, error) {
|
|||
}
|
||||
return nil, fmt.Errorf("unknown format: %v", format)
|
||||
}
|
||||
|
||||
func (e ErrorBody) Format(format *LogFormat) ([]byte, error) {
|
||||
switch format {
|
||||
case LogFormatRawJSON:
|
||||
return json.Marshal(e)
|
||||
case LogFormatPlain:
|
||||
return gperr.Plain(e.Error), nil
|
||||
case LogFormatMarkdown:
|
||||
return gperr.Markdown(e.Error), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown format: %v", format)
|
||||
}
|
||||
|
|
|
@ -259,7 +259,7 @@ func mapUnmarshalValidate(src SerializedObject, dst any, checkValidateTag bool)
|
|||
errs.Add(err)
|
||||
}
|
||||
} else {
|
||||
errs.Add(ErrUnknownField.Subject(k).Withf(strutils.DoYouMean(NearestField(k, mapping))))
|
||||
errs.Add(ErrUnknownField.Subject(k).With(gperr.DoYouMean(NearestField(k, mapping))))
|
||||
}
|
||||
}
|
||||
if hasValidateTag && checkValidateTag {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package ansi
|
||||
|
||||
import "regexp"
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var ansiRegexp = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||
|
||||
|
@ -43,11 +45,3 @@ func WithANSI(s string, ansi string) string {
|
|||
func StripANSI(s string) string {
|
||||
return ansiRegexp.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
var ToHTMLClass = map[string]string{
|
||||
"[91": "log-red",
|
||||
"[92": "log-green",
|
||||
"[93": "log-yellow",
|
||||
"[96": "log-cyan",
|
||||
"[97": "log-white",
|
||||
}
|
||||
|
|
|
@ -5,8 +5,6 @@ import (
|
|||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||
)
|
||||
|
||||
// AppendDuration appends a duration to a buffer with the following format:
|
||||
|
@ -215,13 +213,6 @@ func AppendByteSize[T ~int | ~uint | ~int64 | ~uint64 | ~float64](size T, buf []
|
|||
return buf
|
||||
}
|
||||
|
||||
func DoYouMean(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
return "Did you mean " + ansi.Info(s) + "?"
|
||||
}
|
||||
|
||||
func Pluralize(n int64) string {
|
||||
if n > 1 {
|
||||
return "s"
|
||||
|
|
Loading…
Add table
Reference in a new issue