feat: add generic i18n and validator

main
git 2024-12-28 20:01:56 +08:00
parent 7dad55d2d5
commit b67467c0b4
Signed by: git
GPG Key ID: 3F65EFFA44207ADD
11 changed files with 611 additions and 19 deletions

View File

@ -1,4 +1,4 @@
{
"go.goroot": "/opt/gvm/gos/go1.20",
"go.gopath": "/opt/gvm/pkgsets/go1.20/global"
}
"go.goroot": "/opt/go/sdk/go",
"go.gopath": "/root/.go"
}

169
apis/generic.go Normal file
View File

@ -0,0 +1,169 @@
package apis
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"time"
"github.com/ggicci/httpin"
httpin_integration "github.com/ggicci/httpin/integration"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"git.ifooth.com/common/pkg/i18n"
)
// UnaryFunc Unary or ClientStreaming handle function
type UnaryFunc[In, Out any] func(context.Context, *In) (*Out, error)
// StreamingServer server or bidi streaming server
type StreamingServer interface {
http.ResponseWriter
Context() context.Context
}
// StreamFunc ServerStreaming or BidiStreaming handle function
type StreamFunc[In any] func(*In, StreamingServer) error
// Handle Composable HTTP Handlers using generics
func Handle[In any, Out any](fn UnaryFunc[In, Out]) func(w http.ResponseWriter, r *http.Request) {
handleName := getHandleName(fn)
f := func(w http.ResponseWriter, r *http.Request) {
r.Header.Set("Content-Type", "application/json")
st := time.Now()
var err error
defer func() {
collectHandleMetrics(handleName, r.Method, st, err)
}()
in, err := decodeReq[In](r)
if err != nil {
slog.Error("handle decode request failed", "err", err)
_ = render.Render(w, r, APIError(err))
return
}
// 设置语言
ctx := i18n.SetLang(r.Context(), r.Header.Get("Accept-Language"))
ctx = context.WithValue(ctx, reqCtxKey, r)
out, err := fn(ctx, in)
if err != nil {
_ = render.Render(w, r, APIError(err))
return
}
_ = render.Render(w, r, APIOK(out))
}
return f
}
type streamingServer struct {
http.ResponseWriter
*http.ResponseController
ctx context.Context
}
// Context return svr's context
func (s *streamingServer) Context() context.Context {
return s.ctx
}
// Stream Composable HTTP Handlers using generics
func Stream[In any](fn StreamFunc[In]) func(w http.ResponseWriter, r *http.Request) {
handleName := getHandleName(fn)
f := func(w http.ResponseWriter, r *http.Request) {
st := time.Now()
var err error
defer func() {
collectHandleMetrics(handleName, r.Method, st, err)
}()
in, err := decodeReq[In](r)
if err != nil {
slog.Error("handle decode request failed ", "err", err)
_ = render.Render(w, r, APIError(err))
return
}
// 设置语言
ctx := i18n.SetLang(r.Context(), r.Header.Get("Accept-Language"))
ctx = context.WithValue(ctx, reqCtxKey, r)
svr := &streamingServer{
ResponseWriter: w,
ResponseController: http.NewResponseController(w),
ctx: ctx,
}
err = fn(in, svr)
if err != nil {
_ = render.Render(w, r, APIError(err))
}
}
return f
}
// decodeReq ...
func decodeReq[T any](r *http.Request) (*T, error) {
in := new(T)
var err error
// http.Request 直接返回
if _, ok := any(in).(*http.Request); ok {
return any(r).(*T), nil
}
// 空值不需要反序列化
if _, ok := any(in).(*EmptyReq); ok {
return in, nil
}
in, err = httpin.Decode[T](r)
if err != nil {
return nil, err
}
// Get/Delete 请求, 请求参数从url中获取
if r.Method == http.MethodGet || r.Method == http.MethodDelete {
return in, nil
}
// Post 请求等, 从body中获取
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
if err = json.Unmarshal(body, in); err != nil {
return nil, fmt.Errorf("unmarshal json body: %s", err)
}
return in, nil
}
// EmptyReq 空的请求
type EmptyReq struct{}
// EmptyResp 空的返回
type EmptyResp struct{}
// PaginationReq 分页接口通用请求
type PaginationReq struct {
Offset int `json:"offset" in:"query=offset" validate:"gte=0"`
Limit int `json:"limit" in:"query=limit" validate:"gte=0"`
}
// PaginationResp 分页接口通用返回
type PaginationResp[T any] struct {
Count int64 `json:"count"`
Items []*T `json:"items"`
}
func init() {
httpin_integration.UseGochiURLParam("path", chi.URLParam)
}

