diff --git a/README.md b/README.md index 978d707..e15c7b2 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,22 @@ ## Getting Started +使用 go-coco cli 构建一个新项目: +```bash +# 安装 go-coco cli +go install github.com/iftechio/go-coco@latest + +# 创建项目目录 +mkdir +cd + +# 确保 `go-coco` 在包含 `go.mod` 的目录下运行 +go mod init + +go-coco init +``` + +Notes: 1. 使用 `app.Manager` 组织需要启动的微服务应用 2. 使用 `config` 解析来自 toml 和 env 的应用变量配置 3. 使用 `infra` 快速构建基础层组件 diff --git a/app/server/xgrpc/gateway.go b/app/server/xgrpc/gateway.go new file mode 100644 index 0000000..fd28ba1 --- /dev/null +++ b/app/server/xgrpc/gateway.go @@ -0,0 +1,143 @@ +package xgrpc + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + xLogger "github.com/iftechio/go-coco/utils/logger" + "github.com/labstack/echo/v4" + "github.com/pkg/errors" + "google.golang.org/grpc" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/runtime/protoimpl" + "google.golang.org/protobuf/types/known/structpb" +) + +type CustomError struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Toast *string `protobuf:"bytes,2,opt,name=toast,proto3,oneof" json:"toast,omitempty"` + // E Code + Code *string `protobuf:"bytes,3,opt,name=code,proto3,oneof" json:"code,omitempty"` + Error *string `protobuf:"bytes,4,opt,name=error,proto3,oneof" json:"error,omitempty"` + Data *structpb.Value `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"` +} + +func GatewayMiddleware( + register func( + ctx context.Context, + mux *runtime.ServeMux, + endpoint string, + opts []grpc.DialOption, + ) (err error), + endpoint string, + pattern string, +) echo.MiddlewareFunc { + gateway := runtime.NewServeMux( + runtime.WithIncomingHeaderMatcher(func(s string) (string, bool) { + switch s { + case "Connection": + return "", false + default: + return s, true + } + }), + runtime.WithErrorHandler(gatewayErrorHandler), + runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{ + Marshaler: &runtime.JSONPb{ + MarshalOptions: protojson.MarshalOptions{ + UseProtoNames: true, + EmitUnpopulated: true, + }, + UnmarshalOptions: protojson.UnmarshalOptions{ + DiscardUnknown: true, + }, + }, + }), + ) + if err := register( + context.Background(), gateway, endpoint, []grpc.DialOption{grpc.WithInsecure()}, + ); err != nil { + panic(err) + } + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if strings.HasPrefix(c.Path(), pattern) { + gateway.ServeHTTP(c.Response(), c.Request()) + return nil + } + return next(c) + } + } +} + +// gatewayErrorHandler 自定义 error handler +// 覆盖 runtime.DefaultErrorHandle 默认的错误处理,实现自定义的 error payload +func gatewayErrorHandler( + ctx context.Context, + mux *runtime.ServeMux, + marshaler runtime.Marshaler, + w http.ResponseWriter, + r *http.Request, + err error, +) { + var statusCode int + var customStatus *runtime.HTTPStatusError + if errors.As(err, &customStatus) { + err = customStatus.Err + statusCode = customStatus.HTTPStatus + } + + s := status.Convert(err) + + pb := &CustomError{ + Message: s.Message(), + } + + for _, d := range s.Details() { + m, ok := d.(*CustomError) + if !ok { + continue + } + pb.Message = m.Message + pb.Error = m.Toast + pb.Code = m.Code + pb.Data = m.Data + } + + w.Header().Del("Trailer") + w.Header().Del("Transfer-Encoding") + w.Header().Set("Content-Type", marshaler.ContentType(pb)) + + buf, merr := marshaler.Marshal(pb) + if merr != nil { + w.WriteHeader(http.StatusInternalServerError) + xLogger.F(ctx).Errorf("Failed to marshal error message %q: %v", s, merr) + return + } + + if md, ok := runtime.ServerMetadataFromContext(ctx); ok { + for k, vs := range md.HeaderMD { + h := fmt.Sprintf("%s%s", runtime.MetadataHeaderPrefix, k) + for _, v := range vs { + w.Header().Add(h, v) + } + } + } + + if statusCode == 0 { + statusCode = runtime.HTTPStatusFromCode(s.Code()) + } + + w.WriteHeader(statusCode) + if _, err := w.Write(buf); err != nil { + xLogger.F(ctx).Errorf("Failed to write response: %v", err) + } +} diff --git a/cmd/init.go b/cmd/init.go index 448c2d8..ce85f5a 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -208,6 +208,9 @@ type CurDir struct { func modInfoJSON(args ...string) []byte { cmdArgs := append([]string{"list", "-json"}, args...) out, err := exec.Command("go", cmdArgs...).Output() + if len(out) == 0 { + panic("No 'go.mod' found, please make sure to run 'go-coco' in golang project directory containing 'go.mod' file.") + } cobra.CheckErr(err) return out } diff --git a/cmd/tpl/app.go b/cmd/tpl/app.go index 5550c27..1069fa7 100644 --- a/cmd/tpl/app.go +++ b/cmd/tpl/app.go @@ -55,6 +55,13 @@ func (s *Server) Start() error { s.srv.Use(middleware.Sentry()) {{- end }} // TODO: Add Routings +{{- if .WithProto }} + s.srv.Use(xgrpc.GatewayMiddleware( + public.RegisterPublicServiceHandlerFromEndpoint, + s.conf.GRPCAddr, + "/1.0", + )) +{{ end }} {{- if .WithProto }} // Swagger Doc s.srv.GET("/swagger/*", echo.WrapHandler(http.FileServer(http.FS(swagger)))) diff --git a/cmd/version.go b/cmd/version.go index 493e85b..551cb73 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -11,7 +11,7 @@ var versionCmd = &cobra.Command{ Short: "Print the version number of go-coco", Long: `All software has versions. This is go-coco's`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println("go-coco Generator v1.3.6") + fmt.Println("go-coco Generator v0.0.2") }, } diff --git a/go.mod b/go.mod index 99b2621..9b328af 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/go-redis/redis/v8 v8.11.4 github.com/google/gops v0.3.17 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.13.0 github.com/labstack/echo/v4 v4.6.1 github.com/labstack/gommon v0.3.0 github.com/pkg/errors v0.9.1 @@ -28,6 +29,7 @@ require ( go.mongodb.org/mongo-driver v1.5.3 golang.org/x/text v0.4.0 google.golang.org/grpc v1.50.1 + google.golang.org/protobuf v1.28.1 ) require ( @@ -47,7 +49,6 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jcchavezs/porto v0.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -91,7 +92,6 @@ require ( golang.org/x/tools v0.1.12 // indirect google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect google.golang.org/grpc/examples v0.0.0-20220413171549-7567a5d96538 // indirect - google.golang.org/protobuf v1.28.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect howett.net/plist v1.0.0 // indirect diff --git a/go.sum b/go.sum index b5e954e..630f8c4 100644 --- a/go.sum +++ b/go.sum @@ -133,6 +133,7 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -163,7 +164,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gops v0.3.17 h1:CguOcnDVYG32soOj2YevV8mW9asrIh1lZw3d7Ovty/o= @@ -174,6 +174,8 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.13.0 h1:fi9bGIUJOGzzrHBbP8NWbTfNC5fKO6X7kFw40TOqGB8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.13.0/go.mod h1:uY3Aurq+SxwQCpdX91xZ9CgxIMT1EsYtcidljXufYIY= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=