Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions cmd/protoc-gen-go-errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import (
)

const (
errorsPackage = protogen.GoImportPath("github.com/go-kratos/kratos/v2/errors")
fmtPackage = protogen.GoImportPath("fmt")
errorsPackage = protogen.GoImportPath("github.com/go-kratos/kratos/v2/errors")
fmtPackage = protogen.GoImportPath("fmt")
contextPackage = protogen.GoImportPath("context")
)

var enCases = cases.Title(language.AmericanEnglish, cases.NoLower)
Expand All @@ -32,6 +33,7 @@ func generateFile(gen *protogen.Plugin, file *protogen.File) *protogen.Generated
g.P()
g.P("package ", file.GoPackageName)
g.P()
g.QualifiedGoIdent(contextPackage.Ident(""))
g.QualifiedGoIdent(fmtPackage.Ident(""))
generateFileContent(gen, file, g)
return g
Expand Down
7 changes: 7 additions & 0 deletions cmd/protoc-gen-go-errors/errorsTemplate.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@ func Error{{ .CamelValue }}(format string, args ...interface{}) *errors.Error {
return errors.New({{ .HTTPCode }}, {{ .Name }}_{{ .Value }}.String(), fmt.Sprintf(format, args...))
}

func Error{{ .CamelValue }}WithContext(ctx context.Context, args ...interface{}) *errors.Error {
if len(args) == 0 {
return errors.NewWithContext(ctx, {{ .HTTPCode }}, {{ .Name }}_{{ .Value }}.String(), "")
}
return errors.NewWithContext(ctx, {{ .HTTPCode }}, {{ .Name }}_{{ .Value }}.String(), args[0])
}

{{- end }}
11 changes: 11 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package errors

import (
"context"
"errors"
"fmt"

Expand Down Expand Up @@ -75,6 +76,16 @@ func New(code int, reason, message string) *Error {
}
}

// NewWithContext Use context to create error objects and support i18n localization
func NewWithContext(ctx context.Context, code int, reason string, data any) *Error {
message := ""
// If the global i18n manager is registered, it is used to localize the error message
if globalI18n != nil {
message = globalI18n.Localize(ctx, reason, data)
}
return New(code, reason, message)
}

// Newf New(code fmt.Sprintf(format, a...))
func Newf(code int, reason, format string, a ...any) *Error {
return New(code, reason, fmt.Sprintf(format, a...))
Expand Down
104 changes: 104 additions & 0 deletions errors/errors_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package errors

import (
"context"
"errors"
"fmt"
"net/http"
"reflect"
"strings"
"testing"
text "text/template"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand Down Expand Up @@ -147,3 +150,104 @@ func TestOther(t *testing.T) {
t.Errorf("Clone(nil) = %v, want %v", Clone(err400), err400)
}
}

// MockI18n is a mock implementation of the I18n interface
type MockI18n struct {
localizeFunc func(ctx context.Context, reason string, data any) string
}

// Localize mocks the Localize method of the I18n interface
func (m *MockI18n) Localize(ctx context.Context, reason string, data any) string {
return m.localizeFunc(ctx, reason, data)
}

func textFormatter(format string, data any) (s string, err error) {
t, err := text.New("text/template").Parse(format)
if err != nil {
return "", nil
}
tmpl := text.Must(t, err)
resultIoWriter := new(strings.Builder)

if err = tmpl.Execute(resultIoWriter, data); err != nil {
return "", err
}
return resultIoWriter.String(), nil
}

// TestNewWithContext_GlobalI18nIsNil tests the NewWithContext function when globalI18n is nil.
// It verifies that the returned Error object has an empty Message field.
func TestNewWithContext_GlobalI18nIsNil(t *testing.T) {
// Arrange
ctx := context.Background()
code := 404
reason := "not_found"
data := map[string]string{"resource": "user"}

// Act
err := NewWithContext(ctx, code, reason, data)

// Assert
if !errors.Is(err, New(code, reason, "")) {
t.Errorf("Expected error to be %v, but got %v", New(code, reason, ""), err)
}
if err.Message != "" {
t.Errorf("Expected error Message to be empty, but got %v", err.Message)
}
}

type langContextKey struct{}

func withLang(ctx context.Context, lang string) context.Context {
return context.WithValue(ctx, langContextKey{}, lang)
}

func getLang(ctx context.Context) string {
lang, _ := ctx.Value(langContextKey{}).(string)
return lang
}

// TestNewWithContext_GlobalI18nIsNotNil tests the NewWithContext function when globalI18n is not nil.
// It verifies that the returned Error object has a localized Message field.
func TestNewWithContext_GlobalI18nIsNotNil(t *testing.T) {
// Arrange
ctx := context.Background()
code := 404
reason := "not_found"
data := map[string]string{"resource": "Kratos"}
lang := map[string]string{
"en_CH": "{{ .resource }} is the best",
"zh_CN": "{{ .resource }}是最好的",
}

// Mock the globalI18n
mockI18n := &MockI18n{
localizeFunc: func(ctx context.Context, reason string, data any) string {
message := lang[getLang(ctx)]
s, err := textFormatter(message, data)
if err != nil {
return ""
}
return s
},
}
RegisterI18nManager(mockI18n)

// Act
err := NewWithContext(withLang(ctx, "en_CH"), code, reason, data)
// Assert
if !errors.Is(err, New(code, reason, "")) {
t.Errorf("Expected error to be %v, but got %v", New(code, reason, ""), err)
}
if err.Message != "Kratos is the best" {
t.Errorf("Expected error message to be %v, but got %v", "Kratos is the best", err.Message)
}

err = NewWithContext(withLang(ctx, "zh_CN"), code, reason, data)
if !errors.Is(err, New(code, reason, "")) {
t.Errorf("Expected error to be %v, but got %v", New(code, reason, ""), err)
}
if err.Message != "Kratos是最好的" {
t.Errorf("Expected error message to be %v, but got %v", "Kratos是最好的", err.Message)
}
}
23 changes: 23 additions & 0 deletions errors/i18n.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package errors

import (
"context"
"sync"
)

// I18nMessage An interface to internationalize error messages is defined
type I18nMessage interface {
// Localize Localization of error causes based on context and data
Localize(ctx context.Context, reason string, data any) string
}

// The global i18n manager
var globalI18n I18nMessage
var globalI18nOnce sync.Once

// RegisterI18nManager Register the global i18n manager
func RegisterI18nManager(i18n I18nMessage) {
globalI18nOnce.Do(func() {
globalI18n = i18n
})
}