103
apis/generic_test.go Normal file
View File

@ -0,0 +1,103 @@
package apis
import (
"bytes"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// NewMockRequest creates a new mock request.
func NewMockRequest(t *testing.T, method string, body io.ReadCloser) *http.Request {
req, err := http.NewRequest(method, "/vm/xxx?name=alice", body)
require.NoError(t, err)
return req
}
// TestDecodeReq tests the decodeReq function.
func TestDecodeReq(t *testing.T) {
// Define a test struct
type TestStruct struct {
Field string `json:"field"`
EnvID string `json:"env_id" in:"path=env_id" validate:"required"`
Name string `json:"name" in:"query=name"`
}
// Test case 1: GET request with no body
t.Run("GET request with no body", func(t *testing.T) {
req := NewMockRequest(t, http.MethodGet, nil)
result, err := decodeReq[TestStruct](req)
assert.NoError(t, err)
assert.Equal(t, "alice", result.Name)
})
// Test case 2: POST request with valid JSON body
t.Run("POST request with valid JSON body", func(t *testing.T) {
jsonBody := `{"field": "value", "env_id": "test", "name": "alice1"}`
body := io.NopCloser(bytes.NewBufferString(jsonBody))
req := NewMockRequest(t, http.MethodPost, body)
result, err := decodeReq[TestStruct](req)
assert.NoError(t, err)
assert.Equal(t, "value", result.Field)
assert.Equal(t, "test", result.EnvID)
assert.Equal(t, "alice1", result.Name)
})
// Test case 3: POST request with invalid JSON body
t.Run("POST request with invalid JSON body", func(t *testing.T) {
jsonBody := `{"field": "value"` // Invalid JSON
body := io.NopCloser(bytes.NewBufferString(jsonBody))
req := NewMockRequest(t, http.MethodPost, body)
result, err := decodeReq[TestStruct](req)
assert.Error(t, err)
assert.Nil(t, result)
})
// Test case 4: Invalid request header
t.Run("invalid request header", func(t *testing.T) {
jsonBody := `{"field": "value", "env_id": "test", "name": "alice1"}`
body := io.NopCloser(bytes.NewBufferString(jsonBody))
req := NewMockRequest(t, http.MethodPost, body)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
result, err := decodeReq[TestStruct](req)
assert.Error(t, err)
assert.Nil(t, result)
})
// Test case 5: EmptyReq type check
t.Run("EmptyReq type check", func(t *testing.T) {
req := NewMockRequest(t, http.MethodPost, nil)
result, err := decodeReq[EmptyReq](req)
assert.NoError(t, err)
assert.NotNil(t, result)
})
// Test case 5: EmptyReq type check
t.Run("HttpRequest type check", func(t *testing.T) {
req := &http.Request{
Method: http.MethodPost,
}
result, err := decodeReq[http.Request](req)
assert.NoError(t, err)
assert.Equal(t, req.Method, result.Method)
})
}
func BenchmarkDecodeReq(b *testing.B) {
r := &http.Request{
Method: http.MethodPost,
}
for i := 0; i < b.N; i++ {
result, err := decodeReq[http.Request](r)
if err != nil {
b.Error(err)
}
if result.Method != http.MethodPost {
b.Error("invalid result")
}
}
}

60
apis/metrics.go Normal file
View File

@ -0,0 +1,60 @@
package apis
import (
"reflect"
"runtime"
"strconv"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
)
var (
requestCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Number of get requests.",
},
[]string{"handler", "method", "code"},
)
responseTimeDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Histogram of response time for HTTP requests.",
Buckets: []float64{0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60},
},
[]string{"handler", "method", "code"},
)
)
// getHandleName 获取FuncHandle/StreamHandle函数名
func getHandleName(fn any) string {
fullName := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
if fullName == "" {
panic("get func name is empty")
}
parts := strings.Split(fullName, ".")
lastPart := parts[len(parts)-1]
name := strings.TrimSuffix(lastPart, "-fm")
return name
}
// collectHandleMetrics api指标数据
func collectHandleMetrics(funcName, method string, st time.Time, err error) {
code := 200
if err != nil {
code = APIError(err).(*APIResponse).HTTPStatusCode
}
codeStr := strconv.Itoa(code)
requestCounter.WithLabelValues(funcName, method, codeStr).Inc()
duration := time.Since(st).Seconds()
responseTimeDuration.WithLabelValues(funcName, method, codeStr).Observe(duration)
}
func init() {
prometheus.MustRegister(requestCounter)
prometheus.MustRegister(responseTimeDuration)
}

View File

@ -13,6 +13,14 @@ import (
"git.ifooth.com/common/pkg/components"
)
type contextKey struct {
name string
}
var (
reqCtxKey = &contextKey{"HTTPRequest"}
)
// RequestIdGenerator request_id
func RequestIdGenerator() string {
uid := uuid.New().String()

View File

@ -1,4 +1,4 @@
package rest
package apis
import (
"net/http"
@ -36,8 +36,8 @@ func (h RestHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
// Result 返回的标准结构
type Response struct {
// APIResponse 返回的标准结构
type APIResponse struct {
Err error `json:"-"` // low-level runtime error
HTTPStatusCode int `json:"-"` // http response status code
Code int `json:"code"`
@ -47,7 +47,7 @@ type Response struct {
}
// Render chi Render 实现
func (res *Response) Render(w http.ResponseWriter, r *http.Request) error {
func (res *APIResponse) Render(w http.ResponseWriter, r *http.Request) error {
statusCode := res.HTTPStatusCode
if statusCode == 0 {
statusCode = http.StatusOK
@ -61,7 +61,7 @@ func (res *Response) Render(w http.ResponseWriter, r *http.Request) error {
// UnauthorizedErrRender 未登入返回
func AbortWithUnauthorizedError(err error) render.Renderer {
return &Response{
return &APIResponse{
Code: 1401,
Message: err.Error(),
HTTPStatusCode: http.StatusUnauthorized,
@ -70,7 +70,17 @@ func AbortWithUnauthorizedError(err error) render.Renderer {
// AbortWithBadRequestError 错误
func AbortWithBadRequestError(err error) render.Renderer {
return &Response{
return &APIResponse{
Code: 1400,
Message: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
}
// AbortWithBadRequestError 错误
func APIError(err error) render.Renderer {
return &APIResponse{
Code: 1400,
Message: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
@ -80,7 +90,7 @@ func AbortWithBadRequestError(err error) render.Renderer {
// AbortWithWithForbiddenError 没有权限
func AbortWithWithForbiddenError(err error) render.Renderer {
return &Response{
return &APIResponse{
Code: 1403,
Message: err.Error(),
HTTPStatusCode: http.StatusForbidden,
@ -88,6 +98,6 @@ func AbortWithWithForbiddenError(err error) render.Renderer {
}
// OKRender 正常返回
func OKRender(data interface{}) render.Renderer {
return &Response{Data: data}
func APIOK(data interface{}) render.Renderer {
return &APIResponse{Data: data}
}

25
go.mod
View File

@ -1,18 +1,24 @@
module git.ifooth.com/common/pkg
go 1.22.0
go 1.21
require (
github.com/dustin/go-humanize v1.0.1
github.com/felixge/fgprof v0.9.3
github.com/go-chi/chi/v5 v5.0.8
github.com/ggicci/httpin v0.19.0
github.com/go-chi/chi/v5 v5.0.11
github.com/go-chi/render v1.0.2
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.23.0
github.com/go-resty/resty/v2 v2.7.0
github.com/google/uuid v1.6.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.14.0
github.com/redis/go-redis/v9 v9.0.3
github.com/samber/lo v1.47.0
github.com/stretchr/testify v1.9.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0
go.opentelemetry.io/otel v1.27.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0
@ -27,23 +33,36 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/ggicci/owl v0.8.2 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/labstack/echo/v4 v4.12.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.opentelemetry.io/otel/metric v1.27.0 // indirect
go.opentelemetry.io/otel/trace v1.27.0 // indirect
go.opentelemetry.io/proto/otlp v1.2.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

55
go.sum
View File

@ -24,8 +24,14 @@ github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/ggicci/httpin v0.19.0 h1:p0B3SWLVgg770VirYiHB14M5wdRx3zR8mCTzM/TkTQ8=
github.com/ggicci/httpin v0.19.0/go.mod h1:hzsQHcbqLabmGOycf7WNw6AAzcVbsMeoOp46bWAbIWc=
github.com/ggicci/owl v0.8.2 h1:og+lhqpzSMPDdEB+NJfzoAJARP7qCG3f8uUC3xvGukA=
github.com/ggicci/owl v0.8.2/go.mod h1:PHRD57u41vFN5UtFz2SF79yTVoM3HlWpjMiE+ZU2dj4=
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg=
github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@ -34,6 +40,14 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -46,9 +60,28 @@ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
@ -67,12 +100,20 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k=
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
@ -91,6 +132,8 @@ go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IO
go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
@ -98,12 +141,14 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd h1:sLpv7bNL1AsX3fdnWh9WVh7ejIzXdOc1RRHGeAmeStU=
google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
@ -112,6 +157,8 @@ google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLp
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

17
i18n/i18n.go Normal file
View File

@ -0,0 +1,17 @@
// Package i18n is used to localize for different languages
package i18n
import "context"
type acceptLangKey struct{}
// SetLang 设定语言
func SetLang(ctx context.Context, val string) context.Context {
return context.WithValue(ctx, acceptLangKey{}, val)
}
// GetLang 获取语言
func GetLang(ctx context.Context) (string, bool) {
value, ok := ctx.Value(acceptLangKey{}).(string)
return value, ok
}

98
validator/validator.go Normal file
View File

@ -0,0 +1,98 @@
// Package validator use for valiate struct
package validator
import (
"context"
"reflect"
"strings"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"github.com/samber/lo"
"git.ifooth.com/common/pkg/i18n"
)
var (
validate *validator.Validate
uni *ut.UniversalTranslator
defaultTrans ut.Translator
zhTrans ut.Translator
)
// ValidationError 校验错误
type ValidationError struct {
ctx context.Context
rawErr error
}
// Validator 实现了 Validate 接口自定义调用
type Validator interface {
Validate() error
}
func (e *ValidationError) getTranslator() ut.Translator {
acceptLang, ok := i18n.GetLang(e.ctx)
if ok && acceptLang == "zh" {
return zhTrans
}
return defaultTrans
}
// Error error iface
func (e *ValidationError) Error() string {
if _, ok := e.rawErr.(*validator.InvalidValidationError); ok {
return e.rawErr.Error()
}
errs := e.rawErr.(validator.ValidationErrors)
// 只返回单个错误
for _, ve := range errs {
return ve.Translate(e.getTranslator())
}
return e.rawErr.Error()
}
// Struct 通过 validate tag 校验结构体, Validate 校验需要传入指针类型
func Struct(ctx context.Context, s any) error {
err := validate.StructCtx(ctx, s)
if err != nil {
return &ValidationError{ctx: ctx, rawErr: err}
}
// 实现了 Validate 接口自定义调用
if v, ok := s.(Validator); ok {
return v.Validate()
}
return nil
}
// tagNameFunc 优先从 json tag 获取名称
func tagNameFunc(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
}
func init() {
validate = validator.New(validator.WithRequiredStructEnabled())
validate.RegisterTagNameFunc(tagNameFunc)
// 默认使用英文
en := en.New()
zh := zh.New()
uni = ut.New(en, en, zh)
defaultTrans, _ = uni.GetTranslator("en")
lo.Must0(en_translations.RegisterDefaultTranslations(validate, defaultTrans))
zhTrans, _ = uni.GetTranslator("zh")
lo.Must0(zh_translations.RegisterDefaultTranslations(validate, zhTrans))
}

View File

@ -0,0 +1,61 @@
package validator
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type testStruct struct {
EnvID string `json:"uid" validate:"required"`
Force bool `json:"-" validate:"required"`
Operator string `validate:"required"`
}
type testStruct2 struct {
EnvID string `json:"uid" validate:"required"`
Force bool `json:"-"`
Operator string `validate:"required"`
}
type testStruct3 struct {
Name string `json:"name" validate:"required,gt=1"`
Count int `json:"count" validate:"required,gt=0"`
}
func (s *testStruct2) Validate() error {
return fmt.Errorf("hit validate")
}
func TestValidate(t *testing.T) {
d := testStruct{}
err := Struct(context.Background(), d)
assert.Equal(t, err.Error(), "uid is a required field")
t2 := &testStruct2{
EnvID: "abc",
Force: false,
Operator: "ab",
}
err = Struct(context.Background(), t2)
if assert.Error(t, err) {
assert.Equal(t, err.Error(), "hit validate")
}
t3 := testStruct3{
Name: "dd_dabc",
Count: 1,
}
err = Struct(context.Background(), t3)
assert.NoError(t, err)
name := "testValidate"
err = Struct(context.Background(), name)
assert.Equal(t, err.Error(), "validator: (nil string)")
// assert.Equal(t, err.Error(), "Force is required")
// assert.Equal(t, err.Error(), "Operator is required")
}