mirror of https://github.com/zhufuyi/sponge
feat: implement sponge commands
This commit is contained in:
parent
552d7b89e8
commit
f67e29c6e1
|
@ -1,27 +0,0 @@
|
|||
name: Test and coverage
|
||||
|
||||
on:
|
||||
# 提交到main分支时触发执行jobs
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# 合并到main分支时触发执行jobs
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
id: go
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Run coverage
|
||||
run: go test -race -coverprofile=coverage.txt -covermode=atomic $(go list ./... | grep -v /vendor/ | grep -v /api/)
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
|
@ -1,29 +0,0 @@
|
|||
name: Go
|
||||
|
||||
on:
|
||||
# 提交到main分支时触发执行jobs
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# 合并到main分支时触发执行jobs
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Get dependencies
|
||||
run: go mod tidy
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
- name: Build
|
||||
run: go build -v cmd/serverNameExample/main.go
|
|
@ -0,0 +1,71 @@
|
|||
name: Test and Build
|
||||
|
||||
on:
|
||||
# triggers the execution of jobs when committed to the main branch
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# trigger execution of jobs when merging to main branch
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
|
||||
lint:
|
||||
name: Golangci-lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Lint Go Code
|
||||
run: |
|
||||
export PATH=$PATH:$(go env GOPATH)/bin
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.49.0
|
||||
make ci-lint
|
||||
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Run Unit tests
|
||||
run: make test
|
||||
- name: Run coverage
|
||||
run: go test -race -coverprofile=coverage.txt -covermode=atomic $(go list ./... | grep -v /vendor/ | grep -v /api/)
|
||||
- name: Upload Coverage report to CodeCov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.txt
|
||||
#token: ${{secrets.CODECOV_TOKEN}}
|
||||
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Build
|
||||
run: make build && make build-sponge
|
|
@ -1,34 +0,0 @@
|
|||
# 主要用于release
|
||||
# see: https://www.qikqiak.com/post/use-github-actions-build-go-app/
|
||||
# https://goreleaser.com/
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
releases-matrix:
|
||||
name: Release goctl binary
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
# build and publish in parallel: linux/386, linux/amd64, linux/arm64,
|
||||
# windows/386, windows/amd64, windows/arm64, darwin/amd64, darwin/arm64
|
||||
goos: [ linux, windows, darwin ]
|
||||
goarch: [ "386", amd64, arm64 ]
|
||||
exclude:
|
||||
- goarch: "386"
|
||||
goos: darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: zhufuyi/sponge@sponge
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
goversion: "https://dl.google.com/go/go1.19.linux-amd64.tar.gz"
|
||||
project_path: "./"
|
||||
binary_name: "sponge"
|
||||
extra_files: README.md
|
|
@ -6,6 +6,7 @@
|
|||
*.dylib
|
||||
*.log
|
||||
cmd/serverNameExample/serverNameExample
|
||||
cmd/sponge/sponge
|
||||
*@@*@@*
|
||||
coverage.txt
|
||||
|
||||
|
|
66
Makefile
66
Makefile
|
@ -61,11 +61,45 @@ cover:
|
|||
go tool cover -html=cover.out
|
||||
|
||||
|
||||
.PHONY: docs
|
||||
# generate swagger docs, the host address can be changed via parameters, e.g. make docs HOST=192.168.3.37
|
||||
docs: mod fmt
|
||||
@bash scripts/swag-docs.sh $(HOST)
|
||||
|
||||
|
||||
.PHONY: graph
|
||||
# generate interactive visual function dependency graphs
|
||||
graph:
|
||||
@echo "generating graph ......"
|
||||
@cp -f cmd/serverNameExample/main.go .
|
||||
go-callvis -skipbrowser -format=svg -nostd -file=serverNameExample github.com/zhufuyi/sponge
|
||||
@rm -f main.go serverNameExample.gv
|
||||
|
||||
|
||||
.PHONY: proto
|
||||
# generate *.pb.go codes from *.proto files
|
||||
proto: mod fmt
|
||||
@bash scripts/protoc.sh
|
||||
|
||||
|
||||
.PHONY: proto-doc
|
||||
# generate doc from *.proto files
|
||||
proto-doc:
|
||||
@bash scripts/proto-doc.sh
|
||||
|
||||
|
||||
.PHONY: build
|
||||
# go build the linux amd64 binary file
|
||||
# build serverNameExample for linux amd64 binary
|
||||
build:
|
||||
@echo "building 'serverNameExample', binary file will output to 'cmd/serverNameExample'"
|
||||
@cd cmd/serverNameExample && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build -gcflags "all=-N -l"
|
||||
@echo "build finished, binary file in path 'cmd/serverNameExample'"
|
||||
|
||||
|
||||
.PHONY: build-sponge
|
||||
# build sponge for linux amd64 binary
|
||||
build-sponge:
|
||||
@echo "building 'sponge', binary file will output to 'cmd/sponge'"
|
||||
@cd cmd/sponge && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build
|
||||
|
||||
|
||||
.PHONY: run
|
||||
|
@ -118,38 +152,12 @@ deploy-docker:
|
|||
# clean binary file, cover.out, redundant dependency packages
|
||||
clean:
|
||||
@rm -vrf cmd/serverNameExample/serverNameExample
|
||||
@rm -vrf cmd/sponge/sponge
|
||||
@rm -vrf cover.out
|
||||
go mod tidy
|
||||
@echo "clean finished"
|
||||
|
||||
|
||||
.PHONY: docs
|
||||
# generate swagger docs, the host address can be changed via parameters, e.g. make docs HOST=192.168.3.37
|
||||
docs: mod fmt
|
||||
@bash scripts/swag-docs.sh $(HOST)
|
||||
|
||||
|
||||
.PHONY: graph
|
||||
# generate interactive visual function dependency graphs
|
||||
graph:
|
||||
@echo "generating graph ......"
|
||||
@cp -f cmd/serverNameExample/main.go .
|
||||
go-callvis -skipbrowser -format=svg -nostd -file=serverNameExample github.com/zhufuyi/sponge
|
||||
@rm -f main.go serverNameExample.gv
|
||||
|
||||
|
||||
.PHONY: proto
|
||||
# generate *.pb.go codes from *.proto files
|
||||
proto: mod fmt
|
||||
@bash scripts/protoc.sh
|
||||
|
||||
|
||||
.PHONY: proto-doc
|
||||
# generate doc from *.proto files
|
||||
proto-doc:
|
||||
@bash scripts/proto-doc.sh
|
||||
|
||||
|
||||
# show help
|
||||
help:
|
||||
@echo ''
|
||||
|
|
44
README.md
44
README.md
|
@ -14,7 +14,7 @@
|
|||
|
||||
</div>
|
||||
|
||||
**sponge** is a go microservices framework, a tool for quickly creating complete microservices codes for http or grpc. Generate `config`, `ecode`, `model`, `dao`, `handler`, `router`, `http`, `proto`, `service`, `grpc` codes from the SQL DDL, which can be combined into full services(similar to how a broken sponge cell automatically reorganises itself into a new sponge).
|
||||
**sponge** is a microservices framework for quickly creating http or grpc code. Generate codes `config`, `ecode`, `model`, `dao`, `handler`, `router`, `http`, `proto`, `service`, `grpc` from SQL DDL, these codes can be combined into complete services (similar to how a broken sponge cell can automatically reorganize into a new sponge).
|
||||
|
||||
Features :
|
||||
|
||||
|
@ -22,17 +22,17 @@ Features :
|
|||
- RPC framework [grpc](https://github.com/grpc/grpc-go)
|
||||
- Configuration file parsing [viper](https://github.com/spf13/viper)
|
||||
- Configuration Center [nacos](https://github.com/alibaba/nacos)
|
||||
- Logging [zap](go.uber.org/zap)
|
||||
- Database component [gorm](gorm.io/gorm)
|
||||
- Caching component [go-redis](github.com/go-redis/redis)
|
||||
- Documentation [swagger](github.com/swaggo/swag)
|
||||
- Authorization [authorization](github.com/golang-jwt/jwt)
|
||||
- Validator [validator](github.com/go-playground/validator)
|
||||
- Rate limiter [ratelimiter](golang.org/x/time/rate)
|
||||
- Circuit Breaker [hystrix](github.com/afex/hystrix-go)
|
||||
- Tracking [opentelemetry](go.opentelemetry.io/otel)
|
||||
- Monitoring [prometheus](github.com/prometheus/client_golang/prometheus) [grafana](https://github.com/grafana/grafana)
|
||||
- Service registration and discovery [etcd](https://github.com/etcd-io/etcd), [consul](https://github.com/hashicorp/consul), [nacos](https://github.com/alibaba/) nacos)
|
||||
- Logging [zap](https://go.uber.org/zap)
|
||||
- Database component [gorm](https://gorm.io/gorm)
|
||||
- Caching component [go-redis](https://github.com/go-redis/redis)
|
||||
- Documentation [swagger](https://github.com/swaggo/swag)
|
||||
- Authorization [authorization](https://github.com/golang-jwt/jwt)
|
||||
- Validator [validator](https://github.com/go-playground/validator)
|
||||
- Rate limiter [aegis](https://github.com/go-kratos/aegis), [rate](https://golang.org/x/time/rate)
|
||||
- Circuit Breaker [aegis](https://github.com/go-kratos/aegis)
|
||||
- Tracking [opentelemetry](https://go.opentelemetry.io/otel)
|
||||
- Monitoring [prometheus](https://github.com/prometheus/client_golang/prometheus), [grafana](https://github.com/grafana/grafana)
|
||||
- Service registration and discovery [etcd](https://github.com/etcd-io/etcd), [consul](https://github.com/hashicorp/consul), [nacos](https://github.com/alibaba/)
|
||||
- Performance analysis [go profile](https://go.dev/blog/pprof)
|
||||
- Code inspection [golangci-lint](https://github.com/golangci/golangci-lint)
|
||||
- Continuous Integration CI [jenkins](https://github.com/jenkinsci/jenkins)
|
||||
|
@ -78,7 +78,15 @@ The development specification follows the [Uber Go Language Coding Specification
|
|||
|
||||
### Install
|
||||
|
||||
> go install github.com/zhufuyi/sponge@sponge
|
||||
|
||||
|
||||
```bash
|
||||
go install github.com/zhufuyi/sponge/cmd/sponge@latest
|
||||
|
||||
sponge update
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
### Quickly create a http project
|
||||
|
||||
|
@ -106,7 +114,7 @@ Way 1: Run locally in the binary
|
|||
|
||||
Copy `http://localhost:8080/swagger/index.html` to your browser and test the api interface.
|
||||
|
||||
Way 2: Run in docker
|
||||
Way 2: Run in docker. Prerequisite: `docker` and `docker-compose` are already installed.
|
||||
|
||||
```bash
|
||||
# Build the docker image
|
||||
|
@ -120,7 +128,7 @@ cd deployments/docker-compose
|
|||
docker-compose ps
|
||||
```
|
||||
|
||||
Way 3: Run in k8s
|
||||
Way 3: Run in k8s. Prerequisite: `docker` and `kubectl` are already installed.
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
|
@ -135,7 +143,7 @@ make deploy-k8s
|
|||
|
||||
# Check the status of the service
|
||||
kubectl get -f account-deployment.yml
|
||||
```
|
||||
```
|
||||
|
||||
You can also use Jenkins to automatically build deployments to k8s.
|
||||
|
||||
|
@ -208,9 +216,9 @@ make deploy-k8s
|
|||
|
||||
# Check the status of the service
|
||||
kubectl get -f account-deployment.yml
|
||||
```
|
||||
```
|
||||
|
||||
You can also use Jenkins to automatically build deployments to k8s.
|
||||
You can also use Jenkins to automatically build deployments to k8s.
|
||||
|
||||
<br>
|
||||
|
||||
|
|
|
@ -59,13 +59,7 @@ func registerInits() []app.Init {
|
|||
_, _ = logger.Init(
|
||||
logger.WithLevel(config.Get().Logger.Level),
|
||||
logger.WithFormat(config.Get().Logger.Format),
|
||||
logger.WithSave(config.Get().Logger.IsSave,
|
||||
logger.WithFileName(config.Get().Logger.LogFileConfig.Filename),
|
||||
logger.WithFileMaxSize(config.Get().Logger.LogFileConfig.MaxSize),
|
||||
logger.WithFileMaxBackups(config.Get().Logger.LogFileConfig.MaxBackups),
|
||||
logger.WithFileMaxAge(config.Get().Logger.LogFileConfig.MaxAge),
|
||||
logger.WithFileIsCompression(config.Get().Logger.LogFileConfig.IsCompression),
|
||||
),
|
||||
logger.WithSave(config.Get().Logger.IsSave),
|
||||
)
|
||||
|
||||
var inits []app.Init
|
||||
|
@ -171,6 +165,7 @@ func grpcOptions() []server.GRPCOption {
|
|||
return opts
|
||||
}
|
||||
|
||||
// 使用etcd实例化服务注册,consul和nacos也类似
|
||||
func getETCDRegistry(etcdEndpoints []string, instanceName string, instanceEndpoints []string) (registry.Registry, *registry.ServiceInstance) {
|
||||
serviceInstance := registry.NewServiceInstance(instanceName, instanceEndpoints)
|
||||
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/gofile"
|
||||
"github.com/zhufuyi/sponge/pkg/replacer"
|
||||
)
|
||||
|
||||
const (
|
||||
// TplNameSponge 模板目录名称
|
||||
TplNameSponge = "sponge"
|
||||
)
|
||||
|
||||
var (
|
||||
// 指定文件替换标记
|
||||
modelFile = "model/userExample.go"
|
||||
modelFileMark = "// todo generate model codes to here"
|
||||
|
||||
daoFile = "dao/userExample.go"
|
||||
daoFileMark = "// todo generate the update fields code to here"
|
||||
daoTestFile = "dao/userExample_test.go"
|
||||
|
||||
handlerFile = "types/userExample_types.go"
|
||||
handlerFileMark = "// todo generate the request and response struct to here"
|
||||
handlerTestFile = "handler/userExample_test.go"
|
||||
|
||||
mainFile = "serverNameExample/main.go"
|
||||
mainFileMark = "// todo generate the code to register http and grpc services here"
|
||||
|
||||
protoFile = "v1/userExample.proto"
|
||||
protoFileMark = "// todo generate the protobuf code here"
|
||||
|
||||
serviceTestFile = "service/userExample_test.go"
|
||||
serviceClientFile = "service/userExample_client_test.go"
|
||||
serviceFileMark = "// todo generate the service struct code here"
|
||||
|
||||
dockerFile = "build/Dockerfile"
|
||||
dockerFileMark = "# todo generate dockerfile code for http or grpc here"
|
||||
|
||||
dockerFileBuild = "build/Dockerfile_build"
|
||||
dockerFileBuildMark = "# todo generate dockerfile_build code for http or grpc here"
|
||||
|
||||
dockerComposeFile = "deployments/docker-compose/docker-compose.yml"
|
||||
dockerComposeFileMark = "# todo generate docker-compose.yml code for http or grpc here"
|
||||
|
||||
k8sDeploymentFile = "deployments/kubernetes/serverNameExample-deployment.yml"
|
||||
k8sDeploymentFileMark = "# todo generate k8s-deployment.yml code for http or grpc here"
|
||||
|
||||
k8sServiceFile = "deployments/kubernetes/serverNameExample-svc.yml"
|
||||
k8sServiceFileMark = "# todo generate k8s-svc.yml code for http or grpc here"
|
||||
|
||||
imageBuildFile = "scripts/image-build.sh"
|
||||
readmeFile = "sponge/README.md"
|
||||
|
||||
// 清除标记的模板代码片段标记
|
||||
startMark = []byte("// delete the templates code start")
|
||||
endMark = []byte("// delete the templates code end")
|
||||
wellStartMark = bytes.ReplaceAll(startMark, []byte("//"), []byte("#"))
|
||||
wellEndMark = bytes.ReplaceAll(endMark, []byte("//"), []byte("#"))
|
||||
onlyGrpcStartMark = []byte("// only grpc use start")
|
||||
onlyGrpcEndMark = []byte("// only grpc use end\n")
|
||||
wellOnlyGrpcStartMark = bytes.ReplaceAll(onlyGrpcStartMark, []byte("//"), []byte("#"))
|
||||
wellOnlyGrpcEndMark = bytes.ReplaceAll(onlyGrpcEndMark, []byte("//"), []byte("#"))
|
||||
|
||||
selfPackageName = "github.com/zhufuyi/sponge"
|
||||
)
|
||||
|
||||
func adjustmentOfIDType(handlerCodes string) string {
|
||||
return idTypeToStr(idTypeFixToUint64(handlerCodes))
|
||||
}
|
||||
|
||||
func idTypeFixToUint64(handlerCodes string) string {
|
||||
subStart := "ByIDRequest struct {"
|
||||
subEnd := "`" + `json:"id" binding:""` + "`"
|
||||
if subBytes := gofile.FindSubBytesNotIn([]byte(handlerCodes), []byte(subStart), []byte(subEnd)); len(subBytes) > 0 {
|
||||
old := subStart + string(subBytes) + subEnd
|
||||
newStr := subStart + "\n\tID uint64 " + subEnd + " // uint64 id\n"
|
||||
handlerCodes = strings.ReplaceAll(handlerCodes, old, newStr)
|
||||
}
|
||||
|
||||
return handlerCodes
|
||||
}
|
||||
|
||||
func idTypeToStr(handlerCodes string) string {
|
||||
subStart := "ByIDRespond struct {"
|
||||
subEnd := "`" + `json:"id"` + "`"
|
||||
if subBytes := gofile.FindSubBytesNotIn([]byte(handlerCodes), []byte(subStart), []byte(subEnd)); len(subBytes) > 0 {
|
||||
old := subStart + string(subBytes) + subEnd
|
||||
newStr := subStart + "\n\tID string " + subEnd + " // covert to string id\n"
|
||||
handlerCodes = strings.ReplaceAll(handlerCodes, old, newStr)
|
||||
}
|
||||
|
||||
return handlerCodes
|
||||
}
|
||||
|
||||
func deleteFieldsMark(r replacer.Replacer, filename string, startMark []byte, endMark []byte) []replacer.Field {
|
||||
var fields []replacer.Field
|
||||
|
||||
data, err := r.ReadFile(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("read the file '%s' error: %v\n", filename, err)
|
||||
return fields
|
||||
}
|
||||
if subBytes := gofile.FindSubBytes(data, startMark, endMark); len(subBytes) > 0 {
|
||||
fields = append(fields,
|
||||
replacer.Field{ // 清除标记的模板代码
|
||||
Old: string(subBytes),
|
||||
New: "",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
func replaceFileContentMark(r replacer.Replacer, filename string, newContent string) []replacer.Field {
|
||||
var fields []replacer.Field
|
||||
|
||||
data, err := r.ReadFile(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("read the file '%s' error: %v\n", filename, err)
|
||||
return fields
|
||||
}
|
||||
|
||||
fields = append(fields, replacer.Field{
|
||||
Old: string(data),
|
||||
New: newContent,
|
||||
})
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// 解析镜像仓库host和name
|
||||
func parseImageRepoAddr(addr string) (string, string) {
|
||||
splits := strings.Split(addr, "/")
|
||||
|
||||
// 官方仓库地址
|
||||
if len(splits) == 1 {
|
||||
return "https://index.docker.io/v1", addr
|
||||
}
|
||||
|
||||
// 非官方仓库地址
|
||||
l := len(splits)
|
||||
return strings.Join(splits[:l-1], "/"), splits[l-1]
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/gofile"
|
||||
"github.com/zhufuyi/sponge/pkg/jy2struct"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ConfigCommand covert yaml to struct command
|
||||
func ConfigCommand() *cobra.Command {
|
||||
var (
|
||||
ysArgs = jy2struct.Args{
|
||||
Tags: "json",
|
||||
SubStruct: true,
|
||||
}
|
||||
serverDir = ""
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Generate go config code from yaml file",
|
||||
Long: `generate go config code from yaml file.
|
||||
|
||||
Examples:
|
||||
# generate config code in server directory, the yaml configuration file must be in <yourServerDir>/configs directory.
|
||||
sponge config --server-dir=/yourServerDir
|
||||
`,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
files, err := getYAMLFile(serverDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = runGenConfigCommand(files, ysArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("covert yaml to go struct successfully.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&serverDir, "server-dir", "d", "", "server directory")
|
||||
_ = cmd.MarkFlagRequired("server-dir")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenConfigCommand(files map[string]configType, ysArgs jy2struct.Args) error {
|
||||
for outputFile, config := range files {
|
||||
ysArgs.Format = "yaml"
|
||||
ysArgs.InputFile = config.configFile
|
||||
|
||||
var startCode string
|
||||
if config.isConfigCenter {
|
||||
ysArgs.Name = "Center"
|
||||
startCode = configFileCcCode
|
||||
} else {
|
||||
ysArgs.Name = "Config"
|
||||
startCode = configFileCode
|
||||
}
|
||||
structCodes, err := jy2struct.Covert(&ysArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = saveFile(config.configFile, outputFile, startCode+structCodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type configType struct {
|
||||
configFile string
|
||||
isConfigCenter bool
|
||||
}
|
||||
|
||||
// 从configs目录读取所有yaml文件目录,一个是.yml,另一个是cc.yml
|
||||
func getYAMLFile(serverDir string) (map[string]configType, error) {
|
||||
// 生成目标文件:配置文件
|
||||
files := make(map[string]configType)
|
||||
configsDir := serverDir + gofile.GetPathDelimiter() + "configs"
|
||||
goConfigDir := serverDir + gofile.GetPathDelimiter() + "internal" + gofile.GetPathDelimiter() + "config"
|
||||
|
||||
ymlFiles, err := gofile.ListFiles(configsDir, gofile.WithSuffix(".yml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ymlFiles) > 2 {
|
||||
return nil, fmt.Errorf("config files are allowed up to 2, currently there are %d", len(ymlFiles))
|
||||
}
|
||||
|
||||
yamlFiles, err := gofile.ListFiles(configsDir, gofile.WithSuffix(".yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(yamlFiles) > 2 {
|
||||
return nil, fmt.Errorf("config files are allowed up to 2, currently there are %d", len(yamlFiles))
|
||||
}
|
||||
|
||||
if len(ymlFiles) == 0 && len(yamlFiles) == 0 {
|
||||
return nil, fmt.Errorf("not found config files in directory %s", configsDir)
|
||||
}
|
||||
|
||||
if len(ymlFiles) != 0 && len(yamlFiles) != 0 {
|
||||
return nil, fmt.Errorf("please use 'yml' or 'yaml' suffixes for configuration files, do not mix them")
|
||||
}
|
||||
|
||||
if len(ymlFiles) > 0 {
|
||||
for _, file := range ymlFiles {
|
||||
name := gofile.GetFilename(file)
|
||||
files[goConfigDir+gofile.GetPathDelimiter()+strings.ReplaceAll(name, ".yml", ".go")] = configType{
|
||||
configFile: file,
|
||||
isConfigCenter: strings.Contains(name, "cc.yml"),
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
if len(yamlFiles) > 0 {
|
||||
for _, file := range yamlFiles {
|
||||
name := gofile.GetFilename(file)
|
||||
files[goConfigDir+gofile.GetPathDelimiter()+strings.ReplaceAll(name, ".yaml", ".go")] = configType{
|
||||
configFile: file,
|
||||
isConfigCenter: strings.Contains(name, "cc.yaml"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func saveFile(inputFile string, outputFile string, code string) error {
|
||||
err := os.WriteFile(outputFile, []byte(code), 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s ----> %s\n", inputFile, outputFile)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/replacer"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// DaoCommand generate dao code
|
||||
func DaoCommand() *cobra.Command {
|
||||
var (
|
||||
moduleName string // 服务名称,也就是包名称
|
||||
outPath string // 输出目录
|
||||
sqlArgs = sql2code.Args{
|
||||
Package: "model",
|
||||
JSONTag: true,
|
||||
GormType: true,
|
||||
}
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "dao",
|
||||
Short: "Generate dao code",
|
||||
Long: `generate dao code.
|
||||
|
||||
Examples:
|
||||
# generate dao code and embed 'gorm.model' struct.
|
||||
sponge dao --module-name=yourModuleName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
|
||||
|
||||
# generate dao code, structure fields correspond to the column names of the table.
|
||||
sponge dao --module-name=yourModuleName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --embed=false
|
||||
|
||||
# generate dao code and specify the output directory, Note: if the file already exists, code generation will be canceled.
|
||||
sponge dao --module-name=yourModuleName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
|
||||
`,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
codes, err := sql2code.Generate(&sqlArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runGenDaoCommand(moduleName, codes, outPath)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&moduleName, "module-name", "m", "", "module-name is the name of the module in the 'go.mod' file")
|
||||
_ = cmd.MarkFlagRequired("module-name")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
|
||||
_ = cmd.MarkFlagRequired("db-dsn")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
|
||||
_ = cmd.MarkFlagRequired("db-table")
|
||||
cmd.Flags().BoolVarP(&sqlArgs.IsEmbed, "embed", "e", true, "whether to embed 'gorm.Model' struct")
|
||||
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./dao_<time>")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenDaoCommand(moduleName string, codes map[string]string, outPath string) error {
|
||||
subTplName := "dao"
|
||||
r := Replacers[TplNameSponge]
|
||||
if r == nil {
|
||||
return errors.New("r is nil")
|
||||
}
|
||||
|
||||
// 设置模板信息
|
||||
subDirs := []string{"internal/model", "internal/cache", "internal/dao"} // 只处理的指定子目录,如果为空或者没有指定的子目录,表示所有文件
|
||||
ignoreDirs := []string{} // 指定子目录下忽略处理的目录
|
||||
ignoreFiles := []string{"init.go", "init_test.go"} // 指定子目录下忽略处理的文件
|
||||
|
||||
r.SetSubDirs(subDirs...)
|
||||
r.SetIgnoreSubDirs(ignoreDirs...)
|
||||
r.SetIgnoreFiles(ignoreFiles...)
|
||||
fields := addDAOFields(moduleName, r, codes)
|
||||
r.SetReplacementFields(fields)
|
||||
_ = r.SetOutputDir(outPath, subTplName)
|
||||
if err := r.SaveFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("generate '%s' code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
|
||||
return nil
|
||||
}
|
||||
|
||||
func addDAOFields(moduleName string, r replacer.Replacer, codes map[string]string) []replacer.Field {
|
||||
var fields []replacer.Field
|
||||
|
||||
fields = append(fields, deleteFieldsMark(r, modelFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, daoFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, daoTestFile, startMark, endMark)...)
|
||||
fields = append(fields, []replacer.Field{
|
||||
{ // 替换model/userExample.go文件内容
|
||||
Old: modelFileMark,
|
||||
New: codes[parser.CodeTypeModel],
|
||||
},
|
||||
{ // 替换dao/userExample.go文件内容
|
||||
Old: daoFileMark,
|
||||
New: codes[parser.CodeTypeDAO],
|
||||
},
|
||||
{
|
||||
Old: selfPackageName + "/" + r.GetSourcePath(),
|
||||
New: moduleName,
|
||||
},
|
||||
{
|
||||
Old: "github.com/zhufuyi/sponge",
|
||||
New: moduleName,
|
||||
},
|
||||
{
|
||||
Old: moduleName + "/pkg",
|
||||
New: "github.com/zhufuyi/sponge/pkg",
|
||||
},
|
||||
{
|
||||
Old: "UserExample",
|
||||
New: codes[parser.TableName],
|
||||
IsCaseSensitive: true,
|
||||
},
|
||||
}...)
|
||||
|
||||
return fields
|
||||
}
|
|
@ -0,0 +1,271 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/gofile"
|
||||
"github.com/zhufuyi/sponge/pkg/replacer"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
|
||||
|
||||
"github.com/huandu/xstrings"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// GRPCCommand generate grpc code
|
||||
func GRPCCommand() *cobra.Command {
|
||||
var (
|
||||
moduleName string // go.mod文件的module名称
|
||||
serverName string // 服务名称
|
||||
projectName string // 项目名称
|
||||
repoAddr string // 镜像仓库地址
|
||||
outPath string // 输出目录
|
||||
sqlArgs = sql2code.Args{
|
||||
Package: "model",
|
||||
JSONTag: true,
|
||||
GormType: true,
|
||||
}
|
||||
)
|
||||
|
||||
//nolint
|
||||
cmd := &cobra.Command{
|
||||
Use: "grpc",
|
||||
Short: "Generate grpc server code",
|
||||
Long: `generate grpc server code.
|
||||
|
||||
Examples:
|
||||
# generate grpc server code and embed 'gorm.model' struct.
|
||||
sponge grpc --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
|
||||
|
||||
# generate grpc server code, structure fields correspond to the column names of the table.
|
||||
sponge grpc --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --embed=false
|
||||
|
||||
# generate grpc server code and specify the output directory, Note: if the file already exists, code generation will be canceled.
|
||||
sponge grpc --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
|
||||
|
||||
# generate grpc server code and specify the docker image repository address.
|
||||
sponge grpc --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --repo-addr=192.168.3.37:9443/user-name --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
|
||||
`,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
codes, err := sql2code.Generate(&sqlArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runGenGRPCCommand(moduleName, serverName, projectName, repoAddr, sqlArgs.DBDsn, codes, outPath)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&moduleName, "module-name", "m", "", "module-name is the name of the module in the 'go.mod' file")
|
||||
_ = cmd.MarkFlagRequired("module-name")
|
||||
cmd.Flags().StringVarP(&serverName, "server-name", "s", "", "server name")
|
||||
_ = cmd.MarkFlagRequired("server-name")
|
||||
cmd.Flags().StringVarP(&projectName, "project-name", "p", "", "project name")
|
||||
_ = cmd.MarkFlagRequired("project-name")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
|
||||
_ = cmd.MarkFlagRequired("db-dsn")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
|
||||
_ = cmd.MarkFlagRequired("db-table")
|
||||
cmd.Flags().BoolVarP(&sqlArgs.IsEmbed, "embed", "e", true, "whether to embed 'gorm.Model' struct")
|
||||
cmd.Flags().StringVarP(&repoAddr, "repo-addr", "r", "", "docker image repository address, excluding http and repository names")
|
||||
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./serverName_grpc_<time>")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenGRPCCommand(moduleName string, serverName string, projectName string, repoAddr string,
|
||||
dbDSN string, codes map[string]string, outPath string) error {
|
||||
subTplName := "grpc"
|
||||
r := Replacers[TplNameSponge]
|
||||
if r == nil {
|
||||
return errors.New("replacer is nil")
|
||||
}
|
||||
|
||||
// 设置模板信息
|
||||
subDirs := []string{} // 只处理的指定子目录,如果为空或者没有指定的子目录,表示所有文件
|
||||
ignoreDirs := []string{"cmd/sponge", "sponge/.github", "sponge/.git", "sponge/docs", "sponge/pkg", "sponge/assets",
|
||||
"sponge/test", "internal/handler", "internal/routers", "internal/types"} // 指定子目录下忽略处理的目录
|
||||
ignoreFiles := []string{"http_systemCode.go", "http_userExample.go", "http.go", "http_test.go", "http_option.go",
|
||||
"userExample.pb.go", "userExample.pb.validate.go", "userExample_grpc.pb.go",
|
||||
"types.pb.go", "types.pb.validate.go", "LICENSE", "doc.go"} // 指定子目录下忽略处理的文件
|
||||
|
||||
r.SetSubDirs(subDirs...)
|
||||
r.SetIgnoreSubDirs(ignoreDirs...)
|
||||
r.SetIgnoreFiles(ignoreFiles...)
|
||||
fields := addGRPCFields(moduleName, serverName, projectName, repoAddr, r, dbDSN, codes)
|
||||
r.SetReplacementFields(fields)
|
||||
_ = r.SetOutputDir(outPath, serverName+"_"+subTplName)
|
||||
if err := r.SaveFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("generate '%s' project code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
|
||||
return nil
|
||||
}
|
||||
|
||||
func addGRPCFields(moduleName string, serverName string, projectName string, repoAddr string,
|
||||
r replacer.Replacer, dbDSN string, codes map[string]string) []replacer.Field {
|
||||
var fields []replacer.Field
|
||||
|
||||
repoHost, _ := parseImageRepoAddr(repoAddr)
|
||||
|
||||
fields = append(fields, deleteFieldsMark(r, modelFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, daoFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, daoTestFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, protoFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, serviceClientFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, serviceTestFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, mainFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, dockerFile, wellStartMark, wellEndMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, dockerFileBuild, wellStartMark, wellEndMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, dockerComposeFile, wellStartMark, wellEndMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, k8sDeploymentFile, wellStartMark, wellEndMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, k8sServiceFile, wellStartMark, wellEndMark)...)
|
||||
fields = append(fields, replaceFileContentMark(r, readmeFile, "## "+serverName)...)
|
||||
fields = append(fields, []replacer.Field{
|
||||
{ // 替换model/userExample.go文件内容
|
||||
Old: modelFileMark,
|
||||
New: codes[parser.CodeTypeModel],
|
||||
},
|
||||
{ // 替换dao/userExample.go文件内容
|
||||
Old: daoFileMark,
|
||||
New: codes[parser.CodeTypeDAO],
|
||||
},
|
||||
{ // 替换v1/userExample.proto文件内容
|
||||
Old: protoFileMark,
|
||||
New: codes[parser.CodeTypeProto],
|
||||
},
|
||||
{ // 替换service/userExample_client_test.go文件内容
|
||||
Old: serviceFileMark,
|
||||
New: adjustmentOfIDType(codes[parser.CodeTypeService]),
|
||||
},
|
||||
{ // 替换main.go文件内容
|
||||
Old: mainFileMark,
|
||||
New: mainFileGrpcCode,
|
||||
},
|
||||
{ // 替换Dockerfile文件内容
|
||||
Old: dockerFileMark,
|
||||
New: dockerFileGrpcCode,
|
||||
},
|
||||
{ // 替换Dockerfile_build文件内容
|
||||
Old: dockerFileBuildMark,
|
||||
New: dockerFileBuildGrpcCode,
|
||||
},
|
||||
{ // 替换docker-compose.yml文件内容
|
||||
Old: dockerComposeFileMark,
|
||||
New: dockerComposeFileGrpcCode,
|
||||
},
|
||||
{ // 替换*-deployment.yml文件内容
|
||||
Old: k8sDeploymentFileMark,
|
||||
New: k8sDeploymentFileGrpcCode,
|
||||
},
|
||||
{ // 替换*-svc.yml文件内容
|
||||
Old: k8sServiceFileMark,
|
||||
New: k8sServiceFileGrpcCode,
|
||||
},
|
||||
// 替换github.com/zhufuyi/sponge/templates/sponge
|
||||
{
|
||||
Old: selfPackageName + "/" + r.GetSourcePath(),
|
||||
New: moduleName,
|
||||
},
|
||||
// 替换目录名称
|
||||
{
|
||||
Old: strings.Join([]string{"api", "userExample", "v1"}, gofile.GetPathDelimiter()),
|
||||
New: strings.Join([]string{"api", serverName, "v1"}, gofile.GetPathDelimiter()),
|
||||
},
|
||||
{
|
||||
Old: "github.com/zhufuyi/sponge",
|
||||
New: moduleName,
|
||||
},
|
||||
{
|
||||
Old: moduleName + "/pkg",
|
||||
New: "github.com/zhufuyi/sponge/pkg",
|
||||
},
|
||||
{
|
||||
Old: "api/userExample/v1",
|
||||
New: fmt.Sprintf("api/%s/v1", serverName),
|
||||
},
|
||||
{
|
||||
Old: "api.userExample.v1",
|
||||
New: fmt.Sprintf("api.%s.v1", strings.ReplaceAll(serverName, "-", "_")), // proto package 不能存在"-"号
|
||||
},
|
||||
{
|
||||
Old: "sponge api docs",
|
||||
New: serverName + " api docs",
|
||||
},
|
||||
{
|
||||
Old: "userExampleNO = 1",
|
||||
New: fmt.Sprintf("userExampleNO = %d", rand.Intn(1000)),
|
||||
},
|
||||
{
|
||||
Old: "serverNameExample",
|
||||
New: serverName,
|
||||
},
|
||||
// docker镜像和k8s部署脚本替换
|
||||
{
|
||||
Old: "server-name-example",
|
||||
New: xstrings.ToKebabCase(serverName),
|
||||
},
|
||||
{
|
||||
Old: "projectNameExample",
|
||||
New: projectName,
|
||||
},
|
||||
// docker镜像和k8s部署脚本替换
|
||||
{
|
||||
Old: "project-name-example",
|
||||
New: xstrings.ToKebabCase(projectName),
|
||||
},
|
||||
{
|
||||
Old: "repo-addr-example",
|
||||
New: repoAddr,
|
||||
},
|
||||
{
|
||||
Old: "image-repo-host",
|
||||
New: repoHost,
|
||||
},
|
||||
{
|
||||
Old: string(onlyGrpcStartMark),
|
||||
New: "",
|
||||
},
|
||||
{
|
||||
Old: string(onlyGrpcEndMark),
|
||||
New: "",
|
||||
},
|
||||
{
|
||||
Old: string(wellOnlyGrpcStartMark),
|
||||
New: "",
|
||||
},
|
||||
{
|
||||
Old: string(wellOnlyGrpcEndMark),
|
||||
New: "",
|
||||
},
|
||||
{
|
||||
Old: "tmp.go.mod",
|
||||
New: "go.mod",
|
||||
},
|
||||
{
|
||||
Old: "tmp.gitignore",
|
||||
New: ".gitignore",
|
||||
},
|
||||
{
|
||||
Old: "tmp.golangci.yml",
|
||||
New: ".golangci.yml",
|
||||
},
|
||||
{
|
||||
Old: "root:123456@(192.168.3.37:3306)/account",
|
||||
New: dbDSN,
|
||||
},
|
||||
{
|
||||
Old: "UserExample",
|
||||
New: codes[parser.TableName],
|
||||
IsCaseSensitive: true,
|
||||
},
|
||||
}...)
|
||||
|
||||
return fields
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/replacer"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// HandlerCommand generate handler code
|
||||
func HandlerCommand() *cobra.Command {
|
||||
var (
|
||||
moduleName string // go.mod文件的module名称
|
||||
outPath string // 输出目录
|
||||
sqlArgs = sql2code.Args{
|
||||
Package: "model",
|
||||
JSONTag: true,
|
||||
GormType: true,
|
||||
}
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "handler",
|
||||
Short: "Generate handler code",
|
||||
Long: `generate handler code.
|
||||
|
||||
Examples:
|
||||
# generate handler code and embed 'gorm.model' struct.
|
||||
sponge handler --module-name=yourModuleName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
|
||||
|
||||
# generate handler code, structure fields correspond to the column names of the table.
|
||||
sponge handler --module-name=yourModuleName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --embed=false
|
||||
|
||||
# generate handler code and specify the output directory, Note: if the file already exists, code generation will be canceled.
|
||||
sponge handler --module-name=yourModuleName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
|
||||
`,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
codes, err := sql2code.Generate(&sqlArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runGenHandlerCommand(moduleName, codes, outPath)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&moduleName, "module-name", "m", "", "module-name is the name of the module in the 'go.mod' file")
|
||||
_ = cmd.MarkFlagRequired("module-name")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
|
||||
_ = cmd.MarkFlagRequired("db-dsn")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
|
||||
_ = cmd.MarkFlagRequired("db-table")
|
||||
cmd.Flags().BoolVarP(&sqlArgs.IsEmbed, "embed", "e", true, "whether to embed 'gorm.Model' struct")
|
||||
|
||||
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./handler_<time>")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenHandlerCommand(moduleName string, codes map[string]string, outPath string) error {
|
||||
subTplName := "handler"
|
||||
r := Replacers[TplNameSponge]
|
||||
if r == nil {
|
||||
return errors.New("replacer is nil")
|
||||
}
|
||||
|
||||
// 设置模板信息
|
||||
subDirs := []string{"internal/model", "internal/cache", "internal/dao",
|
||||
"internal/ecode", "internal/handler", "internal/routers", "internal/types"} // 只处理的指定子目录,如果为空或者没有指定的子目录,表示所有文件
|
||||
ignoreDirs := []string{} // 指定子目录下忽略处理的目录
|
||||
ignoreFiles := []string{"init.go", "init_test.go", "swagger_types.go", "http_systemCode.go",
|
||||
"grpc_systemCode.go", "grpc_userExample.go", "grpc_systemCode_test.go", "routers.go", "routers_test.go"} // 指定子目录下忽略处理的文件
|
||||
|
||||
r.SetSubDirs(subDirs...)
|
||||
r.SetIgnoreSubDirs(ignoreDirs...)
|
||||
r.SetIgnoreFiles(ignoreFiles...)
|
||||
fields := addHandlerFields(moduleName, r, codes)
|
||||
r.SetReplacementFields(fields)
|
||||
_ = r.SetOutputDir(outPath, subTplName)
|
||||
if err := r.SaveFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("generate '%s' code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
|
||||
return nil
|
||||
}
|
||||
|
||||
func addHandlerFields(moduleName string, r replacer.Replacer, codes map[string]string) []replacer.Field {
|
||||
var fields []replacer.Field
|
||||
|
||||
fields = append(fields, deleteFieldsMark(r, modelFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, daoFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, daoTestFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, handlerFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, handlerTestFile, startMark, endMark)...)
|
||||
fields = append(fields, []replacer.Field{
|
||||
{ // 替换model/userExample.go文件内容
|
||||
Old: modelFileMark,
|
||||
New: codes[parser.CodeTypeModel],
|
||||
},
|
||||
{ // 替换dao/userExample.go文件内容
|
||||
Old: daoFileMark,
|
||||
New: codes[parser.CodeTypeDAO],
|
||||
},
|
||||
{ // 替换handler/userExample.go文件内容
|
||||
Old: handlerFileMark,
|
||||
New: adjustmentOfIDType(codes[parser.CodeTypeHandler]),
|
||||
},
|
||||
{
|
||||
Old: selfPackageName + "/" + r.GetSourcePath(),
|
||||
New: moduleName,
|
||||
},
|
||||
{
|
||||
Old: "github.com/zhufuyi/sponge",
|
||||
New: moduleName,
|
||||
},
|
||||
{
|
||||
Old: "userExampleNO = 1",
|
||||
New: fmt.Sprintf("userExampleNO = %d", rand.Intn(1000)),
|
||||
},
|
||||
{
|
||||
Old: moduleName + "/pkg",
|
||||
New: "github.com/zhufuyi/sponge/pkg",
|
||||
},
|
||||
{
|
||||
Old: "UserExample",
|
||||
New: codes[parser.TableName],
|
||||
IsCaseSensitive: true,
|
||||
},
|
||||
}...)
|
||||
|
||||
return fields
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/replacer"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
|
||||
|
||||
"github.com/huandu/xstrings"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// HTTPCommand generate http code
|
||||
func HTTPCommand() *cobra.Command {
|
||||
var (
|
||||
moduleName string // go.mod文件的module名称
|
||||
serverName string // 服务名称
|
||||
projectName string // 项目名称
|
||||
repoAddr string // 镜像仓库地址
|
||||
outPath string // 输出目录
|
||||
sqlArgs = sql2code.Args{
|
||||
Package: "model",
|
||||
JSONTag: true,
|
||||
GormType: true,
|
||||
}
|
||||
)
|
||||
|
||||
//nolint
|
||||
cmd := &cobra.Command{
|
||||
Use: "http",
|
||||
Short: "Generate http server code",
|
||||
Long: `generate http server code.
|
||||
|
||||
Examples:
|
||||
# generate http code and embed 'gorm.model' struct.
|
||||
sponge http --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
|
||||
|
||||
# generate http code, structure fields correspond to the column names of the table.
|
||||
sponge http --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --embed=false
|
||||
|
||||
# generate http code and specify the output directory, Note: if the file already exists, code generation will be canceled.
|
||||
sponge http --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
|
||||
|
||||
# generate http code and specify the docker image repository address.
|
||||
sponge http --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --repo-addr=192.168.3.37:9443/user-name --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
|
||||
`,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
codes, err := sql2code.Generate(&sqlArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runGenHTTPCommand(moduleName, serverName, projectName, repoAddr, sqlArgs.DBDsn, codes, outPath)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&moduleName, "module-name", "m", "", "module-name is the name of the module in the 'go.mod' file")
|
||||
_ = cmd.MarkFlagRequired("module-name")
|
||||
cmd.Flags().StringVarP(&serverName, "server-name", "s", "", "server name")
|
||||
_ = cmd.MarkFlagRequired("server-name")
|
||||
cmd.Flags().StringVarP(&projectName, "project-name", "p", "", "project name")
|
||||
_ = cmd.MarkFlagRequired("project-name")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
|
||||
_ = cmd.MarkFlagRequired("db-dsn")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
|
||||
_ = cmd.MarkFlagRequired("db-table")
|
||||
cmd.Flags().BoolVarP(&sqlArgs.IsEmbed, "embed", "e", true, "whether to embed 'gorm.Model' struct")
|
||||
cmd.Flags().StringVarP(&repoAddr, "repo-addr", "r", "", "docker image repository address, excluding http and repository names")
|
||||
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./serverName_http_<time>")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenHTTPCommand(moduleName string, serverName string, projectName string, repoAddr string,
|
||||
dbDSN string, codes map[string]string, outPath string) error {
|
||||
subTplName := "http"
|
||||
r := Replacers[TplNameSponge]
|
||||
if r == nil {
|
||||
return errors.New("replacer is nil")
|
||||
}
|
||||
|
||||
// 设置模板信息
|
||||
subDirs := []string{} // 只处理的子目录,如果为空或者没有指定的子目录,表示所有文件
|
||||
ignoreDirs := []string{"cmd/sponge", "sponge/.github", "sponge/.git", "sponge/api", "sponge/pkg", "sponge/assets",
|
||||
"sponge/test", "sponge/third_party", "internal/service"} // 指定子目录下忽略处理的目录
|
||||
ignoreFiles := []string{"swagger.json", "swagger.yaml", "proto.html", "protoc.sh",
|
||||
"proto-doc.sh", "grpc.go", "grpc_option.go", "grpc_test.go", "LICENSE", "doc.go",
|
||||
"grpc_userExample.go", "grpc_systemCode.go", "grpc_systemCode_test.go"} // 指定子目录下忽略处理的文件
|
||||
|
||||
r.SetSubDirs(subDirs...)
|
||||
r.SetIgnoreSubDirs(ignoreDirs...)
|
||||
r.SetIgnoreFiles(ignoreFiles...)
|
||||
fields := addHTTPFields(moduleName, serverName, projectName, repoAddr, r, dbDSN, codes)
|
||||
r.SetReplacementFields(fields)
|
||||
_ = r.SetOutputDir(outPath, serverName+"_"+subTplName)
|
||||
if err := r.SaveFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("generate '%s' project code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
|
||||
return nil
|
||||
}
|
||||
|
||||
func addHTTPFields(moduleName string, serverName string, projectName string, repoAddr string,
|
||||
r replacer.Replacer, dbDSN string, codes map[string]string) []replacer.Field {
|
||||
var fields []replacer.Field
|
||||
|
||||
repoHost, _ := parseImageRepoAddr(repoAddr)
|
||||
|
||||
fields = append(fields, deleteFieldsMark(r, modelFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, daoFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, daoTestFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, handlerFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, handlerTestFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, mainFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, mainFile, onlyGrpcStartMark, onlyGrpcEndMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, dockerFile, wellStartMark, wellEndMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, dockerFileBuild, wellStartMark, wellEndMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, dockerComposeFile, wellStartMark, wellEndMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, k8sDeploymentFile, wellStartMark, wellEndMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, k8sServiceFile, wellStartMark, wellEndMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, imageBuildFile, wellOnlyGrpcStartMark, wellOnlyGrpcEndMark)...)
|
||||
fields = append(fields, replaceFileContentMark(r, readmeFile, "## "+serverName)...)
|
||||
fields = append(fields, []replacer.Field{
|
||||
{ // 替换model/userExample.go文件内容
|
||||
Old: modelFileMark,
|
||||
New: codes[parser.CodeTypeModel],
|
||||
},
|
||||
{ // 替换dao/userExample.go文件内容
|
||||
Old: daoFileMark,
|
||||
New: codes[parser.CodeTypeDAO],
|
||||
},
|
||||
{ // 替换handler/userExample.go文件内容
|
||||
Old: handlerFileMark,
|
||||
New: adjustmentOfIDType(codes[parser.CodeTypeHandler]),
|
||||
},
|
||||
{ // 替换main.go文件内容
|
||||
Old: mainFileMark,
|
||||
New: mainFileHTTPCode,
|
||||
},
|
||||
{ // 替换Dockerfile文件内容
|
||||
Old: dockerFileMark,
|
||||
New: dockerFileHTTPCode,
|
||||
},
|
||||
{ // 替换Dockerfile_build文件内容
|
||||
Old: dockerFileBuildMark,
|
||||
New: dockerFileBuildHTTPCode,
|
||||
},
|
||||
{ // 替换docker-compose.yml文件内容
|
||||
Old: dockerComposeFileMark,
|
||||
New: dockerComposeFileHTTPCode,
|
||||
},
|
||||
{ // 替换*-deployment.yml文件内容
|
||||
Old: k8sDeploymentFileMark,
|
||||
New: k8sDeploymentFileHTTPCode,
|
||||
},
|
||||
{ // 替换*-svc.yml文件内容
|
||||
Old: k8sServiceFileMark,
|
||||
New: k8sServiceFileHTTPCode,
|
||||
},
|
||||
// 替换github.com/zhufuyi/sponge/templates/sponge
|
||||
{
|
||||
Old: selfPackageName + "/" + r.GetSourcePath(),
|
||||
New: moduleName,
|
||||
},
|
||||
{
|
||||
Old: "github.com/zhufuyi/sponge",
|
||||
New: moduleName,
|
||||
},
|
||||
{
|
||||
Old: moduleName + "/pkg",
|
||||
New: "github.com/zhufuyi/sponge/pkg",
|
||||
},
|
||||
{
|
||||
Old: "sponge api docs",
|
||||
New: serverName + " api docs",
|
||||
},
|
||||
{
|
||||
Old: "userExampleNO = 1",
|
||||
New: fmt.Sprintf("userExampleNO = %d", rand.Intn(1000)),
|
||||
},
|
||||
{
|
||||
Old: "serverNameExample",
|
||||
New: serverName,
|
||||
},
|
||||
// docker镜像和k8s部署脚本替换
|
||||
{
|
||||
Old: "server-name-example",
|
||||
New: xstrings.ToKebabCase(serverName),
|
||||
},
|
||||
{
|
||||
Old: "projectNameExample",
|
||||
New: projectName,
|
||||
},
|
||||
// docker镜像和k8s部署脚本替换
|
||||
{
|
||||
Old: "project-name-example",
|
||||
New: xstrings.ToKebabCase(projectName),
|
||||
},
|
||||
{
|
||||
Old: "repo-addr-example",
|
||||
New: repoAddr,
|
||||
},
|
||||
{
|
||||
Old: "image-repo-host",
|
||||
New: repoHost,
|
||||
},
|
||||
{
|
||||
Old: "tmp.go.mod",
|
||||
New: "go.mod",
|
||||
},
|
||||
{
|
||||
Old: "tmp.gitignore",
|
||||
New: ".gitignore",
|
||||
},
|
||||
{
|
||||
Old: "tmp.golangci.yml",
|
||||
New: ".golangci.yml",
|
||||
},
|
||||
{
|
||||
Old: "root:123456@(192.168.3.37:3306)/account",
|
||||
New: dbDSN,
|
||||
},
|
||||
{
|
||||
Old: "UserExample",
|
||||
New: codes[parser.TableName],
|
||||
IsCaseSensitive: true,
|
||||
},
|
||||
}...)
|
||||
|
||||
return fields
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/gofile"
|
||||
"github.com/zhufuyi/sponge/pkg/replacer"
|
||||
)
|
||||
|
||||
// Replacers 模板名称对应的接口
|
||||
var Replacers = map[string]replacer.Replacer{}
|
||||
|
||||
// Template 模板信息
|
||||
type Template struct {
|
||||
Name string
|
||||
FS embed.FS
|
||||
FilePath string
|
||||
}
|
||||
|
||||
// Init 初始化模板
|
||||
func Init(name string, filepath string) error {
|
||||
// 判断模板文件是否存在,不存在,提示先更新
|
||||
if !gofile.IsExists(filepath) {
|
||||
if isShowCommand() {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`Hint: for the first time, run the command "sponge update"`)
|
||||
}
|
||||
|
||||
var err error
|
||||
if _, ok := Replacers[name]; ok {
|
||||
panic(fmt.Sprintf("template name '%s' already exists", name))
|
||||
}
|
||||
Replacers[name], err = replacer.New(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitFS 初始化FS模板
|
||||
func InitFS(name string, filepath string, fs embed.FS) {
|
||||
var err error
|
||||
if _, ok := Replacers[name]; ok {
|
||||
panic(fmt.Sprintf("template name '%s' already exists", name))
|
||||
}
|
||||
Replacers[name], err = replacer.NewFS(filepath, fs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func isShowCommand() bool {
|
||||
l := len(os.Args)
|
||||
|
||||
// sponge
|
||||
if l == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
// sponge update or sponge -h
|
||||
if l == 2 {
|
||||
if os.Args[1] == "update" || os.Args[1] == "-h" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if l > 2 {
|
||||
return strings.Contains(strings.Join(os.Args[:3], ""), "update")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/replacer"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ModelCommand generate model code
|
||||
func ModelCommand() *cobra.Command {
|
||||
var (
|
||||
outPath string // 输出目录
|
||||
sqlArgs = sql2code.Args{
|
||||
Package: "model",
|
||||
JSONTag: true,
|
||||
GormType: true,
|
||||
}
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "model",
|
||||
Short: "Generate model code",
|
||||
Long: `generate model code.
|
||||
|
||||
Examples:
|
||||
# generate model code and embed 'gorm.Model' struct.
|
||||
sponge model --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
|
||||
|
||||
# generate model code, structure fields correspond to the column names of the table.
|
||||
sponge model --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --embed=false
|
||||
|
||||
# generate model code and specify the output directory, Note: if the file already exists, code generation will be canceled.
|
||||
sponge model --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
|
||||
`,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
codes, err := sql2code.Generate(&sqlArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runGenModelCommand(codes, outPath)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
|
||||
_ = cmd.MarkFlagRequired("db-dsn")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
|
||||
_ = cmd.MarkFlagRequired("db-table")
|
||||
cmd.Flags().BoolVarP(&sqlArgs.IsEmbed, "embed", "e", true, "whether to embed 'gorm.Model' struct")
|
||||
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./model_<time>")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenModelCommand(codes map[string]string, outPath string) error {
|
||||
subTplName := "model"
|
||||
r := Replacers[TplNameSponge]
|
||||
if r == nil {
|
||||
return errors.New("replacer is nil")
|
||||
}
|
||||
|
||||
// 设置模板信息
|
||||
subDirs := []string{"internal/model"} // 只处理的指定子目录,如果为空或者没有指定的子目录,表示所有文件
|
||||
ignoreDirs := []string{} // 指定子目录下忽略处理的目录
|
||||
ignoreFiles := []string{"init.go", "init_test.go"} // 指定子目录下忽略处理的文件
|
||||
|
||||
r.SetSubDirs(subDirs...)
|
||||
r.SetIgnoreSubDirs(ignoreDirs...)
|
||||
r.SetIgnoreFiles(ignoreFiles...)
|
||||
fields := addModelFields(r, codes)
|
||||
r.SetReplacementFields(fields)
|
||||
_ = r.SetOutputDir(outPath, subTplName)
|
||||
if err := r.SaveFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("generate '%s' code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
|
||||
return nil
|
||||
}
|
||||
|
||||
func addModelFields(r replacer.Replacer, codes map[string]string) []replacer.Field {
|
||||
var fields []replacer.Field
|
||||
|
||||
fields = append(fields, deleteFieldsMark(r, modelFile, startMark, endMark)...)
|
||||
fields = append(fields, []replacer.Field{
|
||||
{ // 替换model/userExample.go文件内容
|
||||
Old: modelFileMark,
|
||||
New: codes[parser.CodeTypeModel],
|
||||
},
|
||||
{
|
||||
Old: "UserExample",
|
||||
New: codes[parser.TableName],
|
||||
IsCaseSensitive: true,
|
||||
},
|
||||
}...)
|
||||
|
||||
return fields
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/gofile"
|
||||
"github.com/zhufuyi/sponge/pkg/replacer"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ProtoCommand generate protobuf code
|
||||
func ProtoCommand() *cobra.Command {
|
||||
var (
|
||||
moduleName string // go.mod文件的module名称
|
||||
serverName string // 服务名称
|
||||
outPath string // 输出目录
|
||||
sqlArgs = sql2code.Args{}
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "proto",
|
||||
Short: "Generate protobuf code",
|
||||
Long: `generate protobuf code.
|
||||
|
||||
Examples:
|
||||
# generate protobuf code.
|
||||
sponge proto --module-name=yourModuleName --server-name=yourServerName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
|
||||
|
||||
# generate protobuf code and specify the output directory, Note: if the file already exists, code generation will be canceled.
|
||||
sponge proto --module-name=yourModuleName --server-name=yourServerName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
|
||||
`,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
codes, err := sql2code.Generate(&sqlArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runGenProtoCommand(moduleName, serverName, codes, outPath)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&moduleName, "module-name", "p", "", "module-name is the name of the module in the 'go.mod' file")
|
||||
_ = cmd.MarkFlagRequired("module-name")
|
||||
cmd.Flags().StringVarP(&serverName, "server-name", "s", "", "server name")
|
||||
_ = cmd.MarkFlagRequired("server-name")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
|
||||
_ = cmd.MarkFlagRequired("db-dsn")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
|
||||
_ = cmd.MarkFlagRequired("db-table")
|
||||
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./proto_<time>")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenProtoCommand(moduleName string, serverName string, codes map[string]string, outPath string) error {
|
||||
subTplName := "proto"
|
||||
r := Replacers[TplNameSponge]
|
||||
if r == nil {
|
||||
return errors.New("replacer is nil")
|
||||
}
|
||||
|
||||
if serverName == "" {
|
||||
serverName = moduleName
|
||||
}
|
||||
|
||||
// 设置模板信息
|
||||
subDirs := []string{"api/serverNameExample"} // 只处理的指定子目录,如果为空或者没有指定的子目录,表示所有文件
|
||||
ignoreDirs := []string{} // 指定子目录下忽略处理的目录
|
||||
ignoreFiles := []string{"userExample.pb.go", "userExample.pb.validate.go",
|
||||
"userExample_grpc.pb.go"} // 指定子目录下忽略处理的文件
|
||||
|
||||
r.SetSubDirs(subDirs...)
|
||||
r.SetIgnoreSubDirs(ignoreDirs...)
|
||||
r.SetIgnoreFiles(ignoreFiles...)
|
||||
fields := addProtoFields(moduleName, serverName, r, codes)
|
||||
r.SetReplacementFields(fields)
|
||||
_ = r.SetOutputDir(outPath, subTplName)
|
||||
if err := r.SaveFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("generate '%s' code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
|
||||
return nil
|
||||
}
|
||||
|
||||
func addProtoFields(moduleName string, serverName string, r replacer.Replacer, codes map[string]string) []replacer.Field {
|
||||
var fields []replacer.Field
|
||||
|
||||
fields = append(fields, deleteFieldsMark(r, protoFile, startMark, endMark)...)
|
||||
fields = append(fields, []replacer.Field{
|
||||
{ // 替换v1/userExample.proto文件内容
|
||||
Old: protoFileMark,
|
||||
New: codes[parser.CodeTypeProto],
|
||||
},
|
||||
{
|
||||
Old: "github.com/zhufuyi/sponge",
|
||||
New: moduleName,
|
||||
},
|
||||
// 替换目录名称
|
||||
{
|
||||
Old: strings.Join([]string{"api", "serverNameExample", "v1"}, gofile.GetPathDelimiter()),
|
||||
New: strings.Join([]string{"api", serverName, "v1"}, gofile.GetPathDelimiter()),
|
||||
},
|
||||
{
|
||||
Old: "api/serverNameExample/v1",
|
||||
New: fmt.Sprintf("api/%s/v1", serverName),
|
||||
},
|
||||
{
|
||||
Old: "api.serverNameExample.v1",
|
||||
New: fmt.Sprintf("api.%s.v1", strings.ReplaceAll(serverName, "-", "_")), // proto package 不能存在"-"号
|
||||
},
|
||||
{
|
||||
Old: "UserExample",
|
||||
New: codes[parser.TableName],
|
||||
IsCaseSensitive: true,
|
||||
},
|
||||
}...)
|
||||
|
||||
return fields
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/gofile"
|
||||
"github.com/zhufuyi/sponge/pkg/replacer"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code"
|
||||
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ServiceCommand generate service code
|
||||
func ServiceCommand() *cobra.Command {
|
||||
var (
|
||||
moduleName string // go.mod文件的module名称
|
||||
serverName string // 服务名称
|
||||
outPath string // 输出目录
|
||||
sqlArgs = sql2code.Args{
|
||||
Package: "model",
|
||||
JSONTag: true,
|
||||
GormType: true,
|
||||
}
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Generate grpc service code",
|
||||
Long: `generate grpc service code.
|
||||
|
||||
Examples:
|
||||
# generate service code and embed 'gorm.model' struct.
|
||||
sponge service --module-name=yourModuleName --server-name=yourServerName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
|
||||
|
||||
# generate service code, structure fields correspond to the column names of the table.
|
||||
sponge service --module-name=yourModuleName --server-name=yourServerName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --embed=false
|
||||
|
||||
# generate service code and specify the output directory, Note: if the file already exists, code generation will be canceled.
|
||||
sponge service --module-name=yourModuleName --server-name=yourServerName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
|
||||
`,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
codes, err := sql2code.Generate(&sqlArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runGenServiceCommand(moduleName, serverName, codes, outPath)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&moduleName, "module-name", "p", "", "module-name is the name of the module in the 'go.mod' file")
|
||||
_ = cmd.MarkFlagRequired("module-name")
|
||||
cmd.Flags().StringVarP(&serverName, "server-name", "s", "", "server name")
|
||||
_ = cmd.MarkFlagRequired("server-name")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
|
||||
_ = cmd.MarkFlagRequired("db-dsn")
|
||||
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
|
||||
_ = cmd.MarkFlagRequired("db-table")
|
||||
cmd.Flags().BoolVarP(&sqlArgs.IsEmbed, "embed", "e", true, "whether to embed 'gorm.Model' struct")
|
||||
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./service_<time>")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenServiceCommand(moduleName string, serverName string, codes map[string]string, outPath string) error {
|
||||
subTplName := "service"
|
||||
r := Replacers[TplNameSponge]
|
||||
if r == nil {
|
||||
return errors.New("replacer is nil")
|
||||
}
|
||||
|
||||
if serverName == "" {
|
||||
serverName = moduleName
|
||||
}
|
||||
|
||||
// 设置模板信息
|
||||
subDirs := []string{"internal/model", "internal/cache", "internal/dao",
|
||||
"internal/ecode", "internal/service", "api/serverNameExample"} // 只处理的指定子目录,如果为空或者没有指定的子目录,表示所有文件
|
||||
ignoreDirs := []string{} // 指定子目录下忽略处理的目录
|
||||
ignoreFiles := []string{"init.go", "init_test.go", "http_systemCode.go", "http_userExample.go",
|
||||
"grpc_systemCode.go", "grpc_systemCode_test.go", "service.go", "service_test.go", "userExample.pb.go",
|
||||
"userExample.pb.validate.go", "userExample_grpc.pb.go"} // 指定子目录下忽略处理的文件
|
||||
|
||||
r.SetSubDirs(subDirs...)
|
||||
r.SetIgnoreSubDirs(ignoreDirs...)
|
||||
r.SetIgnoreFiles(ignoreFiles...)
|
||||
fields := addServiceFields(moduleName, serverName, r, codes)
|
||||
r.SetReplacementFields(fields)
|
||||
_ = r.SetOutputDir(outPath, subTplName)
|
||||
if err := r.SaveFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("generate '%s' code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
|
||||
return nil
|
||||
}
|
||||
|
||||
func addServiceFields(moduleName string, serverName string, r replacer.Replacer, codes map[string]string) []replacer.Field {
|
||||
var fields []replacer.Field
|
||||
|
||||
fields = append(fields, deleteFieldsMark(r, modelFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, daoFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, daoTestFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, protoFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, serviceClientFile, startMark, endMark)...)
|
||||
fields = append(fields, deleteFieldsMark(r, serviceTestFile, startMark, endMark)...)
|
||||
fields = append(fields, []replacer.Field{
|
||||
{ // 替换model/userExample.go文件内容
|
||||
Old: modelFileMark,
|
||||
New: codes[parser.CodeTypeModel],
|
||||
},
|
||||
{ // 替换dao/userExample.go文件内容
|
||||
Old: daoFileMark,
|
||||
New: codes[parser.CodeTypeDAO],
|
||||
},
|
||||
{ // 替换v1/userExample.proto文件内容
|
||||
Old: protoFileMark,
|
||||
New: codes[parser.CodeTypeProto],
|
||||
},
|
||||
{ // 替换service/userExample_client_test.go文件内容
|
||||
Old: serviceFileMark,
|
||||
New: adjustmentOfIDType(codes[parser.CodeTypeService]),
|
||||
},
|
||||
{
|
||||
Old: selfPackageName + "/" + r.GetSourcePath(),
|
||||
New: moduleName,
|
||||
},
|
||||
{
|
||||
Old: "github.com/zhufuyi/sponge",
|
||||
New: moduleName,
|
||||
},
|
||||
// 替换目录名称
|
||||
{
|
||||
Old: strings.Join([]string{"api", "serverNameExample", "v1"}, gofile.GetPathDelimiter()),
|
||||
New: strings.Join([]string{"api", serverName, "v1"}, gofile.GetPathDelimiter()),
|
||||
},
|
||||
{
|
||||
Old: "api/serverNameExample/v1",
|
||||
New: fmt.Sprintf("api/%s/v1", serverName),
|
||||
},
|
||||
{
|
||||
Old: "api.serverNameExample.v1",
|
||||
New: fmt.Sprintf("api.%s.v1", strings.ReplaceAll(serverName, "-", "_")), // proto package 不能存在"-"号
|
||||
},
|
||||
{
|
||||
Old: "userExampleNO = 1",
|
||||
New: fmt.Sprintf("userExampleNO = %d", rand.Intn(1000)),
|
||||
},
|
||||
{
|
||||
Old: moduleName + "/pkg",
|
||||
New: "github.com/zhufuyi/sponge/pkg",
|
||||
},
|
||||
{
|
||||
Old: "UserExample",
|
||||
New: codes[parser.TableName],
|
||||
IsCaseSensitive: true,
|
||||
},
|
||||
}...)
|
||||
|
||||
return fields
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
package generate
|
||||
|
||||
const (
|
||||
mainFileHTTPCode = `func registerServers() []app.IServer {
|
||||
var servers []app.IServer
|
||||
|
||||
// 创建http服务
|
||||
httpAddr := ":" + strconv.Itoa(config.Get().HTTP.Port)
|
||||
httpServer := server.NewHTTPServer(httpAddr,
|
||||
server.WithHTTPReadTimeout(time.Second*time.Duration(config.Get().HTTP.ReadTimeout)),
|
||||
server.WithHTTPWriteTimeout(time.Second*time.Duration(config.Get().HTTP.WriteTimeout)),
|
||||
server.WithHTTPIsProd(config.Get().App.Env == "prod"),
|
||||
)
|
||||
servers = append(servers, httpServer)
|
||||
|
||||
return servers
|
||||
}`
|
||||
|
||||
mainFileGrpcCode = `func registerServers() []app.IServer {
|
||||
var servers []app.IServer
|
||||
|
||||
// 创建grpc服务
|
||||
grpcAddr := ":" + strconv.Itoa(config.Get().Grpc.Port)
|
||||
grpcServer := server.NewGRPCServer(grpcAddr, grpcOptions()...)
|
||||
servers = append(servers, grpcServer)
|
||||
|
||||
return servers
|
||||
}
|
||||
|
||||
func grpcOptions() []server.GRPCOption {
|
||||
var opts []server.GRPCOption
|
||||
|
||||
if config.Get().App.EnableRegistryDiscovery {
|
||||
iRegistry, instance := getETCDRegistry(
|
||||
config.Get().Etcd.Addrs,
|
||||
config.Get().App.Name,
|
||||
[]string{fmt.Sprintf("grpc://%s:%d", config.Get().App.Host, config.Get().Grpc.Port)},
|
||||
)
|
||||
opts = append(opts, server.WithRegistry(iRegistry, instance))
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func getETCDRegistry(etcdEndpoints []string, instanceName string, instanceEndpoints []string) (registry.Registry, *registry.ServiceInstance) {
|
||||
serviceInstance := registry.NewServiceInstance(instanceName, instanceEndpoints)
|
||||
|
||||
cli, err := clientv3.New(clientv3.Config{
|
||||
Endpoints: etcdEndpoints,
|
||||
DialTimeout: 5 * time.Second,
|
||||
DialOptions: []grpc.DialOption{
|
||||
grpc.WithBlock(),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
iRegistry := etcd.New(cli)
|
||||
|
||||
return iRegistry, serviceInstance
|
||||
}`
|
||||
|
||||
dockerFileHTTPCode = `# 添加curl,用在http服务的检查,如果用部署在k8s,可以不用安装
|
||||
RUN apk add curl
|
||||
|
||||
COPY configs/ /app/configs/
|
||||
COPY serverNameExample /app/serverNameExample
|
||||
RUN chmod +x /app/serverNameExample
|
||||
|
||||
# http端口
|
||||
EXPOSE 8080`
|
||||
|
||||
dockerFileGrpcCode = `# 添加grpc_health_probe,用在grpc服务的健康检查
|
||||
COPY grpc_health_probe /bin/grpc_health_probe
|
||||
RUN chmod +x /bin/grpc_health_probe
|
||||
|
||||
COPY configs/ /app/configs/
|
||||
COPY serverNameExample /app/serverNameExample
|
||||
RUN chmod +x /app/serverNameExample`
|
||||
|
||||
dockerFileBuildHTTPCode = `# 添加curl,用在http服务的检查,如果用部署在k8s,可以不用安装
|
||||
RUN apk add curl
|
||||
|
||||
COPY --from=build /serverNameExample /app/serverNameExample
|
||||
COPY --from=build /go/src/serverNameExample/configs/serverNameExample.yml /app/configs/serverNameExample.yml
|
||||
|
||||
# http端口
|
||||
EXPOSE 8080`
|
||||
|
||||
dockerFileBuildGrpcCode = `# 添加grpc_health_probe,用在grpc服务的健康检查
|
||||
COPY --from=build /grpc_health_probe /bin/grpc_health_probe
|
||||
COPY --from=build /serverNameExample /app/serverNameExample
|
||||
COPY --from=build /go/src/serverNameExample/configs/serverNameExample.yml /app/configs/serverNameExample.yml`
|
||||
|
||||
dockerComposeFileHTTPCode = ` ports:
|
||||
- "8080:8080" # http端口
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"] # http健康检查,注:镜像必须包含curl命令`
|
||||
|
||||
dockerComposeFileGrpcCode = `
|
||||
ports:
|
||||
- "8282:8282" # grpc服务端口
|
||||
- "9082:9082" # grpc metrics端口
|
||||
healthcheck:
|
||||
test: ["CMD", "grpc_health_probe", "-addr=localhost:8282"] # grpc健康检查,注:镜像必须包含grpc_health_probe命令`
|
||||
|
||||
k8sDeploymentFileHTTPCode = `
|
||||
ports:
|
||||
- name: http-port
|
||||
containerPort: 8080
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
port: http-port
|
||||
path: /health
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 2
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
port: http-port
|
||||
path: /health`
|
||||
|
||||
k8sDeploymentFileGrpcCode = `
|
||||
ports:
|
||||
- name: grpc-port
|
||||
containerPort: 8282
|
||||
- name: metrics-port
|
||||
containerPort: 9082
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:8282"]
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 2
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:8282"]`
|
||||
|
||||
k8sServiceFileHTTPCode = ` ports:
|
||||
- name: server-name-example-svc-http-port
|
||||
port: 8080
|
||||
targetPort: 8080`
|
||||
|
||||
k8sServiceFileGrpcCode = ` ports:
|
||||
- name: server-name-example-svc-grpc-port
|
||||
port: 8282
|
||||
targetPort: 8282
|
||||
- name: server-name-example-svc-grpc-metrics-port
|
||||
port: 9082
|
||||
targetPort: 9082`
|
||||
|
||||
configFileCode = `// nolint
|
||||
// code generated by sponge.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/zhufuyi/sponge/pkg/conf"
|
||||
)
|
||||
|
||||
var config *Config
|
||||
|
||||
func Init(configFile string, fs ...func()) error {
|
||||
config = &Config{}
|
||||
return conf.Parse(configFile, config, fs...)
|
||||
}
|
||||
|
||||
func Show() string {
|
||||
return conf.Show(config)
|
||||
}
|
||||
|
||||
func Get() *Config {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func Set(conf *Config) {
|
||||
config = conf
|
||||
}
|
||||
`
|
||||
|
||||
configFileCcCode = `// nolint
|
||||
// code generated by sponge.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/zhufuyi/sponge/pkg/conf"
|
||||
)
|
||||
|
||||
func NewCenter(configFile string) (*Center, error) {
|
||||
nacosConf := &Center{}
|
||||
err := conf.Parse(configFile, nacosConf)
|
||||
return nacosConf, err
|
||||
}
|
||||
`
|
||||
)
|
|
@ -0,0 +1,34 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/zhufuyi/sponge/cmd/sponge/commands/generate"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Version 命令版本号
|
||||
const Version = "0.0.0"
|
||||
|
||||
// NewRootCMD 命令入口
|
||||
func NewRootCMD() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "sponge",
|
||||
Long: "sponge management tools",
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
Version: Version,
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
generate.ModelCommand(),
|
||||
generate.DaoCommand(),
|
||||
generate.HandlerCommand(),
|
||||
generate.HTTPCommand(),
|
||||
generate.ProtoCommand(),
|
||||
generate.ServiceCommand(),
|
||||
generate.GRPCCommand(),
|
||||
generate.ConfigCommand(),
|
||||
UpdateCommand(),
|
||||
)
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/gobash"
|
||||
"github.com/zhufuyi/sponge/pkg/gofile"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// UpdateCommand update sponge binaries
|
||||
func UpdateCommand() *cobra.Command {
|
||||
var executor string
|
||||
var enableCNGoProxy bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update sponge to the latest version",
|
||||
Long: `update sponge to the latest version.
|
||||
|
||||
Examples:
|
||||
# for linux
|
||||
sponge update
|
||||
# for windows, need to specify the bash file
|
||||
sponge update --executor="D:\Program Files\cmder\vendor\git-for-windows\bin\bash.exe"
|
||||
# use https://goproxy.cn goproxy
|
||||
sponge update -g
|
||||
`,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if executor != "" {
|
||||
gobash.SetExecutorPath(executor)
|
||||
}
|
||||
err := runUpdateCommand(enableCNGoProxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = CopyToTempDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("update sponge successfully.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&executor, "executor", "e", "", "for windows systems, you need to specify the bash executor path.")
|
||||
cmd.Flags().BoolVarP(&enableCNGoProxy, "enable-cn-goproxy", "g", false, "is $GOPROXY turn on 'https://goproxy.cn'")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUpdateCommand(enableCNGoProxy bool) error {
|
||||
fmt.Println("update sponge ......")
|
||||
ctx, _ := context.WithTimeout(context.Background(), time.Minute*5) //nolint
|
||||
command := "go install github.com/zhufuyi/sponge/cmd/sponge@latest"
|
||||
if enableCNGoProxy {
|
||||
command = "GOPROXY=https://goproxy.cn,direct && " + command
|
||||
}
|
||||
result := gobash.Run(ctx, command)
|
||||
for v := range result.StdOut {
|
||||
fmt.Printf("%s", v)
|
||||
}
|
||||
if result.Err != nil {
|
||||
return fmt.Errorf("exec command failed, %v", result.Err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyToTempDir 复制模板文件到临时目录下
|
||||
func CopyToTempDir() error {
|
||||
result, err := gobash.Exec("go env GOPATH")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Exec() error %v", err)
|
||||
}
|
||||
gopath := strings.ReplaceAll(string(result), "\n", "")
|
||||
if gopath == "" {
|
||||
return fmt.Errorf("$GOPATH is empty, you need set $GOPATH in your $PATH")
|
||||
}
|
||||
|
||||
// 找出新版本sponge代码文件夹
|
||||
command := "ls $(go env GOPATH)/pkg/mod/github.com/zhufuyi | grep sponge@ | sort -r | head -1"
|
||||
result, err = gobash.Exec(command)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Exec() error %v", err)
|
||||
}
|
||||
latestSpongeDirName := strings.ReplaceAll(string(result), "\n", "")
|
||||
if latestSpongeDirName == "" {
|
||||
return fmt.Errorf("not found 'sponge' directory in '$GOPATH/pkg/mod/github.com/zhufuyi'")
|
||||
}
|
||||
srcDir := fmt.Sprintf("%s/pkg/mod/github.com/zhufuyi/%s", gopath, latestSpongeDirName)
|
||||
destDir := os.TempDir() + "/sponge"
|
||||
|
||||
// 复制到临时目录
|
||||
_ = os.RemoveAll(adaptPathDelimiter(destDir))
|
||||
command = fmt.Sprintf(`cp -rf %s %s`, adaptPathDelimiter(srcDir), adaptPathDelimiter(destDir))
|
||||
_, err = gobash.Exec(command)
|
||||
return err
|
||||
}
|
||||
|
||||
func adaptPathDelimiter(filePath string) string {
|
||||
if gofile.IsWindows() {
|
||||
filePath = strings.ReplaceAll(filePath, "\\", "/")
|
||||
}
|
||||
return filePath
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/zhufuyi/sponge/cmd/sponge/commands"
|
||||
"github.com/zhufuyi/sponge/cmd/sponge/commands/generate"
|
||||
"github.com/zhufuyi/sponge/pkg/gofile"
|
||||
)
|
||||
|
||||
/*
|
||||
//go:embed templates/sponge
|
||||
var microServiceFS embed.FS
|
||||
*/
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// 初始化模板,执行命令需要依赖真实文件
|
||||
err := generate.Init(generate.TplNameSponge, os.TempDir()+gofile.GetPathDelimiter()+"sponge")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 初始FS化模板,执行命令不需要依赖文件
|
||||
//replacer.InitFS(gen.TplNameSponge, "templates/sponge", microServiceFS)
|
||||
|
||||
rootCMD := commands.NewRootCMD()
|
||||
if err := rootCMD.Execute(); err != nil {
|
||||
rootCMD.PrintErrln("Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -4,12 +4,13 @@
|
|||
# app 设置
|
||||
app:
|
||||
name: "serverNameExample" # 服务名称
|
||||
env: "dev" # 运行环境,dev:开发环境,prod:生产环境,pre:预生产环境
|
||||
env: "dev" # 运行环境,dev:开发环境,prod:生产环境,test:测试环境
|
||||
version: "v0.0.0" # 版本
|
||||
host: "127.0.0.1" # 主机名称或ip
|
||||
enableProfile: false # 是否开启性能分析功能,true:开启,false:关闭
|
||||
enableMetrics: true # 是否开启指标采集,true:开启,false:关闭
|
||||
enableLimit: false # 是否开启限流,true:开启,false:关闭
|
||||
enableCircuitBreaker: false # 是否开启熔断,true:开启,false:关闭
|
||||
enableTracing: false # 是否开启链路跟踪,true:开启,false:关闭
|
||||
enableRegistryDiscovery: false # 是否开启注册与发现,true:开启,false:关闭
|
||||
|
||||
|
@ -34,12 +35,6 @@ logger:
|
|||
level: "info" # 输出日志级别 debug, info, warn, error,默认是debug
|
||||
format: "console" # 输出格式,console或json,默认是console
|
||||
isSave: false # false:输出到终端,true:输出到文件,默认是false
|
||||
logFileConfig: # isSave=true时有效
|
||||
filename: "out.log" # 文件名称,默认值out.log
|
||||
maxSize: 20 # 最大文件大小(MB),默认值10MB
|
||||
maxBackups: 50 # 保留旧文件的最大个数,默认值100个
|
||||
maxAge: 15 # 保留旧文件的最大天数,默认值30天
|
||||
isCompression: true # 是否压缩/归档旧文件,默认值false
|
||||
|
||||
|
||||
# mysql 设置
|
||||
|
@ -72,10 +67,3 @@ jaeger:
|
|||
# etcd 配置
|
||||
etcd:
|
||||
addrs: ["192.168.3.37:2379"]
|
||||
|
||||
|
||||
# limit 配置
|
||||
rateLimiter:
|
||||
dimension: "path" # 限流维度,支持path和ip两种,默认是path
|
||||
qpsLimit: 1000 # 持续每秒允许成功请求数,默认是500
|
||||
maxLimit: 2000 # 瞬时最大允许峰值,默认是1000,通常大于qpsLimit
|
||||
|
|
11
doc.go
11
doc.go
|
@ -1,8 +1,6 @@
|
|||
// Package sponge is a go microservices framework, a tool for quickly creating complete microservices code for http or grpc.
|
||||
// Generate `config`, `ecode`, `model`, `dao`, `handler`, `router`, `http`, `proto`, `service`, `grpc` code from the SQL DDL,
|
||||
// which can be combined into full services(similar to how a broken sponge cell automatically reorganises itself into a new sponge).
|
||||
//
|
||||
// combined with the [sponge](https://github.com/zhufuyi/sponge@sponge) tool to generate framework code。
|
||||
// Package sponge is a microservices framework for quickly creating http or grpc code.
|
||||
// Generate codes `config`, `ecode`, `model`, `dao`, `handler`, `router`, `http`, `proto`, `service`, `grpc` from the SQL DDL,
|
||||
// these codes can be combined into complete services (similar to how a broken sponge cell can automatically reorganize into a new sponge).
|
||||
//
|
||||
// sponge -h
|
||||
// sponge management tools
|
||||
|
@ -12,7 +10,7 @@
|
|||
//
|
||||
// Available Commands:
|
||||
// completion Generate the autocompletion script for the specified shell
|
||||
// config Generate go config code
|
||||
// config Generate go config code from yaml file
|
||||
// dao Generate dao code
|
||||
// grpc Generate grpc server code
|
||||
// handler Generate handler code
|
||||
|
@ -21,4 +19,5 @@
|
|||
// model Generate model code
|
||||
// proto Generate protobuf code
|
||||
// service Generate grpc service code
|
||||
// update Update sponge to the latest version
|
||||
package sponge
|
||||
|
|
|
@ -187,7 +187,7 @@ const docTemplate = `{
|
|||
},
|
||||
"/api/v1/userExamples/ids": {
|
||||
"post": {
|
||||
"description": "使用post请求,根据id数组获取userExample列表",
|
||||
"description": "使用post请求,根据多个id获取userExample列表",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -197,7 +197,7 @@ const docTemplate = `{
|
|||
"tags": [
|
||||
"userExample"
|
||||
],
|
||||
"summary": "根据id数组获取userExample列表",
|
||||
"summary": "根据多个id获取userExample列表",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "id 数组",
|
||||
|
@ -332,6 +332,7 @@ const docTemplate = `{
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"ids": {
|
||||
"description": "id列表",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
|
|
|
@ -183,7 +183,7 @@
|
|||
},
|
||||
"/api/v1/userExamples/ids": {
|
||||
"post": {
|
||||
"description": "使用post请求,根据id数组获取userExample列表",
|
||||
"description": "使用post请求,根据多个id获取userExample列表",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -193,7 +193,7 @@
|
|||
"tags": [
|
||||
"userExample"
|
||||
],
|
||||
"summary": "根据id数组获取userExample列表",
|
||||
"summary": "根据多个id获取userExample列表",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "id 数组",
|
||||
|
@ -328,6 +328,7 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"ids": {
|
||||
"description": "id列表",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
|
|
|
@ -51,6 +51,7 @@ definitions:
|
|||
types.GetUserExamplesByIDsRequest:
|
||||
properties:
|
||||
ids:
|
||||
description: id列表
|
||||
items:
|
||||
type: integer
|
||||
minItems: 1
|
||||
|
@ -234,7 +235,7 @@ paths:
|
|||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 使用post请求,根据id数组获取userExample列表
|
||||
description: 使用post请求,根据多个id获取userExample列表
|
||||
parameters:
|
||||
- description: id 数组
|
||||
in: body
|
||||
|
@ -249,7 +250,7 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/types.Result'
|
||||
summary: 根据id数组获取userExample列表
|
||||
summary: 根据多个id获取userExample列表
|
||||
tags:
|
||||
- userExample
|
||||
/health:
|
||||
|
|
30
go.mod
30
go.mod
|
@ -4,8 +4,8 @@ go 1.19
|
|||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5
|
||||
github.com/alicebob/miniredis/v2 v2.23.0
|
||||
github.com/blastrain/vitess-sqlparser v0.0.0-20201030050434-a139afbb1aba
|
||||
github.com/bojand/ghz v0.110.0
|
||||
github.com/dgraph-io/ristretto v0.1.0
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.2
|
||||
|
@ -13,23 +13,25 @@ require (
|
|||
github.com/gin-contrib/cors v1.3.1
|
||||
github.com/gin-contrib/pprof v1.4.0
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
github.com/go-kratos/aegis v0.1.3
|
||||
github.com/go-playground/validator/v10 v10.11.0
|
||||
github.com/go-redis/redis/extra/redisotel v0.3.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/golang/protobuf v1.5.2
|
||||
github.com/golang/snappy v0.0.3
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
|
||||
github.com/gunerhuseyin/goprometheus v0.0.2
|
||||
github.com/hashicorp/consul/api v1.12.0
|
||||
github.com/huandu/xstrings v1.3.1
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/jinzhu/inflection v1.0.0
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.1.0
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad
|
||||
github.com/spf13/cast v1.5.0
|
||||
github.com/spf13/cobra v1.4.0
|
||||
github.com/spf13/viper v1.12.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
|
||||
|
@ -47,9 +49,9 @@ require (
|
|||
go.opentelemetry.io/otel/trace v1.9.0
|
||||
go.uber.org/zap v1.21.0
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
|
||||
google.golang.org/grpc v1.48.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.3.5
|
||||
gorm.io/gorm v1.23.8
|
||||
)
|
||||
|
@ -57,21 +59,19 @@ require (
|
|||
require (
|
||||
cloud.google.com/go/compute v1.6.1 // indirect
|
||||
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||
github.com/DataDog/datadog-go v4.8.3+incompatible // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 // indirect
|
||||
github.com/armon/go-metrics v0.3.10 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cactus/go-statsd-client/statsd v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
|
||||
|
@ -86,6 +86,7 @@ require (
|
|||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.5 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
|
@ -93,7 +94,6 @@ require (
|
|||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-redis/redis/extra/rediscmd v0.2.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/goccy/go-json v0.9.7 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||
|
@ -107,13 +107,14 @@ require (
|
|||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/serf v0.9.7 // indirect
|
||||
github.com/imdario/mergo v0.3.11 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jhump/protoreflect v1.9.0 // indirect
|
||||
github.com/jinzhu/configor v1.1.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/juju/errors v0.0.0-20170703010042-c7d06af17c68 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
|
@ -133,13 +134,14 @@ require (
|
|||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.21.8 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/smartystreets/goconvey v1.7.2 // indirect
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.3.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.9 // indirect
|
||||
github.com/tklauser/numcpus v0.3.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.1.15 // indirect
|
||||
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect
|
||||
|
@ -153,15 +155,11 @@ require (
|
|||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.9.0
|
||||
|
||||
replace github.com/cactus/go-statsd-client/statsd => github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c
|
||||
|
|
63
go.sum
63
go.sum
|
@ -61,8 +61,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
|||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bpDIRRV4/gUtIBjh8Q=
|
||||
github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
|
@ -71,15 +69,13 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I
|
|||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
|
||||
github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
|
||||
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
||||
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
|
||||
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
|
@ -108,12 +104,12 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
|
|||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/blastrain/vitess-sqlparser v0.0.0-20201030050434-a139afbb1aba h1:hBK2BWzm0OzYZrZy9yzvZZw59C5Do4/miZ8FhEwd5P8=
|
||||
github.com/blastrain/vitess-sqlparser v0.0.0-20201030050434-a139afbb1aba/go.mod h1:FGQp+RNQwVmLzDq6HBrYCww9qJQyNwH9Qji/quTQII4=
|
||||
github.com/bojand/ghz v0.110.0 h1:P7G26B573UeC+XvRevse5M0tP8cmkG0EQm/kS/eHUjM=
|
||||
github.com/bojand/ghz v0.110.0/go.mod h1:pej2JQkTDjMckjsPsIwSVjuup2ZWr0hD/4vUrtGQm18=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c h1:HIGF0r/56+7fuIZw2V4isE22MK6xpxWx7BbV8dJ290w=
|
||||
github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
|
@ -144,6 +140,7 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
|||
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
|
@ -185,10 +182,8 @@ github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d
|
|||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
|
||||
github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
|
@ -199,6 +194,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
|||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-kratos/aegis v0.1.3 h1:hXw/iO51ofO0hSRBijbmGNidthfAo9Oc/PZSl/lRCxk=
|
||||
github.com/go-kratos/aegis v0.1.3/go.mod h1:jYeSQ3Gesba478zEnujOiG5QdsyF3Xk/8owFUeKcHxw=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
|
@ -208,6 +205,8 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
|||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
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-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
|
@ -301,7 +300,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
|
@ -334,7 +332,6 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0
|
|||
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
|
||||
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
|
||||
|
@ -342,8 +339,6 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2
|
|||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/gunerhuseyin/goprometheus v0.0.2 h1:CQNUdyoGmBPDBX3mTIEUo+jcl+uwe/ISxxaSbJlGsP8=
|
||||
github.com/gunerhuseyin/goprometheus v0.0.2/go.mod h1:Nd+CBNGDNC3Qrx7NMuS3cAEfI99HZp7kcqFiMfK1TAg=
|
||||
github.com/hashicorp/consul/api v1.12.0 h1:k3y1FYv6nuKyNTqj6w9gXOx5r5CfLj/k/euUeBXj1OY=
|
||||
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
|
||||
github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU=
|
||||
|
@ -395,6 +390,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
|||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jhump/protoreflect v1.9.0 h1:npqHz788dryJiR/l6K/RUQAyh2SwV91+d1dnh4RjO9w=
|
||||
github.com/jhump/protoreflect v1.9.0/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg=
|
||||
github.com/jinzhu/configor v1.1.1 h1:gntDP+ffGhs7aJ0u8JvjCDts2OsxsI7bnz3q+jC+hSY=
|
||||
|
@ -421,8 +418,13 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
|||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/juju/errors v0.0.0-20170703010042-c7d06af17c68 h1:d2hBkTvi7B89+OXY8+bBBshPlc+7JYacGrG/dFak8SQ=
|
||||
github.com/juju/errors v0.0.0-20170703010042-c7d06af17c68/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
|
||||
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 h1:UUHMLvzt/31azWTN/ifGWef4WUqvXk0iRqdhdy/2uzI=
|
||||
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
|
||||
github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b h1:Rrp0ByJXEjhREMPGTt3aWYjoIsUGCbt21ekbeJcTWv0=
|
||||
github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
|
@ -457,7 +459,6 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
|
|||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
|
@ -566,32 +567,26 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
|||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad h1:WtSUHi5zthjudjIi3L6QmL/V9vpJPbc/j/F2u55d3fs=
|
||||
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad/go.mod h1:h0+DiDRe2Y+6iHTjIq/9HzUq7NII/Nffp0HkFrsAKq4=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shirou/gopsutil/v3 v3.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA=
|
||||
github.com/shirou/gopsutil/v3 v3.21.8/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
||||
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
|
@ -600,6 +595,8 @@ github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfA
|
|||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
|
||||
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
|
@ -628,8 +625,11 @@ github.com/swaggo/gin-swagger v1.5.2 h1:dj2es17EaOHoy0Owu4xn3An1mI8/xjdFyIH6KAbO
|
|||
github.com/swaggo/gin-swagger v1.5.2/go.mod h1:Cbj/MlHApPOjZdf4joWFXLLgmZVPyh54GPvPPyVjVZM=
|
||||
github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI=
|
||||
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
||||
github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo=
|
||||
github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs=
|
||||
github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ=
|
||||
github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
|
@ -667,14 +667,19 @@ go.opentelemetry.io/contrib v1.9.0 h1:2KAoCVu4OMI9TYoSWvcV7+UbbIPOi4623S77nV+M/K
|
|||
go.opentelemetry.io/contrib v1.9.0/go.mod h1:yp0N4+hnpWCpnMzs6T6WbD9Amfg7reEZsS0jAd/5M2Q=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.34.0 h1:PNEMW4EvpNQ7SuoPFNkvbZqi1STkTPKq+8vfoMl/6AE=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.34.0/go.mod h1:fk1+icoN47ytLSgkoWHLJrtVTSQ+HgmkNgPTKrk/Nsc=
|
||||
go.opentelemetry.io/otel v0.13.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY=
|
||||
go.opentelemetry.io/otel v0.16.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA=
|
||||
go.opentelemetry.io/otel v0.17.0/go.mod h1:Oqtdxmf7UtEvL037ohlgnaYa1h7GtMh0NcSd9eqkC9s=
|
||||
go.opentelemetry.io/otel v1.9.0 h1:8WZNQFIB2a71LnANS9JeyidJKKGOOremcUtb/OtHISw=
|
||||
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.9.0 h1:gAEgEVGDWwFjcis9jJTOJqZNxDzoZfR12WNIxr7g9Ww=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.9.0/go.mod h1:hquezOLVAybNW6vanIxkdLXTXvzlj2Vn3wevSP15RYs=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.9.0 h1:0uV0qzHk48i1SF8qRI8odMYiwPOLh9gBhiJFpj8H6JY=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.9.0/go.mod h1:Fl1iS5ZhWgXXXTdJMuBSVsS5nkL5XluHbg97kjOuYU4=
|
||||
go.opentelemetry.io/otel/metric v0.17.0/go.mod h1:hUz9lH1rNXyEwWAhIWCMFWKhYtpASgSnObJFnU26dJ0=
|
||||
go.opentelemetry.io/otel/metric v0.31.0 h1:6SiklT+gfWAwWUR0meEMxQBtihpiEs4c+vL9spDTqUs=
|
||||
go.opentelemetry.io/otel/metric v0.31.0/go.mod h1:ohmwj9KTSIeBnDBm/ZwH2PSZxZzoOaG2xZeekTRzL5A=
|
||||
go.opentelemetry.io/otel/oteltest v0.17.0/go.mod h1:JT/LGFxPwpN+nlsTiinSYjdIx3hZIGqHCpChcIZmdoE=
|
||||
go.opentelemetry.io/otel/sdk v1.9.0 h1:LNXp1vrr83fNXTHgU8eO89mhzxb/bbWAsHG6fNf3qWo=
|
||||
go.opentelemetry.io/otel/sdk v1.9.0/go.mod h1:AEZc8nt5bd2F7BC24J5R0mrjYnpEgYHyTcM/vrSple4=
|
||||
go.opentelemetry.io/otel/trace v0.17.0/go.mod h1:bIujpqg6ZL6xUTubIUgziI1jSaUPthmabA/ygf/6Cfg=
|
||||
|
@ -853,6 +858,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -909,6 +915,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
@ -927,6 +934,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20180302201248-b7ef84aaf62a/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
@ -1187,11 +1195,12 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
|||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
|
||||
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
|
||||
var config *Config
|
||||
|
||||
func Init(configFile string) error {
|
||||
func Init(configFile string, fs ...func()) error {
|
||||
config = &Config{}
|
||||
return conf.Parse(configFile, config)
|
||||
return conf.Parse(configFile, config, fs...)
|
||||
}
|
||||
|
||||
func Show() string {
|
||||
|
@ -30,15 +30,14 @@ func Set(conf *Config) {
|
|||
}
|
||||
|
||||
type Config struct {
|
||||
App App `yaml:"app" json:"app"`
|
||||
Etcd Etcd `yaml:"etcd" json:"etcd"`
|
||||
Grpc Grpc `yaml:"grpc" json:"grpc"`
|
||||
HTTP HTTP `yaml:"http" json:"http"`
|
||||
Jaeger Jaeger `yaml:"jaeger" json:"jaeger"`
|
||||
Logger Logger `yaml:"logger" json:"logger"`
|
||||
Mysql Mysql `yaml:"mysql" json:"mysql"`
|
||||
RateLimiter RateLimiter `yaml:"rateLimiter" json:"rateLimiter"`
|
||||
Redis Redis `yaml:"redis" json:"redis"`
|
||||
App App `yaml:"app" json:"app"`
|
||||
Etcd Etcd `yaml:"etcd" json:"etcd"`
|
||||
Grpc Grpc `yaml:"grpc" json:"grpc"`
|
||||
HTTP HTTP `yaml:"http" json:"http"`
|
||||
Jaeger Jaeger `yaml:"jaeger" json:"jaeger"`
|
||||
Logger Logger `yaml:"logger" json:"logger"`
|
||||
Mysql Mysql `yaml:"mysql" json:"mysql"`
|
||||
Redis Redis `yaml:"redis" json:"redis"`
|
||||
}
|
||||
|
||||
type Etcd struct {
|
||||
|
@ -67,13 +66,8 @@ type Redis struct {
|
|||
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
|
||||
}
|
||||
|
||||
type RateLimiter struct {
|
||||
Dimension string `yaml:"dimension" json:"dimension"`
|
||||
MaxLimit int `yaml:"maxLimit" json:"maxLimit"`
|
||||
QPSLimit int `yaml:"qpsLimit" json:"qpsLimit"`
|
||||
}
|
||||
|
||||
type App struct {
|
||||
EnableCircuitBreaker bool `yaml:"enableCircuitBreaker" json:"enableCircuitBreaker"`
|
||||
EnableLimit bool `yaml:"enableLimit" json:"enableLimit"`
|
||||
EnableMetrics bool `yaml:"enableMetrics" json:"enableMetrics"`
|
||||
EnableProfile bool `yaml:"enableProfile" json:"enableProfile"`
|
||||
|
@ -85,19 +79,10 @@ type App struct {
|
|||
Version string `yaml:"version" json:"version"`
|
||||
}
|
||||
|
||||
type LogFileConfig struct {
|
||||
Filename string `yaml:"filename" json:"filename"`
|
||||
IsCompression bool `yaml:"isCompression" json:"isCompression"`
|
||||
MaxAge int `yaml:"maxAge" json:"maxAge"`
|
||||
MaxBackups int `yaml:"maxBackups" json:"maxBackups"`
|
||||
MaxSize int `yaml:"maxSize" json:"maxSize"`
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
Format string `yaml:"format" json:"format"`
|
||||
IsSave bool `yaml:"isSave" json:"isSave"`
|
||||
Level string `yaml:"level" json:"level"`
|
||||
LogFileConfig LogFileConfig `yaml:"logFileConfig" json:"logFileConfig"`
|
||||
Format string `yaml:"format" json:"format"`
|
||||
IsSave bool `yaml:"isSave" json:"isSave"`
|
||||
Level string `yaml:"level" json:"level"`
|
||||
}
|
||||
|
||||
type Grpc struct {
|
||||
|
|
|
@ -116,7 +116,7 @@ func (d *userExampleDao) GetByID(ctx context.Context, id uint64) (*model.UserExa
|
|||
err = d.db.WithContext(ctx).Where("id = ?", id).First(table).Error
|
||||
if err != nil {
|
||||
// if data is empty, set not found cache to prevent cache penetration(防止缓存穿透)
|
||||
if err.Error() == model.ErrRecordNotFound.Error() {
|
||||
if errors.Is(err, model.ErrRecordNotFound) {
|
||||
err = d.cache.SetCacheWithNotFound(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -214,7 +214,7 @@ func (d *userExampleDao) GetByIDs(ctx context.Context, ids []uint64) ([]*model.U
|
|||
func (d *userExampleDao) GetByColumns(ctx context.Context, params *query.Params) ([]*model.UserExample, int64, error) {
|
||||
queryStr, args, err := params.ConvertToGormConditions()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, 0, errors.New("query params error: " + err.Error())
|
||||
}
|
||||
|
||||
var total int64
|
||||
|
|
|
@ -20,9 +20,10 @@ var (
|
|||
StatusForbidden = errcode.StatusForbidden
|
||||
StatusLimitExceed = errcode.StatusLimitExceed
|
||||
|
||||
StatusDeadlineExceeded = errcode.StatusDeadlineExceeded
|
||||
StatusAccessDenied = errcode.StatusAccessDenied
|
||||
StatusMethodNotAllowed = errcode.StatusMethodNotAllowed
|
||||
StatusDeadlineExceeded = errcode.StatusDeadlineExceeded
|
||||
StatusAccessDenied = errcode.StatusAccessDenied
|
||||
StatusMethodNotAllowed = errcode.StatusMethodNotAllowed
|
||||
StatusServiceUnavailable = errcode.StatusServiceUnavailable
|
||||
)
|
||||
|
||||
// Any kev-value
|
||||
|
|
|
@ -19,7 +19,8 @@ var (
|
|||
Forbidden = errcode.Forbidden
|
||||
LimitExceed = errcode.LimitExceed
|
||||
|
||||
DeadlineExceeded = errcode.DeadlineExceeded
|
||||
AccessDenied = errcode.AccessDenied
|
||||
MethodNotAllowed = errcode.MethodNotAllowed
|
||||
DeadlineExceeded = errcode.DeadlineExceeded
|
||||
AccessDenied = errcode.AccessDenied
|
||||
MethodNotAllowed = errcode.MethodNotAllowed
|
||||
MethodServiceUnavailable = errcode.MethodServiceUnavailable
|
||||
)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/zhufuyi/sponge/internal/cache"
|
||||
"github.com/zhufuyi/sponge/internal/dao"
|
||||
"github.com/zhufuyi/sponge/internal/ecode"
|
||||
|
@ -64,14 +66,14 @@ func (h *userExampleHandler) Create(c *gin.Context) {
|
|||
err = copier.Copy(userExample, form)
|
||||
if err != nil {
|
||||
logger.Warn("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||
response.Error(c, ecode.InternalServerError)
|
||||
response.Error(c, ecode.ErrCreateUserExample)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.iDao.Create(c.Request.Context(), userExample)
|
||||
if err != nil {
|
||||
logger.Error("Create error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||
response.Error(c, ecode.ErrCreateUserExample)
|
||||
response.Output(c, ecode.InternalServerError.ToHTTPCode())
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -96,7 +98,7 @@ func (h *userExampleHandler) DeleteByID(c *gin.Context) {
|
|||
err := h.iDao.DeleteByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
logger.Error("DeleteByID error", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
|
||||
response.Error(c, ecode.ErrDeleteUserExample)
|
||||
response.Output(c, ecode.InternalServerError.ToHTTPCode())
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -132,14 +134,14 @@ func (h *userExampleHandler) UpdateByID(c *gin.Context) {
|
|||
err = copier.Copy(userExample, form)
|
||||
if err != nil {
|
||||
logger.Warn("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||
response.Error(c, ecode.InternalServerError)
|
||||
response.Error(c, ecode.ErrUpdateUserExample)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.iDao.UpdateByID(c.Request.Context(), userExample)
|
||||
if err != nil {
|
||||
logger.Error("UpdateByID error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||
response.Error(c, ecode.ErrUpdateUserExample)
|
||||
response.Output(c, ecode.InternalServerError.ToHTTPCode())
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -163,12 +165,12 @@ func (h *userExampleHandler) GetByID(c *gin.Context) {
|
|||
|
||||
userExample, err := h.iDao.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
if err.Error() == query.ErrNotFound.Error() {
|
||||
if errors.Is(err, query.ErrNotFound) {
|
||||
logger.Warn("GetByID not found", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
|
||||
response.Error(c, ecode.NotFound)
|
||||
} else {
|
||||
logger.Error("GetByID error", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
|
||||
response.Error(c, ecode.ErrGetUserExample)
|
||||
response.Output(c, ecode.InternalServerError.ToHTTPCode())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -177,7 +179,7 @@ func (h *userExampleHandler) GetByID(c *gin.Context) {
|
|||
err = copier.Copy(data, userExample)
|
||||
if err != nil {
|
||||
logger.Warn("Copy error", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
|
||||
response.Error(c, ecode.InternalServerError)
|
||||
response.Error(c, ecode.ErrGetUserExample)
|
||||
return
|
||||
}
|
||||
data.ID = idStr
|
||||
|
@ -206,14 +208,15 @@ func (h *userExampleHandler) ListByIDs(c *gin.Context) {
|
|||
userExamples, err := h.iDao.GetByIDs(c.Request.Context(), form.IDs)
|
||||
if err != nil {
|
||||
logger.Error("GetByIDs error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||
response.Error(c, ecode.ErrListUserExample)
|
||||
response.Output(c, ecode.InternalServerError.ToHTTPCode())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
data, err := convertUserExamples(userExamples)
|
||||
if err != nil {
|
||||
logger.Warn("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||
response.Error(c, ecode.InternalServerError)
|
||||
response.Error(c, ecode.ErrListUserExample)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -243,14 +246,14 @@ func (h *userExampleHandler) List(c *gin.Context) {
|
|||
userExamples, total, err := h.iDao.GetByColumns(c.Request.Context(), &form.Params)
|
||||
if err != nil {
|
||||
logger.Error("GetByColumns error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||
response.Error(c, ecode.ErrListUserExample)
|
||||
response.Output(c, ecode.InternalServerError.ToHTTPCode())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := convertUserExamples(userExamples)
|
||||
if err != nil {
|
||||
logger.Warn("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||
response.Error(c, ecode.InternalServerError)
|
||||
response.Error(c, ecode.ErrListUserExample)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@ func Test_userExampleHandler_Create(t *testing.T) {
|
|||
h.MockDao.SqlMock.ExpectCommit()
|
||||
result = &gohttp.StdResult{}
|
||||
err = gohttp.Post(result, h.GetRequestURL("Create"), testData)
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, err)
|
||||
// delete the templates code end
|
||||
}
|
||||
|
||||
|
@ -155,7 +155,7 @@ func Test_userExampleHandler_DeleteByID(t *testing.T) {
|
|||
|
||||
// delete error test
|
||||
err = gohttp.Delete(result, h.GetRequestURL("DeleteByID", 111))
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_userExampleHandler_UpdateByID(t *testing.T) {
|
||||
|
@ -185,7 +185,7 @@ func Test_userExampleHandler_UpdateByID(t *testing.T) {
|
|||
|
||||
// update error test
|
||||
err = gohttp.Put(result, h.GetRequestURL("UpdateByID", 111), testData)
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_userExampleHandler_GetByID(t *testing.T) {
|
||||
|
@ -215,7 +215,7 @@ func Test_userExampleHandler_GetByID(t *testing.T) {
|
|||
|
||||
// get error test
|
||||
err = gohttp.Get(result, h.GetRequestURL("GetByID", 111))
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_userExampleHandler_ListByIDs(t *testing.T) {
|
||||
|
@ -243,7 +243,7 @@ func Test_userExampleHandler_ListByIDs(t *testing.T) {
|
|||
|
||||
// get error test
|
||||
err = gohttp.Post(result, h.GetRequestURL("ListByIDs"), &types.GetUserExamplesByIDsRequest{IDs: []uint64{111}})
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_userExampleHandler_List(t *testing.T) {
|
||||
|
|
|
@ -2,18 +2,16 @@ package routers
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/zhufuyi/sponge/docs"
|
||||
"github.com/zhufuyi/sponge/internal/config"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/gin/handlerfunc"
|
||||
"github.com/zhufuyi/sponge/pkg/gin/middleware"
|
||||
"github.com/zhufuyi/sponge/pkg/gin/middleware/metrics"
|
||||
"github.com/zhufuyi/sponge/pkg/gin/middleware/ratelimiter"
|
||||
"github.com/zhufuyi/sponge/pkg/gin/validator"
|
||||
"github.com/zhufuyi/sponge/pkg/logger"
|
||||
|
||||
"github.com/zhufuyi/sponge/docs"
|
||||
"github.com/zhufuyi/sponge/internal/config"
|
||||
|
||||
"github.com/gin-contrib/pprof"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
|
@ -53,14 +51,12 @@ func NewRouter() *gin.Engine {
|
|||
|
||||
// limit 中间件
|
||||
if config.Get().App.EnableLimit {
|
||||
opts := []ratelimiter.Option{
|
||||
ratelimiter.WithQPS(config.Get().RateLimiter.QPSLimit),
|
||||
ratelimiter.WithBurst(config.Get().RateLimiter.MaxLimit),
|
||||
}
|
||||
if strings.ToUpper(config.Get().RateLimiter.Dimension) == "IP" {
|
||||
opts = append(opts, ratelimiter.WithIP())
|
||||
}
|
||||
r.Use(ratelimiter.QPS(opts...))
|
||||
r.Use(middleware.RateLimit())
|
||||
}
|
||||
|
||||
// circuit breaker 中间件
|
||||
if config.Get().App.EnableCircuitBreaker {
|
||||
r.Use(middleware.CircuitBreaker())
|
||||
}
|
||||
|
||||
// trace 中间件
|
||||
|
|
|
@ -115,9 +115,12 @@ func (s *grpcServer) serverOptions() []grpc.ServerOption {
|
|||
|
||||
// limit 拦截器
|
||||
if config.Get().App.EnableLimit {
|
||||
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerRateLimit(
|
||||
interceptor.WithRateLimitQPS(config.Get().RateLimiter.QPSLimit),
|
||||
))
|
||||
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerRateLimit())
|
||||
}
|
||||
|
||||
// circuit breaker 拦截器
|
||||
if config.Get().App.EnableCircuitBreaker {
|
||||
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerCircuitBreaker())
|
||||
}
|
||||
|
||||
// trace 拦截器
|
||||
|
|
|
@ -2,6 +2,8 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
pb "github.com/zhufuyi/sponge/api/serverNameExample/v1"
|
||||
"github.com/zhufuyi/sponge/internal/cache"
|
||||
|
@ -53,13 +55,13 @@ func (s *userExampleService) Create(ctx context.Context, req *pb.CreateUserExamp
|
|||
err = copier.Copy(userExample, req)
|
||||
if err != nil {
|
||||
logger.Warn("copier.Copy error", logger.Err(err), logger.Any("req", req))
|
||||
return nil, ecode.StatusInternalServerError.Err()
|
||||
return nil, ecode.StatusCreateUserExample.Err()
|
||||
}
|
||||
|
||||
err = s.iDao.Create(ctx, userExample)
|
||||
if err != nil {
|
||||
logger.Error("s.iDao.Create error", logger.Err(err), logger.Any("userExample", userExample))
|
||||
return nil, ecode.StatusCreateUserExample.Err()
|
||||
return nil, ecode.StatusInternalServerError.ToRPCErr()
|
||||
}
|
||||
|
||||
return &pb.CreateUserExampleReply{Id: userExample.ID}, nil
|
||||
|
@ -76,7 +78,7 @@ func (s *userExampleService) DeleteByID(ctx context.Context, req *pb.DeleteUserE
|
|||
err = s.iDao.DeleteByID(ctx, req.Id)
|
||||
if err != nil {
|
||||
logger.Error("s.iDao.DeleteByID error", logger.Err(err), logger.Any("id", req.Id))
|
||||
return nil, ecode.StatusDeleteUserExample.Err()
|
||||
return nil, ecode.StatusInternalServerError.ToRPCErr()
|
||||
}
|
||||
|
||||
return &pb.DeleteUserExampleByIDReply{}, nil
|
||||
|
@ -94,14 +96,14 @@ func (s *userExampleService) UpdateByID(ctx context.Context, req *pb.UpdateUserE
|
|||
err = copier.Copy(userExample, req)
|
||||
if err != nil {
|
||||
logger.Warn("copier.Copy error", logger.Err(err), logger.Any("req", req))
|
||||
return nil, ecode.StatusInternalServerError.Err()
|
||||
return nil, ecode.StatusUpdateUserExample.Err()
|
||||
}
|
||||
userExample.ID = req.Id
|
||||
|
||||
err = s.iDao.UpdateByID(ctx, userExample)
|
||||
if err != nil {
|
||||
logger.Error("s.iDao.UpdateByID error", logger.Err(err), logger.Any("userExample", userExample))
|
||||
return nil, ecode.StatusUpdateUserExample.Err()
|
||||
return nil, ecode.StatusInternalServerError.ToRPCErr()
|
||||
}
|
||||
|
||||
return &pb.UpdateUserExampleByIDReply{}, nil
|
||||
|
@ -117,18 +119,18 @@ func (s *userExampleService) GetByID(ctx context.Context, req *pb.GetUserExample
|
|||
|
||||
record, err := s.iDao.GetByID(ctx, req.Id)
|
||||
if err != nil {
|
||||
if err.Error() == query.ErrNotFound.Error() {
|
||||
if errors.Is(err, query.ErrNotFound) {
|
||||
logger.Warn("s.iDao.GetByID error", logger.Err(err), logger.Any("id", req.Id))
|
||||
return nil, ecode.StatusNotFound.Err()
|
||||
}
|
||||
logger.Error("s.iDao.GetByID error", logger.Err(err), logger.Any("id", req.Id))
|
||||
return nil, ecode.StatusGetUserExample.Err()
|
||||
return nil, ecode.StatusInternalServerError.ToRPCErr()
|
||||
}
|
||||
|
||||
data, err := covertUserExample(record)
|
||||
if err != nil {
|
||||
logger.Warn("covertUserExample error", logger.Err(err), logger.Any("record", record))
|
||||
return nil, ecode.StatusInternalServerError.Err()
|
||||
return nil, ecode.StatusGetUserExample.Err()
|
||||
}
|
||||
|
||||
return &pb.GetUserExampleByIDReply{UserExample: data}, nil
|
||||
|
@ -145,7 +147,7 @@ func (s *userExampleService) ListByIDs(ctx context.Context, req *pb.ListUserExam
|
|||
records, err := s.iDao.GetByIDs(ctx, req.Ids)
|
||||
if err != nil {
|
||||
logger.Error("s.iDao.GetByID error", logger.Err(err), logger.Any("ids", req.Ids))
|
||||
return nil, ecode.StatusGetUserExample.Err()
|
||||
return nil, ecode.StatusInternalServerError.ToRPCErr()
|
||||
}
|
||||
|
||||
datas := []*pb.UserExample{}
|
||||
|
@ -173,14 +175,18 @@ func (s *userExampleService) List(ctx context.Context, req *pb.ListUserExampleRe
|
|||
err = copier.Copy(params, req.Params)
|
||||
if err != nil {
|
||||
logger.Warn("copier.Copy error", logger.Err(err), logger.Any("params", req.Params))
|
||||
return nil, ecode.StatusInternalServerError.Err()
|
||||
return nil, ecode.StatusListUserExample.Err()
|
||||
}
|
||||
params.Size = int(req.Params.Limit)
|
||||
|
||||
records, total, err := s.iDao.GetByColumns(ctx, params)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "query params error:") {
|
||||
logger.Warn("s.iDao.GetByColumns error", logger.Err(err), logger.Any("params", params))
|
||||
return nil, ecode.StatusInvalidParams.Err()
|
||||
}
|
||||
logger.Error("s.iDao.GetByColumns error", logger.Err(err), logger.Any("params", params))
|
||||
return nil, ecode.StatusListUserExample.Err()
|
||||
return nil, ecode.StatusInternalServerError.ToRPCErr()
|
||||
}
|
||||
|
||||
datas := []*pb.UserExample{}
|
||||
|
|
|
@ -29,7 +29,7 @@ func initUserExampleServiceClient() pb.UserExampleServiceClient {
|
|||
//grpccli.WithEnableLog(logger.Get()),
|
||||
//grpccli.WithDiscovery(discovery),
|
||||
//grpccli.WithEnableTrace(),
|
||||
//grpccli.WithEnableHystrix("user"),
|
||||
//grpccli.WithEnableCircuitBreaker(),
|
||||
//grpccli.WithEnableLoadBalance(),
|
||||
//grpccli.WithEnableRetry(),
|
||||
//grpccli.WithEnableMetrics(),
|
||||
|
@ -177,7 +177,7 @@ func Test_userExampleService_benchmark(t *testing.T) {
|
|||
fn: func() error {
|
||||
// todo test after filling in parameters
|
||||
message := &pb.GetUserExampleByIDRequest{
|
||||
Id: 3,
|
||||
Id: 1,
|
||||
}
|
||||
b, err := benchmark.New(host, protoFile, "GetByID", message, 1000, importPaths...)
|
||||
if err != nil {
|
||||
|
|
|
@ -41,7 +41,7 @@ func (d *userExampleDao) GetByID(ctx context.Context, id uint64) (*model.UserExa
|
|||
err := d.db.WithContext(ctx).Where("id = ?", id).First(table).Error
|
||||
if err != nil {
|
||||
// if data is empty, set not found cache to prevent cache penetration(防止缓存穿透)
|
||||
if err.Error() == model.ErrRecordNotFound.Error() {
|
||||
if errors.Is(err, model.ErrRecordNotFound) {
|
||||
err = d.cache.SetCacheWithNotFound(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -19,4 +19,4 @@ redis:
|
|||
readTimeout: 2 # 读超时,单位(秒)
|
||||
writeTimeout: 2 # 写超时,单位(秒)
|
||||
|
||||
#########
|
||||
###############
|
|
@ -0,0 +1,42 @@
|
|||
package group
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
func (c *Counter) Incr() {
|
||||
c.Value++
|
||||
}
|
||||
|
||||
func ExampleGroup_Get() {
|
||||
group := NewGroup(func() interface{} {
|
||||
fmt.Println("Only Once")
|
||||
return &Counter{}
|
||||
})
|
||||
|
||||
// Create a new Counter
|
||||
group.Get("pass").(*Counter).Incr()
|
||||
|
||||
// Get the created Counter again.
|
||||
group.Get("pass").(*Counter).Incr()
|
||||
// Output:
|
||||
// Only Once
|
||||
}
|
||||
|
||||
func ExampleGroup_Reset() {
|
||||
group := NewGroup(func() interface{} {
|
||||
return &Counter{}
|
||||
})
|
||||
|
||||
// Reset the new function and clear all created objects.
|
||||
group.Reset(func() interface{} {
|
||||
fmt.Println("reset")
|
||||
return &Counter{}
|
||||
})
|
||||
|
||||
// Create a new Counter
|
||||
group.Get("pass").(*Counter).Incr()
|
||||
// Output:reset
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// Package group provides a sample lazy load container.
|
||||
// The group only creating a new object not until the object is needed by user.
|
||||
// And it will cache all the objects to reduce the creation of object.
|
||||
package group
|
||||
|
||||
import "sync"
|
||||
|
||||
// Group is a lazy load container.
|
||||
type Group struct {
|
||||
new func() interface{}
|
||||
vals map[string]interface{}
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// NewGroup news a group container.
|
||||
func NewGroup(new func() interface{}) *Group {
|
||||
if new == nil {
|
||||
panic("container.group: can't assign a nil to the new function")
|
||||
}
|
||||
return &Group{
|
||||
new: new,
|
||||
vals: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Get gets the object by the given key.
|
||||
func (g *Group) Get(key string) interface{} {
|
||||
g.RLock()
|
||||
v, ok := g.vals[key]
|
||||
if ok {
|
||||
g.RUnlock()
|
||||
return v
|
||||
}
|
||||
g.RUnlock()
|
||||
|
||||
// slow path for group don`t have specified key value
|
||||
g.Lock()
|
||||
defer g.Unlock()
|
||||
v, ok = g.vals[key]
|
||||
if ok {
|
||||
return v
|
||||
}
|
||||
v = g.new()
|
||||
g.vals[key] = v
|
||||
return v
|
||||
}
|
||||
|
||||
// Reset resets the new function and deletes all existing objects.
|
||||
func (g *Group) Reset(new func() interface{}) {
|
||||
if new == nil {
|
||||
panic("container.group: can't assign a nil to the new function")
|
||||
}
|
||||
g.Lock()
|
||||
g.new = new
|
||||
g.Unlock()
|
||||
g.Clear()
|
||||
}
|
||||
|
||||
// Clear deletes all objects.
|
||||
func (g *Group) Clear() {
|
||||
g.Lock()
|
||||
g.vals = make(map[string]interface{})
|
||||
g.Unlock()
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGroupGet(t *testing.T) {
|
||||
count := 0
|
||||
g := NewGroup(func() interface{} {
|
||||
count++
|
||||
return count
|
||||
})
|
||||
v := g.Get("key_0")
|
||||
if !reflect.DeepEqual(v.(int), 1) {
|
||||
t.Errorf("expect 1, actual %v", v)
|
||||
}
|
||||
|
||||
v = g.Get("key_1")
|
||||
if !reflect.DeepEqual(v.(int), 2) {
|
||||
t.Errorf("expect 2, actual %v", v)
|
||||
}
|
||||
|
||||
v = g.Get("key_0")
|
||||
if !reflect.DeepEqual(v.(int), 1) {
|
||||
t.Errorf("expect 1, actual %v", v)
|
||||
}
|
||||
if !reflect.DeepEqual(count, 2) {
|
||||
t.Errorf("expect count 2, actual %v", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupReset(t *testing.T) {
|
||||
g := NewGroup(func() interface{} {
|
||||
return 1
|
||||
})
|
||||
g.Get("key")
|
||||
call := false
|
||||
g.Reset(func() interface{} {
|
||||
call = true
|
||||
return 1
|
||||
})
|
||||
|
||||
length := 0
|
||||
for range g.vals {
|
||||
length++
|
||||
}
|
||||
if !reflect.DeepEqual(length, 0) {
|
||||
t.Errorf("expect length 0, actual %v", length)
|
||||
}
|
||||
|
||||
g.Get("key")
|
||||
if !reflect.DeepEqual(call, true) {
|
||||
t.Errorf("expect call true, actual %v", call)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupClear(t *testing.T) {
|
||||
g := NewGroup(func() interface{} {
|
||||
return 1
|
||||
})
|
||||
g.Get("key")
|
||||
length := 0
|
||||
for range g.vals {
|
||||
length++
|
||||
}
|
||||
if !reflect.DeepEqual(length, 1) {
|
||||
t.Errorf("expect length 1, actual %v", length)
|
||||
}
|
||||
|
||||
g.Clear()
|
||||
length = 0
|
||||
for range g.vals {
|
||||
length++
|
||||
}
|
||||
if !reflect.DeepEqual(length, 0) {
|
||||
t.Errorf("expect length 0, actual %v", length)
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package errcode
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
@ -58,26 +59,64 @@ func (g *GRPCStatus) Err(details ...Detail) error {
|
|||
return status.Errorf(g.status.Code(), "%s details = %v", g.status.Message(), dts)
|
||||
}
|
||||
|
||||
// ToRPCCode 转换为RPC识别的错误码,避免返回Unknown状态码
|
||||
func ToRPCCode(code codes.Code) codes.Code {
|
||||
switch code {
|
||||
// ToRPCErr converted to standard RPC error
|
||||
func (g *GRPCStatus) ToRPCErr(desc ...string) error {
|
||||
switch g.status.Code() {
|
||||
case StatusInternalServerError.status.Code():
|
||||
code = codes.Internal
|
||||
return toRPCErr(codes.Internal, desc...)
|
||||
case StatusInvalidParams.status.Code():
|
||||
code = codes.InvalidArgument
|
||||
return toRPCErr(codes.InvalidArgument, desc...)
|
||||
case StatusUnauthorized.status.Code():
|
||||
code = codes.Unauthenticated
|
||||
return toRPCErr(codes.Unauthenticated, desc...)
|
||||
case StatusNotFound.status.Code():
|
||||
code = codes.NotFound
|
||||
return toRPCErr(codes.NotFound, desc...)
|
||||
case StatusDeadlineExceeded.status.Code():
|
||||
code = codes.DeadlineExceeded
|
||||
return toRPCErr(codes.DeadlineExceeded, desc...)
|
||||
case StatusAccessDenied.status.Code():
|
||||
code = codes.PermissionDenied
|
||||
return toRPCErr(codes.PermissionDenied, desc...)
|
||||
case StatusLimitExceed.status.Code():
|
||||
code = codes.ResourceExhausted
|
||||
return toRPCErr(codes.ResourceExhausted, desc...)
|
||||
case StatusMethodNotAllowed.status.Code():
|
||||
code = codes.Unimplemented
|
||||
return toRPCErr(codes.Unimplemented, desc...)
|
||||
case StatusServiceUnavailable.status.Code():
|
||||
return toRPCErr(codes.Unavailable, desc...)
|
||||
}
|
||||
|
||||
return code
|
||||
return g.status.Err()
|
||||
}
|
||||
|
||||
func toRPCErr(code codes.Code, descs ...string) error {
|
||||
var desc string
|
||||
if len(descs) > 0 {
|
||||
desc = strings.Join(descs, ", ")
|
||||
} else {
|
||||
desc = code.String()
|
||||
}
|
||||
return status.New(code, desc).Err()
|
||||
}
|
||||
|
||||
// ToRPCCode converted to standard RPC error code
|
||||
func (g *GRPCStatus) ToRPCCode() codes.Code {
|
||||
switch g.status.Code() {
|
||||
case StatusInternalServerError.status.Code():
|
||||
return codes.Internal
|
||||
case StatusInvalidParams.status.Code():
|
||||
return codes.InvalidArgument
|
||||
case StatusUnauthorized.status.Code():
|
||||
return codes.Unauthenticated
|
||||
case StatusNotFound.status.Code():
|
||||
return codes.NotFound
|
||||
case StatusDeadlineExceeded.status.Code():
|
||||
return codes.DeadlineExceeded
|
||||
case StatusAccessDenied.status.Code():
|
||||
return codes.PermissionDenied
|
||||
case StatusLimitExceed.status.Code():
|
||||
return codes.ResourceExhausted
|
||||
case StatusMethodNotAllowed.status.Code():
|
||||
return codes.Unimplemented
|
||||
case StatusServiceUnavailable.status.Code():
|
||||
return codes.Unavailable
|
||||
}
|
||||
|
||||
return g.status.Code()
|
||||
}
|
||||
|
|
|
@ -34,13 +34,23 @@ func TestToRPCCode(t *testing.T) {
|
|||
StatusDeadlineExceeded,
|
||||
StatusAccessDenied,
|
||||
StatusMethodNotAllowed,
|
||||
StatusServiceUnavailable,
|
||||
}
|
||||
|
||||
var codes []string
|
||||
for _, s := range status {
|
||||
codes = append(codes, ToRPCCode(s.status.Code()).String())
|
||||
codes = append(codes, s.ToRPCCode().String())
|
||||
}
|
||||
t.Log(codes)
|
||||
var errors []error
|
||||
for i, s := range status {
|
||||
if i%2 == 0 {
|
||||
errors = append(errors, s.ToRPCErr())
|
||||
continue
|
||||
}
|
||||
errors = append(errors, s.ToRPCErr(s.status.Message()))
|
||||
}
|
||||
t.Log(errors)
|
||||
}
|
||||
|
||||
func TestGCode(t *testing.T) {
|
||||
|
|
|
@ -10,12 +10,13 @@ var (
|
|||
StatusInternalServerError = NewGRPCStatus(300003, "服务内部错误")
|
||||
StatusNotFound = NewGRPCStatus(300004, "资源不存在")
|
||||
StatusAlreadyExists = NewGRPCStatus(300005, "资源已存在")
|
||||
StatusTimeout = NewGRPCStatus(300006, "超时")
|
||||
StatusTimeout = NewGRPCStatus(300006, "访问超时")
|
||||
StatusTooManyRequests = NewGRPCStatus(300007, "请求过多")
|
||||
StatusForbidden = NewGRPCStatus(300008, "拒绝访问")
|
||||
StatusLimitExceed = NewGRPCStatus(300009, "访问限制")
|
||||
|
||||
StatusDeadlineExceeded = NewGRPCStatus(300010, "已超过最后期限")
|
||||
StatusAccessDenied = NewGRPCStatus(300011, "拒绝访问")
|
||||
StatusMethodNotAllowed = NewGRPCStatus(300012, "不允许使用的方法")
|
||||
StatusDeadlineExceeded = NewGRPCStatus(300010, "已超过最后期限")
|
||||
StatusAccessDenied = NewGRPCStatus(300011, "拒绝访问")
|
||||
StatusMethodNotAllowed = NewGRPCStatus(300012, "不允许使用的方法")
|
||||
StatusServiceUnavailable = NewGRPCStatus(300013, "服务不可用")
|
||||
)
|
||||
|
|
|
@ -60,8 +60,8 @@ func (e *Error) WithDetails(details ...string) *Error {
|
|||
return &newError
|
||||
}
|
||||
|
||||
// StatusCode 对应http错误码
|
||||
func (e *Error) StatusCode() int {
|
||||
// ToHTTPCode 转换为http错误码
|
||||
func (e *Error) ToHTTPCode() int {
|
||||
switch e.Code() {
|
||||
case Success.Code():
|
||||
return http.StatusOK
|
||||
|
|
|
@ -35,7 +35,7 @@ func TestNewError(t *testing.T) {
|
|||
}
|
||||
var httpCodes []int
|
||||
for _, e := range errorsCodes {
|
||||
httpCodes = append(httpCodes, e.StatusCode())
|
||||
httpCodes = append(httpCodes, e.ToHTTPCode())
|
||||
}
|
||||
t.Log(httpCodes)
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ var (
|
|||
Forbidden = NewError(100008, "拒绝访问")
|
||||
LimitExceed = NewError(100009, "访问限制")
|
||||
|
||||
DeadlineExceeded = NewError(100010, "已超过最后期限")
|
||||
AccessDenied = NewError(100011, "拒绝访问")
|
||||
MethodNotAllowed = NewError(100012, "不允许使用的方法")
|
||||
DeadlineExceeded = NewError(100010, "已超过最后期限")
|
||||
AccessDenied = NewError(100011, "拒绝访问")
|
||||
MethodNotAllowed = NewError(100012, "不允许使用的方法")
|
||||
MethodServiceUnavailable = NewError(100013, "服务不可用")
|
||||
)
|
||||
|
|
|
@ -42,34 +42,33 @@ gin中间件插件。
|
|||
|
||||
<br>
|
||||
|
||||
### qps限流
|
||||
### 限流
|
||||
|
||||
#### path维度的qps限流
|
||||
#### 方式一:根据硬件资源自适应限流
|
||||
|
||||
```go
|
||||
r := gin.Default()
|
||||
|
||||
// e.g. (1) use default
|
||||
// r.Use(RateLimit())
|
||||
|
||||
// e.g. (2) custom parameters
|
||||
r.Use(RateLimit(
|
||||
WithWindow(time.Second*10),
|
||||
WithBucket(100),
|
||||
WithCPUThreshold(100),
|
||||
WithCPUQuota(0.5),
|
||||
))
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
### 熔断器
|
||||
|
||||
```go
|
||||
r := gin.Default()
|
||||
|
||||
// path, 默认qps=500, burst=1000
|
||||
r.Use(ratelimiter.QPS())
|
||||
|
||||
// path, 自定义qps=50, burst=100
|
||||
r.Use(ratelimiter.QPS(
|
||||
ratelimiter.WithQPS(50),
|
||||
ratelimiter.WithBurst(100),
|
||||
))
|
||||
r.Use(CircuitBreaker())
|
||||
```
|
||||
|
||||
#### ip维度的qps限流
|
||||
|
||||
```go
|
||||
// ip, 自定义qps=40, burst=80
|
||||
r.Use(ratelimiter.QPS(
|
||||
ratelimiter.WithIP(),
|
||||
ratelimiter.WithQPS(40),
|
||||
ratelimiter.WithBurst(80),
|
||||
))
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
### jwt鉴权
|
||||
|
@ -131,4 +130,4 @@ func SpanDemo(serviceName string, spanName string, ctx context.Context) {
|
|||
//metrics.WithIgnoreRequestMethods(http.MethodHead), // ignore request methods
|
||||
//metrics.WithIgnoreRequestPaths("/ping", "/health"), // ignore request paths
|
||||
))
|
||||
```
|
||||
```
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/container/group"
|
||||
"github.com/zhufuyi/sponge/pkg/gin/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-kratos/aegis/circuitbreaker"
|
||||
"github.com/go-kratos/aegis/circuitbreaker/sre"
|
||||
)
|
||||
|
||||
// ErrNotAllowed error not allowed.
|
||||
var ErrNotAllowed = circuitbreaker.ErrNotAllowed
|
||||
|
||||
// CircuitBreakerOption set the circuit breaker circuitBreakerOptions.
|
||||
type CircuitBreakerOption func(*circuitBreakerOptions)
|
||||
|
||||
type circuitBreakerOptions struct {
|
||||
group *group.Group
|
||||
}
|
||||
|
||||
func defaultCircuitBreakerOptions() *circuitBreakerOptions {
|
||||
return &circuitBreakerOptions{
|
||||
group: group.NewGroup(func() interface{} {
|
||||
return sre.NewBreaker()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *circuitBreakerOptions) apply(opts ...CircuitBreakerOption) {
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
}
|
||||
|
||||
// WithGroup with circuit breaker group.
|
||||
// NOTE: implements generics circuitbreaker.CircuitBreaker
|
||||
func WithGroup(g *group.Group) CircuitBreakerOption {
|
||||
return func(o *circuitBreakerOptions) {
|
||||
o.group = g
|
||||
}
|
||||
}
|
||||
|
||||
// CircuitBreaker a circuit breaker middleware
|
||||
func CircuitBreaker(opts ...CircuitBreakerOption) gin.HandlerFunc {
|
||||
o := defaultCircuitBreakerOptions()
|
||||
o.apply(opts...)
|
||||
|
||||
return func(c *gin.Context) {
|
||||
breaker := o.group.Get(c.FullPath()).(circuitbreaker.CircuitBreaker)
|
||||
if err := breaker.Allow(); err != nil {
|
||||
// NOTE: when client reject request locally,
|
||||
// continue add counter let the drop ratio higher.
|
||||
breaker.MarkFailed()
|
||||
response.Output(c, http.StatusServiceUnavailable, err.Error())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
|
||||
code := c.Writer.Status()
|
||||
// NOTE: need to check internal and service unavailable error
|
||||
if code == http.StatusInternalServerError || code == http.StatusServiceUnavailable || code == http.StatusGatewayTimeout {
|
||||
breaker.MarkFailed()
|
||||
} else {
|
||||
breaker.MarkSuccess()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/container/group"
|
||||
"github.com/zhufuyi/sponge/pkg/gin/response"
|
||||
"github.com/zhufuyi/sponge/pkg/gohttp"
|
||||
"github.com/zhufuyi/sponge/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-kratos/aegis/circuitbreaker/sre"
|
||||
)
|
||||
|
||||
func runCircuitBreakerHTTPServer() string {
|
||||
serverAddr, requestAddr := utils.GetLocalHTTPAddrPairs()
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.Use(CircuitBreaker(WithGroup(group.NewGroup(func() interface{} {
|
||||
return sre.NewBreaker()
|
||||
}))))
|
||||
|
||||
r.GET("/hello", func(c *gin.Context) {
|
||||
if rand.Int()%2 == 0 {
|
||||
response.Output(c, http.StatusInternalServerError)
|
||||
} else {
|
||||
response.Success(c, "hello "+c.ClientIP())
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
err := r.Run(serverAddr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
return requestAddr
|
||||
}
|
||||
|
||||
func TestCircuitBreaker(t *testing.T) {
|
||||
requestAddr := runCircuitBreakerHTTPServer()
|
||||
|
||||
var success, failures, countBreaker int32
|
||||
for j := 0; j < 5; j++ {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 100; i++ {
|
||||
result := &gohttp.StdResult{}
|
||||
if err := gohttp.Get(result, requestAddr+"/hello"); err != nil {
|
||||
if strings.Contains(err.Error(), ErrNotAllowed.Error()) {
|
||||
atomic.AddInt32(&countBreaker, 1)
|
||||
}
|
||||
atomic.AddInt32(&failures, 1)
|
||||
} else {
|
||||
atomic.AddInt32(&success, 1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
t.Logf("%s success: %d, failures: %d, breakerOpen: %d\n",
|
||||
time.Now().Format(time.RFC3339Nano), success, failures, countBreaker)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/gin/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
rl "github.com/go-kratos/aegis/ratelimit"
|
||||
"github.com/go-kratos/aegis/ratelimit/bbr"
|
||||
)
|
||||
|
||||
// ErrLimitExceed is returned when the rate limiter is
|
||||
// triggered and the request is rejected due to limit exceeded.
|
||||
var ErrLimitExceed = rl.ErrLimitExceed
|
||||
|
||||
// RateLimitOption set the rate limits rateLimitOptions.
|
||||
type RateLimitOption func(*rateLimitOptions)
|
||||
|
||||
type rateLimitOptions struct {
|
||||
window time.Duration
|
||||
bucket int
|
||||
cpuThreshold int64
|
||||
cpuQuota float64
|
||||
}
|
||||
|
||||
func defaultRatelimitOptions() *rateLimitOptions {
|
||||
return &rateLimitOptions{
|
||||
window: time.Second * 10,
|
||||
bucket: 100,
|
||||
cpuThreshold: 800,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *rateLimitOptions) apply(opts ...RateLimitOption) {
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
}
|
||||
|
||||
// WithWindow with window size.
|
||||
func WithWindow(d time.Duration) RateLimitOption {
|
||||
return func(o *rateLimitOptions) {
|
||||
o.window = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithBucket with bucket size.
|
||||
func WithBucket(b int) RateLimitOption {
|
||||
return func(o *rateLimitOptions) {
|
||||
o.bucket = b
|
||||
}
|
||||
}
|
||||
|
||||
// WithCPUThreshold with cpu threshold
|
||||
func WithCPUThreshold(threshold int64) RateLimitOption {
|
||||
return func(o *rateLimitOptions) {
|
||||
o.cpuThreshold = threshold
|
||||
}
|
||||
}
|
||||
|
||||
// WithCPUQuota with real cpu quota(if it can not collect from process correct);
|
||||
func WithCPUQuota(quota float64) RateLimitOption {
|
||||
return func(o *rateLimitOptions) {
|
||||
o.cpuQuota = quota
|
||||
}
|
||||
}
|
||||
|
||||
// RateLimit an adaptive rate limiter middleware
|
||||
func RateLimit(opts ...RateLimitOption) gin.HandlerFunc {
|
||||
o := defaultRatelimitOptions()
|
||||
o.apply(opts...)
|
||||
limiter := bbr.NewLimiter(
|
||||
bbr.WithWindow(o.window),
|
||||
bbr.WithBucket(o.bucket),
|
||||
bbr.WithCPUThreshold(o.cpuThreshold),
|
||||
bbr.WithCPUQuota(o.cpuQuota),
|
||||
)
|
||||
|
||||
return func(c *gin.Context) {
|
||||
done, err := limiter.Allow()
|
||||
if err != nil {
|
||||
response.Output(c, http.StatusTooManyRequests, err.Error())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
|
||||
done(rl.DoneInfo{Err: c.Request.Context().Err()})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/gin/response"
|
||||
"github.com/zhufuyi/sponge/pkg/gohttp"
|
||||
"github.com/zhufuyi/sponge/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func runRateLimiterHTTPServer() string {
|
||||
serverAddr, requestAddr := utils.GetLocalHTTPAddrPairs()
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
|
||||
// e.g. (1) use default
|
||||
// r.Use(RateLimit())
|
||||
|
||||
// e.g. (2) custom parameters
|
||||
r.Use(RateLimit(
|
||||
WithWindow(time.Second*10),
|
||||
WithBucket(200),
|
||||
WithCPUThreshold(500),
|
||||
WithCPUQuota(0.5),
|
||||
))
|
||||
|
||||
r.GET("/hello", func(c *gin.Context) {
|
||||
if rand.Int()%2 == 0 {
|
||||
response.Output(c, http.StatusInternalServerError)
|
||||
} else {
|
||||
response.Success(c, "hello "+c.ClientIP())
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
err := r.Run(serverAddr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
return requestAddr
|
||||
}
|
||||
|
||||
func TestRateLimiter(t *testing.T) {
|
||||
requestAddr := runRateLimiterHTTPServer()
|
||||
|
||||
var success, failures int32
|
||||
for j := 0; j < 10; j++ {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 100; i++ {
|
||||
result := &gohttp.StdResult{}
|
||||
if err := gohttp.Get(result, requestAddr+"/hello"); err != nil {
|
||||
atomic.AddInt32(&failures, 1)
|
||||
} else {
|
||||
atomic.AddInt32(&success, 1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
t.Logf("%s success: %d, failures: %d\n",
|
||||
time.Now().Format(time.RFC3339Nano), success, failures)
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
## ratelimiter
|
||||
|
||||
gin `path` or `ip` limit.
|
||||
|
||||
<br>
|
||||
|
||||
### Usage
|
||||
|
||||
```go
|
||||
r := gin.Default()
|
||||
|
||||
// e.g. (1) default path limit, qps=500, burst=1000
|
||||
// r.Use(QPS())
|
||||
|
||||
// e.g. (2) path limit, qps=50, burst=100
|
||||
r.Use(QPS(
|
||||
WithPath(),
|
||||
WithQPS(50),
|
||||
WithBurst(100),
|
||||
))
|
||||
|
||||
// e.g. (3) ip limit, qps=20, burst=40
|
||||
// r.Use(QPS(
|
||||
// WithIP(),
|
||||
// WithQPS(20),
|
||||
// WithBurst(40),
|
||||
// ))
|
||||
```
|
|
@ -1,67 +0,0 @@
|
|||
package ratelimiter
|
||||
|
||||
import (
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
var (
|
||||
// default qps value
|
||||
defaultQPS rate.Limit = 500
|
||||
|
||||
// default the maximum instantaneous request spike allowed, burst >= qps
|
||||
defaultBurst = 1000
|
||||
|
||||
// default is path limit, fault:path limit, true:ip limit
|
||||
defaultIsIP = false //nolint
|
||||
)
|
||||
|
||||
// Option set the rate limits options.
|
||||
type Option func(*options)
|
||||
|
||||
func defaultOptions() *options {
|
||||
return &options{
|
||||
qps: defaultQPS,
|
||||
burst: defaultBurst,
|
||||
isIP: false,
|
||||
}
|
||||
}
|
||||
|
||||
type options struct {
|
||||
qps rate.Limit
|
||||
burst int
|
||||
isIP bool // false: path limit, true: IP limit
|
||||
}
|
||||
|
||||
func (o *options) apply(opts ...Option) {
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
}
|
||||
|
||||
// WithQPS set the qps value
|
||||
func WithQPS(qps int) Option {
|
||||
return func(o *options) {
|
||||
o.qps = rate.Limit(qps)
|
||||
}
|
||||
}
|
||||
|
||||
// WithBurst set the burst value, burst >= qps
|
||||
func WithBurst(burst int) Option {
|
||||
return func(o *options) {
|
||||
o.burst = burst
|
||||
}
|
||||
}
|
||||
|
||||
// WithPath set the path limit mode
|
||||
func WithPath() Option {
|
||||
return func(o *options) {
|
||||
o.isIP = false
|
||||
}
|
||||
}
|
||||
|
||||
// WithIP set the path limit mode
|
||||
func WithIP() Option {
|
||||
return func(o *options) {
|
||||
o.isIP = true
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package ratelimiter
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
var l *Limiter
|
||||
|
||||
// Limiter is a controller for the request rate.
|
||||
type Limiter struct {
|
||||
qpsLimiter sync.Map
|
||||
}
|
||||
|
||||
// NewLimiter instantiated limiter
|
||||
func NewLimiter() *Limiter {
|
||||
return &Limiter{}
|
||||
}
|
||||
|
||||
// GetLimiter get Limiter object, can be updated or query
|
||||
func GetLimiter() *Limiter {
|
||||
return l
|
||||
}
|
||||
|
||||
// SetLimiter set limiter parameters
|
||||
// "limit" indicates the number of token buckets to be added at a rate = value/second (e.g. 10 means 1 token every 100 ms)
|
||||
// "burst" the maximum instantaneous request spike allowed
|
||||
func (l *Limiter) SetLimiter(limit rate.Limit, burst int) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
path := c.FullPath()
|
||||
l.qpsLimiter.LoadOrStore(path, rate.NewLimiter(limit, burst))
|
||||
if !l.allow(path) {
|
||||
c.JSON(http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests))
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Limiter) allow(path string) bool {
|
||||
if limiter, exist := l.qpsLimiter.Load(path); exist {
|
||||
if ql, ok := limiter.(*rate.Limiter); ok && !ql.Allow() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// UpdateQPSLimiter updates the settings for a given path's QPS limiter.
|
||||
func (l *Limiter) UpdateQPSLimiter(path string, limit rate.Limit, burst int) {
|
||||
if limiter, exist := l.qpsLimiter.Load(path); exist {
|
||||
limiter.(*rate.Limiter).SetLimit(limit)
|
||||
limiter.(*rate.Limiter).SetBurst(burst)
|
||||
} else {
|
||||
l.qpsLimiter.Store(path, rate.NewLimiter(limit, burst))
|
||||
}
|
||||
}
|
||||
|
||||
// GetQPSLimiterStatus returns the status of a given path's QPS limiter.
|
||||
func (l *Limiter) GetQPSLimiterStatus(path string) (limit rate.Limit, burst int) {
|
||||
if limiter, exist := l.qpsLimiter.Load(path); exist {
|
||||
return limiter.(*rate.Limiter).Limit(), limiter.(*rate.Limiter).Burst()
|
||||
}
|
||||
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// QPS set limit qps parameters
|
||||
func QPS(opts ...Option) gin.HandlerFunc {
|
||||
o := defaultOptions()
|
||||
o.apply(opts...)
|
||||
l = NewLimiter()
|
||||
|
||||
return func(c *gin.Context) {
|
||||
var path string
|
||||
if !o.isIP {
|
||||
path = c.FullPath()
|
||||
} else {
|
||||
path = c.ClientIP()
|
||||
}
|
||||
|
||||
l.qpsLimiter.LoadOrStore(path, rate.NewLimiter(o.qps, o.burst))
|
||||
if !l.allow(path) {
|
||||
c.Abort()
|
||||
c.JSON(http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests))
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
|
@ -1,234 +0,0 @@
|
|||
package ratelimiter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/gin/response"
|
||||
"github.com/zhufuyi/sponge/pkg/gohttp"
|
||||
"github.com/zhufuyi/sponge/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
func runRateLimiterHTTPServer() string {
|
||||
serverAddr, requestAddr := utils.GetLocalHTTPAddrPairs()
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
|
||||
// e.g. (1) path limit, qps=500, burst=1000
|
||||
// r.Use(QPS())
|
||||
|
||||
// e.g. (2) path limit, qps=50, burst=100
|
||||
r.Use(QPS(
|
||||
WithPath(),
|
||||
WithQPS(50),
|
||||
WithBurst(100),
|
||||
))
|
||||
|
||||
// e.g. (3) ip limit, qps=20, burst=40
|
||||
// r.Use(QPS(
|
||||
// WithIP(),
|
||||
// WithQPS(20),
|
||||
// WithBurst(40),
|
||||
// ))
|
||||
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
response.Success(c, "pong "+c.ClientIP())
|
||||
})
|
||||
|
||||
r.GET("/hello", func(c *gin.Context) {
|
||||
response.Success(c, "hello "+c.ClientIP())
|
||||
})
|
||||
|
||||
go func() {
|
||||
err := r.Run(serverAddr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
return requestAddr
|
||||
}
|
||||
|
||||
func TestLimiter_QPS(t *testing.T) {
|
||||
requestAddr := runRateLimiterHTTPServer()
|
||||
|
||||
success, failure := 0, 0
|
||||
start := time.Now()
|
||||
for i := 0; i < 150; i++ {
|
||||
result := &gohttp.StdResult{}
|
||||
err := gohttp.Get(result, requestAddr+"/hello")
|
||||
if err != nil {
|
||||
failure++
|
||||
if failure%10 == 0 {
|
||||
fmt.Printf("%d %v\n", i, err)
|
||||
}
|
||||
} else {
|
||||
success++
|
||||
}
|
||||
}
|
||||
|
||||
end := time.Now().Sub(start).Seconds()
|
||||
t.Logf("time=%.3fs, success=%d, failure=%d, qps=%.1f", end, success, failure, float64(success)/end)
|
||||
}
|
||||
|
||||
func TestRateLimiter(t *testing.T) {
|
||||
requestAddr := runRateLimiterHTTPServer()
|
||||
|
||||
var pingSuccess, pingFailures int32
|
||||
var helloSuccess, helloFailures int32
|
||||
|
||||
for j := 0; j < 5; j++ {
|
||||
wg := &sync.WaitGroup{}
|
||||
for i := 0; i < 40; i++ {
|
||||
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
result := &gohttp.StdResult{}
|
||||
if err := gohttp.Get(result, requestAddr+"/ping"); err != nil {
|
||||
atomic.AddInt32(&pingFailures, 1)
|
||||
} else {
|
||||
atomic.AddInt32(&pingSuccess, 1)
|
||||
}
|
||||
}(i)
|
||||
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
result := &gohttp.StdResult{}
|
||||
if err := gohttp.Get(result, requestAddr+"/hello"); err != nil {
|
||||
atomic.AddInt32(&helloFailures, 1)
|
||||
} else {
|
||||
atomic.AddInt32(&helloSuccess, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
fmt.Printf("%s helloSuccess: %d, helloFailures: %d pingSuccess: %d, pingFailures: %d\n", time.Now().Format(time.RFC3339Nano), helloSuccess, helloFailures, pingSuccess, pingFailures)
|
||||
|
||||
//time.Sleep(time.Millisecond * 200)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimiter_GetQPSLimiterStatus(t *testing.T) {
|
||||
requestAddr := runRateLimiterHTTPServer()
|
||||
|
||||
var pingSuccess, pingFailures int32
|
||||
|
||||
for j := 0; j < 5; j++ {
|
||||
wg := &sync.WaitGroup{}
|
||||
for i := 0; i < 40; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
result := &gohttp.StdResult{}
|
||||
if err := gohttp.Get(result, requestAddr+"/ping"); err != nil {
|
||||
atomic.AddInt32(&pingFailures, 1)
|
||||
} else {
|
||||
atomic.AddInt32(&pingSuccess, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
qps, _ := GetLimiter().GetQPSLimiterStatus("/ping")
|
||||
fmt.Printf("%s pingSuccess: %d, pingFailures: %d limit:%.f\n", time.Now().Format(time.RFC3339Nano), pingSuccess, pingFailures, qps)
|
||||
//time.Sleep(time.Millisecond * 200)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimiter_UpdateQPSLimiter(t *testing.T) {
|
||||
requestAddr := runRateLimiterHTTPServer()
|
||||
|
||||
var pingSuccess, pingFailures int32
|
||||
|
||||
for j := 0; j < 5; j++ {
|
||||
wg := &sync.WaitGroup{}
|
||||
for i := 0; i < 40; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
result := &gohttp.StdResult{}
|
||||
if err := gohttp.Get(result, requestAddr+"/ping"); err != nil {
|
||||
atomic.AddInt32(&pingFailures, 1)
|
||||
} else {
|
||||
atomic.AddInt32(&pingSuccess, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
limit, burst := GetLimiter().GetQPSLimiterStatus("/ping")
|
||||
GetLimiter().UpdateQPSLimiter("/ping", limit+rate.Limit(j), burst)
|
||||
fmt.Printf("%s pingSuccess: %d, pingFailures: %d limit:%.f\n", time.Now().Format(time.RFC3339Nano), pingSuccess, pingFailures, limit)
|
||||
//time.Sleep(time.Millisecond * 200)
|
||||
}
|
||||
}
|
||||
|
||||
func runRateLimiterHTTPServer2() string {
|
||||
serverAddr, requestAddr := utils.GetLocalHTTPAddrPairs()
|
||||
l := NewLimiter()
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
|
||||
r.Use(l.SetLimiter(10, 20))
|
||||
|
||||
r.Use(QPS(
|
||||
WithIP(),
|
||||
WithQPS(10),
|
||||
WithBurst(20),
|
||||
))
|
||||
|
||||
r.GET("/hello", func(c *gin.Context) {
|
||||
response.Success(c, "hello "+c.ClientIP())
|
||||
})
|
||||
|
||||
go func() {
|
||||
err := r.Run(serverAddr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
return requestAddr
|
||||
}
|
||||
|
||||
func TestRateLimiter2(t *testing.T) {
|
||||
requestAddr := runRateLimiterHTTPServer2()
|
||||
|
||||
var pingSuccess, pingFailures int32
|
||||
var helloSuccess, helloFailures int32
|
||||
|
||||
for j := 0; j < 3; j++ {
|
||||
wg := &sync.WaitGroup{}
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
result := &gohttp.StdResult{}
|
||||
if err := gohttp.Get(result, requestAddr+"/hello"); err != nil {
|
||||
atomic.AddInt32(&helloFailures, 1)
|
||||
} else {
|
||||
atomic.AddInt32(&helloSuccess, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
fmt.Printf("%s helloSuccess: %d, helloFailures: %d pingSuccess: %d, pingFailures: %d\n", time.Now().Format(time.RFC3339Nano), helloSuccess, helloFailures, pingSuccess, pingFailures)
|
||||
|
||||
//time.Sleep(time.Millisecond * 200)
|
||||
}
|
||||
}
|
|
@ -80,6 +80,10 @@ func Output(c *gin.Context, code int, msg ...interface{}) {
|
|||
respJSONWithStatusCode(c, http.StatusConflict, errcode.AlreadyExists.Msg(), msg...)
|
||||
case http.StatusInternalServerError:
|
||||
respJSONWithStatusCode(c, http.StatusInternalServerError, errcode.InternalServerError.Msg(), msg...)
|
||||
case http.StatusTooManyRequests:
|
||||
respJSONWithStatusCode(c, http.StatusTooManyRequests, errcode.LimitExceed.Msg(), msg...)
|
||||
case http.StatusServiceUnavailable:
|
||||
respJSONWithStatusCode(c, http.StatusServiceUnavailable, errcode.MethodServiceUnavailable.Msg(), msg...)
|
||||
|
||||
default:
|
||||
respJSONWithStatusCode(c, code, http.StatusText(code), msg...)
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
## gobash
|
||||
|
||||
在go环境中执行命令、脚本、可执行文件,日志实时输出。
|
||||
|
||||
<br>
|
||||
|
||||
## 安装
|
||||
|
||||
> go get -u github.com/zhufuyi/pkg/gobash
|
||||
|
||||
<br>
|
||||
|
||||
## 使用示例
|
||||
|
||||
### Run
|
||||
|
||||
Run执行命令,可以主动结束命令,实时返回日志和错误信息,推荐使用
|
||||
|
||||
```go
|
||||
|
||||
command := "for i in $(seq 1 5); do echo 'test cmd' $i;sleep 1; done"
|
||||
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) // 超时控制
|
||||
|
||||
// 执行
|
||||
result := Run(ctx, command)
|
||||
// 实时输出日志和错误信息
|
||||
for v := range result.StdOut {
|
||||
fmt.Printf(v)
|
||||
}
|
||||
if result.Err != nil {
|
||||
fmt.Println("exec command failed,", result.Err.Error())
|
||||
}
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
### Exec
|
||||
|
||||
Exec 适合执行单条非阻塞命令,输出标准和错误日志,但日志输出不是实时,注:如果执行命令永久阻塞,会造成协程泄露
|
||||
|
||||
```go
|
||||
command := "for i in $(seq 1 5); do echo 'test cmd' $i;sleep 1; done"
|
||||
out, err := gobash.Exec(command)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(string(out))
|
||||
```
|
|
@ -0,0 +1,135 @@
|
|||
package gobash
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// linux default executor
|
||||
var executor = "/bin/bash"
|
||||
|
||||
// SetExecutorPath 设置执行器
|
||||
func SetExecutorPath(path string) {
|
||||
executor = path
|
||||
}
|
||||
|
||||
// Exec 适合执行单条非阻塞命令,输出标准和错误日志,但日志输出不是实时,
|
||||
// 注:如果执行命令永久阻塞,会造成协程泄露
|
||||
func Exec(command string) ([]byte, error) {
|
||||
cmd := exec.Command(executor, "-c", command)
|
||||
|
||||
stdout, stderr, err := getCmdReader(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bytes, err := io.ReadAll(stdout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bytesErr, err := io.ReadAll(stderr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
if len(bytesErr) != 0 {
|
||||
return nil, errors.New(string(bytesErr))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
// Run 执行命令,可以主动结束命令,执行结果实时返回在Result.StdOut中
|
||||
func Run(ctx context.Context, command string) *Result {
|
||||
result := &Result{StdOut: make(chan string), Err: error(nil)}
|
||||
|
||||
go func() {
|
||||
defer func() { close(result.StdOut) }() // 执行完毕,关闭通道
|
||||
|
||||
cmd := exec.CommandContext(ctx, executor, "-c", command)
|
||||
handleExec(ctx, cmd, result)
|
||||
}()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Result 执行命令的结果
|
||||
type Result struct {
|
||||
StdOut chan string
|
||||
Err error // 执行完毕命令后,如果为nil,执行命令成功
|
||||
}
|
||||
|
||||
func handleExec(ctx context.Context, cmd *exec.Cmd, result *Result) {
|
||||
result.StdOut <- strings.Join(cmd.Args, " ") + "\n"
|
||||
|
||||
stdout, stderr, err := getCmdReader(cmd)
|
||||
if err != nil {
|
||||
result.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(stdout)
|
||||
// 实时读取每行内容
|
||||
line := ""
|
||||
for {
|
||||
line, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) { // 判断是否已经读取完毕
|
||||
break
|
||||
}
|
||||
result.Err = err
|
||||
break
|
||||
}
|
||||
select {
|
||||
case result.StdOut <- line:
|
||||
case <-ctx.Done():
|
||||
result.Err = fmt.Errorf("%v", ctx.Err())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 捕获错误日志
|
||||
bytesErr, err := io.ReadAll(stderr)
|
||||
if err != nil {
|
||||
result.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
if len(bytesErr) != 0 {
|
||||
result.Err = errors.New(string(bytesErr))
|
||||
return
|
||||
}
|
||||
result.Err = err
|
||||
}
|
||||
}
|
||||
|
||||
func getCmdReader(cmd *exec.Cmd) (io.ReadCloser, io.ReadCloser, error) {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return stdout, stderr, nil
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package gobash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS == "windows" {
|
||||
SetExecutorPath("D:\\Program Files\\cmder\\vendor\\git-for-windows\\bin\\bash.exe")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
cmds := []string{
|
||||
"for i in $(seq 1 3); do exit 1; done",
|
||||
"notFoundCommand",
|
||||
"pwd",
|
||||
"for i in $(seq 1 5); do echo 'test cmd' $i;sleep 0.2; done",
|
||||
}
|
||||
|
||||
for _, cmd := range cmds {
|
||||
ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*500) // 超时控制
|
||||
result := Run(ctx, cmd) // 执行
|
||||
for v := range result.StdOut { // 实时输出日志和错误信息
|
||||
t.Logf(v)
|
||||
}
|
||||
if result.Err != nil {
|
||||
t.Logf("exec command failed, %v", result.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExec(t *testing.T) {
|
||||
cmds := []string{
|
||||
"for i in $(seq 1 3); do exit 1; done",
|
||||
"notFoundCommand",
|
||||
"pwd",
|
||||
"for i in $(seq 1 3); do echo 'test cmd' $i;sleep 0.2; done",
|
||||
}
|
||||
|
||||
for _, cmd := range cmds {
|
||||
out, err := Exec(cmd)
|
||||
if err != nil {
|
||||
t.Logf("exec command[%s] failed, %v\n", cmd, err)
|
||||
continue
|
||||
}
|
||||
t.Logf("%s\n", out)
|
||||
}
|
||||
}
|
|
@ -10,7 +10,10 @@ import (
|
|||
// IsExists 判断文件或文件夹是否存在
|
||||
func IsExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
if err != nil {
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetRunPath 获取程序执行的绝对路径
|
||||
|
@ -29,10 +32,15 @@ func GetFilename(filePath string) string {
|
|||
return name
|
||||
}
|
||||
|
||||
// IsWindows 判断是否window环境
|
||||
func IsWindows() bool {
|
||||
return runtime.GOOS == "windows"
|
||||
}
|
||||
|
||||
// GetPathDelimiter 根据系统类型获取分隔符
|
||||
func GetPathDelimiter() string {
|
||||
delimiter := "/"
|
||||
if runtime.GOOS == "windows" {
|
||||
if IsWindows() {
|
||||
delimiter = "\\"
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,10 @@ import (
|
|||
)
|
||||
|
||||
func TestIsExists(t *testing.T) {
|
||||
ok := IsExists("/tmp/test")
|
||||
if !ok {
|
||||
t.Log("not exists")
|
||||
}
|
||||
ok := IsExists("/tmp/tmp/tmp")
|
||||
assert.Equal(t, false, ok)
|
||||
ok = IsExists("README.md")
|
||||
assert.Equal(t, true, ok)
|
||||
}
|
||||
|
||||
func TestGetRunPath(t *testing.T) {
|
||||
|
@ -78,10 +78,36 @@ func TestGetPathDelimiter(t *testing.T) {
|
|||
t.Log(d)
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
func TestNotMatch(t *testing.T) {
|
||||
fn := matchPrefix("")
|
||||
assert.Equal(t, false, fn("."))
|
||||
|
||||
fn = matchContain("")
|
||||
assert.Equal(t, false, fn("."))
|
||||
|
||||
fn = matchSuffix("")
|
||||
assert.NotNil(t, fn)
|
||||
}
|
||||
|
||||
func TestIsWindows(t *testing.T) {
|
||||
t.Log(IsWindows())
|
||||
}
|
||||
|
||||
func TestErrorPath(t *testing.T) {
|
||||
dir := "/notfound"
|
||||
|
||||
_, err := ListFiles(dir)
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = ListDirsAndFiles(dir)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = walkDirWithFilter(dir, nil, nil)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = walkDir(dir, nil)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = walkDir2(dir, nil, nil)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ func grpcClientExample() pb.UserExampleServiceClient {
|
|||
conn, err := grpccli.DialInsecure(ctx, endpoint,
|
||||
grpccli.WithEnableLog(logger.Get()),
|
||||
grpccli.WithDiscovery(discovery),
|
||||
//grpccli.WithEnableCircuitBreaker(),
|
||||
//grpccli.WithEnableTrace(),
|
||||
//grpccli.WithEnableHystrix("user"),
|
||||
//grpccli.WithEnableLoadBalance(),
|
||||
//grpccli.WithEnableRetry(),
|
||||
//grpccli.WithEnableMetrics(),
|
||||
|
|
|
@ -66,9 +66,9 @@ func dial(ctx context.Context, endpoint string, isSecure bool, opts ...Option) (
|
|||
clientOptions = append(clientOptions, grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`))
|
||||
}
|
||||
|
||||
// 熔断器 hystrix
|
||||
if o.enableHystrix {
|
||||
unaryClientInterceptors = append(unaryClientInterceptors, interceptor.UnaryClientHystrix(o.hystrixName))
|
||||
// 熔断器
|
||||
if o.enableCircuitBreaker {
|
||||
unaryClientInterceptors = append(unaryClientInterceptors, interceptor.UnaryClientCircuitBreaker())
|
||||
}
|
||||
|
||||
// 重试 retry
|
||||
|
|
|
@ -29,7 +29,7 @@ func Test_dial(t *testing.T) {
|
|||
WithEnableLog(zap.NewNop()),
|
||||
WithEnableMetrics(),
|
||||
WithEnableLoadBalance(),
|
||||
WithEnableHystrix("foo"),
|
||||
WithEnableCircuitBreaker(),
|
||||
WithEnableRetry(),
|
||||
WithDiscovery(etcd.New(&clientv3.Client{})),
|
||||
)
|
||||
|
|
|
@ -25,12 +25,11 @@ type options struct {
|
|||
enableLog bool // 是否开启日志
|
||||
log *zap.Logger
|
||||
|
||||
enableTrace bool // 是否开启链路跟踪
|
||||
enableMetrics bool // 是否开启指标
|
||||
enableRetry bool // 是否开启重试
|
||||
enableLoadBalance bool // 是否开启负载均衡器
|
||||
enableHystrix bool // 是否开启熔断
|
||||
hystrixName string // hystrix命令名称
|
||||
enableTrace bool // 是否开启链路跟踪
|
||||
enableMetrics bool // 是否开启指标
|
||||
enableRetry bool // 是否开启重试
|
||||
enableLoadBalance bool // 是否开启负载均衡器
|
||||
enableCircuitBreaker bool // 是否开启熔断器
|
||||
|
||||
discovery registry.Discovery // 服务发现接口
|
||||
}
|
||||
|
@ -97,11 +96,10 @@ func WithEnableRetry() Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithEnableHystrix enable hystrix
|
||||
func WithEnableHystrix(name string) Option {
|
||||
// WithEnableCircuitBreaker enable circuit breaker
|
||||
func WithEnableCircuitBreaker() Option {
|
||||
return func(o *options) {
|
||||
o.enableHystrix = true
|
||||
o.hystrixName = name
|
||||
o.enableCircuitBreaker = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,12 +37,11 @@ func TestWithDiscovery(t *testing.T) {
|
|||
assert.NotEqual(t, testData, o.discovery)
|
||||
}
|
||||
|
||||
func TestWithEnableHystrix(t *testing.T) {
|
||||
testData := "hystrix"
|
||||
opt := WithEnableHystrix(testData)
|
||||
func TestWithEnableCircuitBreaker(t *testing.T) {
|
||||
opt := WithEnableCircuitBreaker()
|
||||
o := new(options)
|
||||
o.apply(opt)
|
||||
assert.Equal(t, testData, o.hystrixName)
|
||||
assert.Equal(t, true, o.enableCircuitBreaker)
|
||||
}
|
||||
|
||||
func TestWithEnableLoadBalance(t *testing.T) {
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
## hystrix
|
||||
|
||||
### 使用示例
|
||||
|
||||
#### grpc client
|
||||
|
||||
```go
|
||||
func getDialOptions() []grpc.DialOption {
|
||||
var options []grpc.DialOption
|
||||
|
||||
// 禁止tls加密
|
||||
options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
|
||||
// 熔断拦截器
|
||||
option := grpc.WithUnaryInterceptor(
|
||||
grpc_middleware.ChainUnaryClient(
|
||||
hystrix.UnaryClientInterceptor("hello_grpc",
|
||||
hystrix.WithTimeout(time.Second*2), // 执行command的超时时间,时间单位是ms,默认时间是1000ms
|
||||
hystrix.WithMaxConcurrentRequests(20), // command的最大并发量,默认值是10并发量
|
||||
hystrix.WithSleepWindow(10*time.Second), // 熔断器被打开后使用,在熔断器被打开后,根据SleepWindow设置的时间控制多久后尝试服务是否可用,默认时间为5000ms
|
||||
hystrix.WithRequestVolumeThreshold(1000), // 判断熔断开关的条件之一,统计10s(代码中写死了)内请求数量,达到这个请求数量后再根据错误率判断是否要开启熔断;
|
||||
hystrix.WithErrorPercentThreshold(25), // 判断熔断开关的条件之一,统计错误百分比,请求数量大于等于RequestVolumeThreshold并且错误率到达这个百分比后就会启动熔断 默认值是50
|
||||
),
|
||||
),
|
||||
)
|
||||
options = append(options, option)
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func main() {
|
||||
conn, err := grpc.Dial("127.0.0.1:8080", getDialOptions()...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// ......
|
||||
}
|
||||
```
|
|
@ -1,107 +0,0 @@
|
|||
package hystrix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/afex/hystrix-go/hystrix"
|
||||
metricCollector "github.com/afex/hystrix-go/hystrix/metric_collector"
|
||||
"github.com/afex/hystrix-go/plugins"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// https://github.com/soyacen/grpc-middleware/tree/main/hystrix
|
||||
|
||||
// UnaryClientInterceptor set the hystrix of unary client interceptor
|
||||
func UnaryClientInterceptor(commandName string, opts ...Option) grpc.UnaryClientInterceptor {
|
||||
o := defaultOptions()
|
||||
o.apply(opts...)
|
||||
|
||||
if o.statsD != nil {
|
||||
c, err := plugins.InitializeStatsdCollector(o.statsD)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
metricCollector.Registry.Register(c.NewStatsdCollector)
|
||||
}
|
||||
|
||||
hystrix.ConfigureCommand(commandName, hystrix.CommandConfig{
|
||||
Timeout: durationToInt(o.timeout, time.Millisecond),
|
||||
MaxConcurrentRequests: o.maxConcurrentRequests,
|
||||
RequestVolumeThreshold: o.requestVolumeThreshold,
|
||||
SleepWindow: durationToInt(o.sleepWindow, time.Millisecond),
|
||||
ErrorPercentThreshold: o.errorPercentThreshold,
|
||||
})
|
||||
|
||||
return unaryClientInterceptor(commandName, o)
|
||||
}
|
||||
|
||||
func unaryClientInterceptor(commandName string, o *options) grpc.UnaryClientInterceptor {
|
||||
return func(ctx context.Context, method string, req interface{}, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) (err error) {
|
||||
err = hystrix.DoC(ctx, commandName,
|
||||
// 熔断开关
|
||||
func(ctx context.Context) error {
|
||||
err = invoker(ctx, method, req, reply, cc, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
// 降级处理
|
||||
o.fallbackFunc,
|
||||
)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// StreamClientInterceptor set the hystrix of clientStream client interceptor
|
||||
func StreamClientInterceptor(commandName string, opts ...Option) grpc.StreamClientInterceptor {
|
||||
o := defaultOptions()
|
||||
o.apply(opts...)
|
||||
|
||||
if o.statsD != nil {
|
||||
c, err := plugins.InitializeStatsdCollector(o.statsD)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
metricCollector.Registry.Register(c.NewStatsdCollector)
|
||||
}
|
||||
|
||||
hystrix.ConfigureCommand(commandName, hystrix.CommandConfig{
|
||||
Timeout: durationToInt(o.timeout, time.Millisecond),
|
||||
MaxConcurrentRequests: o.maxConcurrentRequests,
|
||||
RequestVolumeThreshold: o.requestVolumeThreshold,
|
||||
SleepWindow: durationToInt(o.sleepWindow, time.Millisecond),
|
||||
ErrorPercentThreshold: o.errorPercentThreshold,
|
||||
})
|
||||
|
||||
return streamClientInterceptor(commandName, o)
|
||||
}
|
||||
|
||||
func streamClientInterceptor(commandName string, o *options) grpc.StreamClientInterceptor {
|
||||
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
var clientStream grpc.ClientStream
|
||||
err := hystrix.DoC(ctx, commandName,
|
||||
// 熔断开关
|
||||
func(ctx context.Context) error {
|
||||
var err error
|
||||
clientStream, err = streamer(ctx, desc, cc, method, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
// 降级处理
|
||||
o.fallbackFunc,
|
||||
)
|
||||
return clientStream, err
|
||||
}
|
||||
}
|
||||
|
||||
func durationToInt(duration time.Duration, unit time.Duration) int {
|
||||
durationAsNumber := duration / unit
|
||||
if int64(durationAsNumber) > int64(maxInt) {
|
||||
return maxInt
|
||||
}
|
||||
return int(durationAsNumber)
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package hystrix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestUnaryClientInterceptor(t *testing.T) {
|
||||
interceptor := UnaryClientInterceptor("hystrix",
|
||||
WithStatsDCollector("localhost:5555", "hystrix", 0.5, 2048))
|
||||
assert.NotNil(t, interceptor)
|
||||
|
||||
err := interceptor(context.Background(), "test.ping", nil, nil, nil, clientInvoker)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestStreamClientInterceptor(t *testing.T) {
|
||||
interceptor := StreamClientInterceptor("hystrix",
|
||||
WithStatsDCollector("localhost:5555", "hystrix", 0.5, 2048))
|
||||
assert.NotNil(t, interceptor)
|
||||
|
||||
streamer := func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
return &clientStream{}, nil
|
||||
}
|
||||
_, err := interceptor(context.Background(), nil, nil, "test.ping", streamer)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_durationToInt(t *testing.T) {
|
||||
durationToInt(10*time.Second, time.Second)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
var clientInvoker = func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type clientStream struct {
|
||||
}
|
||||
|
||||
func (s clientStream) Header() (metadata.MD, error) {
|
||||
return metadata.MD{}, nil
|
||||
}
|
||||
|
||||
func (s clientStream) Trailer() metadata.MD {
|
||||
return metadata.MD{}
|
||||
}
|
||||
|
||||
func (s clientStream) CloseSend() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s clientStream) Context() context.Context {
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
func (s clientStream) SendMsg(m interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s clientStream) RecvMsg(m interface{}) error {
|
||||
return nil
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
package hystrix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/afex/hystrix-go/plugins"
|
||||
"github.com/gunerhuseyin/goprometheus"
|
||||
hystrixmiddleware "github.com/gunerhuseyin/goprometheus/middleware/hystrix"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHystrixTimeout = 30 * time.Second
|
||||
defaultMaxConcurrentRequests = 100
|
||||
defaultErrorPercentThreshold = 25
|
||||
defaultSleepWindow = 10
|
||||
defaultRequestVolumeThreshold = 10
|
||||
|
||||
maxUint = ^uint(0)
|
||||
maxInt = int(maxUint >> 1)
|
||||
)
|
||||
|
||||
func defaultOptions() *options {
|
||||
return &options{
|
||||
fallbackFunc: nil,
|
||||
timeout: defaultHystrixTimeout,
|
||||
maxConcurrentRequests: defaultMaxConcurrentRequests,
|
||||
errorPercentThreshold: defaultErrorPercentThreshold,
|
||||
sleepWindow: defaultSleepWindow,
|
||||
requestVolumeThreshold: defaultRequestVolumeThreshold,
|
||||
}
|
||||
}
|
||||
|
||||
// options is the hystrix client implementation
|
||||
type options struct {
|
||||
timeout time.Duration
|
||||
maxConcurrentRequests int
|
||||
requestVolumeThreshold int
|
||||
sleepWindow time.Duration
|
||||
errorPercentThreshold int
|
||||
fallbackFunc func(ctx context.Context, err error) error
|
||||
statsD *plugins.StatsdCollectorConfig
|
||||
}
|
||||
|
||||
func (o *options) apply(opts ...Option) {
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
}
|
||||
|
||||
// Option represents the hystrix client options
|
||||
type Option func(*options)
|
||||
|
||||
// WithTimeout sets hystrix timeout
|
||||
func WithTimeout(timeout time.Duration) Option {
|
||||
return func(c *options) {
|
||||
c.timeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxConcurrentRequests sets hystrix max concurrent requests
|
||||
func WithMaxConcurrentRequests(maxConcurrentRequests int) Option {
|
||||
return func(c *options) {
|
||||
c.maxConcurrentRequests = maxConcurrentRequests
|
||||
}
|
||||
}
|
||||
|
||||
// WithRequestVolumeThreshold sets hystrix request volume threshold
|
||||
func WithRequestVolumeThreshold(requestVolumeThreshold int) Option {
|
||||
return func(c *options) {
|
||||
c.requestVolumeThreshold = requestVolumeThreshold
|
||||
}
|
||||
}
|
||||
|
||||
// WithSleepWindow sets hystrix sleep window
|
||||
func WithSleepWindow(sleepWindow time.Duration) Option {
|
||||
return func(c *options) {
|
||||
c.sleepWindow = sleepWindow
|
||||
}
|
||||
}
|
||||
|
||||
// WithErrorPercentThreshold sets hystrix error percent threshold
|
||||
func WithErrorPercentThreshold(errorPercentThreshold int) Option {
|
||||
return func(c *options) {
|
||||
c.errorPercentThreshold = errorPercentThreshold
|
||||
}
|
||||
}
|
||||
|
||||
// WithFallbackFunc sets the fallback function
|
||||
func WithFallbackFunc(fn func(ctx context.Context, err error) error) Option {
|
||||
return func(c *options) {
|
||||
c.fallbackFunc = fn
|
||||
}
|
||||
}
|
||||
|
||||
// WithPrometheus sets the hystrix metrics
|
||||
func WithPrometheus() Option {
|
||||
return func(c *options) {
|
||||
gpm := goprometheus.New()
|
||||
// 采集hystrix指标
|
||||
gpHystrixConfig := &hystrixmiddleware.Config{
|
||||
Prefix: "hystrix_circuit_breaker_",
|
||||
}
|
||||
gpHystrix := hystrixmiddleware.New(gpm, gpHystrixConfig)
|
||||
gpm.UseHystrix(gpHystrix)
|
||||
|
||||
gpm.Run() // 添加go prometheus路由/metrics
|
||||
}
|
||||
}
|
||||
|
||||
// WithStatsDCollector exports hystrix metrics to a statsD backend
|
||||
func WithStatsDCollector(addr string, prefix string, sampleRate float32, flushBytes int) Option {
|
||||
return func(c *options) {
|
||||
c.statsD = &plugins.StatsdCollectorConfig{StatsdAddr: addr, Prefix: prefix, SampleRate: sampleRate, FlushBytes: flushBytes}
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
package hystrix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWithErrorPercentThreshold(t *testing.T) {
|
||||
testData := 50
|
||||
opt := WithErrorPercentThreshold(testData)
|
||||
o := new(options)
|
||||
o.apply(opt)
|
||||
assert.Equal(t, testData, o.errorPercentThreshold)
|
||||
}
|
||||
|
||||
func TestWithFallbackFunc(t *testing.T) {
|
||||
testData := func(ctx context.Context, err error) error {
|
||||
t.Log("this is fall back")
|
||||
return nil
|
||||
}
|
||||
opt := WithFallbackFunc(testData)
|
||||
o := new(options)
|
||||
o.apply(opt)
|
||||
assert.Equal(t, nil, o.fallbackFunc(context.Background(), nil))
|
||||
}
|
||||
|
||||
func TestWithMaxConcurrentRequests(t *testing.T) {
|
||||
testData := 1000
|
||||
opt := WithMaxConcurrentRequests(testData)
|
||||
o := new(options)
|
||||
o.apply(opt)
|
||||
assert.Equal(t, testData, o.maxConcurrentRequests)
|
||||
}
|
||||
|
||||
func TestWithPrometheus(t *testing.T) {
|
||||
opt := WithPrometheus()
|
||||
o := new(options)
|
||||
o.apply(opt)
|
||||
}
|
||||
|
||||
func TestWithRequestVolumeThreshold(t *testing.T) {
|
||||
testData := 1000
|
||||
opt := WithRequestVolumeThreshold(testData)
|
||||
o := new(options)
|
||||
o.apply(opt)
|
||||
assert.Equal(t, testData, o.requestVolumeThreshold)
|
||||
}
|
||||
|
||||
func TestWithSleepWindow(t *testing.T) {
|
||||
testData := time.Second * 10
|
||||
opt := WithSleepWindow(testData)
|
||||
o := new(options)
|
||||
o.apply(opt)
|
||||
assert.Equal(t, testData, o.sleepWindow)
|
||||
}
|
||||
|
||||
func TestWithStatsDCollector(t *testing.T) {
|
||||
opt := WithStatsDCollector("localhost:5555", "hystrix", 0.5, 2048)
|
||||
o := new(options)
|
||||
o.apply(opt)
|
||||
assert.Equal(t, "hystrix", o.statsD.Prefix)
|
||||
}
|
||||
|
||||
func TestWithTimeout(t *testing.T) {
|
||||
testData := time.Second * 10
|
||||
opt := WithTimeout(testData)
|
||||
o := new(options)
|
||||
o.apply(opt)
|
||||
assert.Equal(t, testData, o.timeout)
|
||||
}
|
||||
|
||||
func Test_defaultOptions(t *testing.T) {
|
||||
o := defaultOptions()
|
||||
assert.NotNil(t, o)
|
||||
}
|
||||
|
||||
func Test_options_apply(t *testing.T) {
|
||||
testData := time.Second * 10
|
||||
opt := WithTimeout(testData)
|
||||
o := new(options)
|
||||
o.apply(opt)
|
||||
assert.Equal(t, testData, o.timeout)
|
||||
}
|
|
@ -121,6 +121,53 @@ func getDialOptions() []grpc.DialOption {
|
|||
|
||||
<br>
|
||||
|
||||
#### 限流
|
||||
|
||||
```go
|
||||
func getDialOptions() []grpc.DialOption {
|
||||
var options []grpc.DialOption
|
||||
|
||||
// 禁用tls
|
||||
options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
|
||||
// circuit breaker
|
||||
option := grpc.WithUnaryInterceptor(
|
||||
grpc_middleware.ChainUnaryClient(
|
||||
interceptor.UnaryRateLimit(),
|
||||
),
|
||||
)
|
||||
options = append(options, option)
|
||||
|
||||
return options
|
||||
}
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
#### 熔断器
|
||||
|
||||
```go
|
||||
func getDialOptions() []grpc.DialOption {
|
||||
var options []grpc.DialOption
|
||||
|
||||
// 禁用tls
|
||||
options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
|
||||
// circuit breaker
|
||||
option := grpc.WithUnaryInterceptor(
|
||||
grpc_middleware.ChainUnaryClient(
|
||||
interceptor.UnaryClientCircuitBreaker(),
|
||||
),
|
||||
)
|
||||
options = append(options, option)
|
||||
|
||||
return options
|
||||
}
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
#### timeout
|
||||
|
||||
```go
|
||||
|
@ -209,8 +256,3 @@ func SpanDemo(serviceName string, spanName string, ctx context.Context) {
|
|||
使用示例 [metrics](../metrics/README.md)。
|
||||
|
||||
<br>
|
||||
|
||||
#### hystrix
|
||||
|
||||
使用示例 [hystrix](../hystrix/README.md)。
|
||||
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
package interceptor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/zhufuyi/sponge/pkg/container/group"
|
||||
"github.com/zhufuyi/sponge/pkg/errcode"
|
||||
|
||||
"github.com/go-kratos/aegis/circuitbreaker"
|
||||
"github.com/go-kratos/aegis/circuitbreaker/sre"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// ErrNotAllowed error not allowed.
|
||||
var ErrNotAllowed = circuitbreaker.ErrNotAllowed
|
||||
|
||||
// CircuitBreakerOption set the circuit breaker circuitBreakerOptions.
|
||||
type CircuitBreakerOption func(*circuitBreakerOptions)
|
||||
|
||||
type circuitBreakerOptions struct {
|
||||
group *group.Group
|
||||
}
|
||||
|
||||
func defaultCircuitBreakerOptions() *circuitBreakerOptions {
|
||||
return &circuitBreakerOptions{
|
||||
group: group.NewGroup(func() interface{} {
|
||||
return sre.NewBreaker()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *circuitBreakerOptions) apply(opts ...CircuitBreakerOption) {
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
}
|
||||
|
||||
// WithGroup with circuit breaker group.
|
||||
// NOTE: implements generics circuitbreaker.CircuitBreaker
|
||||
func WithGroup(g *group.Group) CircuitBreakerOption {
|
||||
return func(o *circuitBreakerOptions) {
|
||||
o.group = g
|
||||
}
|
||||
}
|
||||
|
||||
// UnaryClientCircuitBreaker client-side unary circuit breaker interceptor
|
||||
func UnaryClientCircuitBreaker(opts ...CircuitBreakerOption) grpc.UnaryClientInterceptor {
|
||||
o := defaultCircuitBreakerOptions()
|
||||
o.apply(opts...)
|
||||
|
||||
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
breaker := o.group.Get(method).(circuitbreaker.CircuitBreaker)
|
||||
if err := breaker.Allow(); err != nil {
|
||||
// NOTE: when client reject request locally,
|
||||
// continue add counter let the drop ratio higher.
|
||||
breaker.MarkFailed()
|
||||
return errcode.StatusServiceUnavailable.ToRPCErr(err.Error())
|
||||
}
|
||||
|
||||
err := invoker(ctx, method, req, reply, cc, opts...)
|
||||
if err != nil {
|
||||
// NOTE: need to check internal and service unavailable error
|
||||
s, ok := status.FromError(err)
|
||||
if ok && (s.Code() == codes.Internal || s.Code() == codes.Unavailable) {
|
||||
breaker.MarkFailed()
|
||||
} else {
|
||||
breaker.MarkSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// SteamClientCircuitBreaker client-side stream circuit breaker interceptor
|
||||
func SteamClientCircuitBreaker(opts ...CircuitBreakerOption) grpc.StreamClientInterceptor {
|
||||
o := defaultCircuitBreakerOptions()
|
||||
o.apply(opts...)
|
||||
|
||||
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
breaker := o.group.Get(method).(circuitbreaker.CircuitBreaker)
|
||||
if err := breaker.Allow(); err != nil {
|
||||
// NOTE: when client reject request locally,
|
||||
// continue add counter let the drop ratio higher.
|
||||
breaker.MarkFailed()
|
||||
return nil, errcode.StatusServiceUnavailable.ToRPCErr(err.Error())
|
||||
}
|
||||
|
||||
clientStream, err := streamer(ctx, desc, cc, method, opts...)
|
||||
if err != nil {
|
||||
// NOTE: need to check internal and service unavailable error
|
||||
s, ok := status.FromError(err)
|
||||
if ok && (s.Code() == codes.Internal || s.Code() == codes.Unavailable) {
|
||||
breaker.MarkFailed()
|
||||
} else {
|
||||
breaker.MarkSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
return clientStream, err
|
||||
}
|
||||
}
|
||||
|
||||
// UnaryServerCircuitBreaker server-side unary circuit breaker interceptor
|
||||
func UnaryServerCircuitBreaker(opts ...CircuitBreakerOption) grpc.UnaryServerInterceptor {
|
||||
o := defaultCircuitBreakerOptions()
|
||||
o.apply(opts...)
|
||||
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
|
||||
breaker := o.group.Get(info.FullMethod).(circuitbreaker.CircuitBreaker)
|
||||
if err := breaker.Allow(); err != nil {
|
||||
// NOTE: when client reject request locally,
|
||||
// continue add counter let the drop ratio higher.
|
||||
breaker.MarkFailed()
|
||||
return nil, errcode.StatusServiceUnavailable.ToRPCErr(err.Error())
|
||||
}
|
||||
|
||||
reply, err := handler(ctx, req)
|
||||
if err != nil {
|
||||
// NOTE: need to check internal and service unavailable error
|
||||
s, ok := status.FromError(err)
|
||||
if ok && (s.Code() == codes.Internal || s.Code() == codes.Unavailable) {
|
||||
breaker.MarkFailed()
|
||||
} else {
|
||||
breaker.MarkSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
return reply, err
|
||||
}
|
||||
}
|
||||
|
||||
// SteamServerCircuitBreaker server-side stream circuit breaker interceptor
|
||||
func SteamServerCircuitBreaker(opts ...CircuitBreakerOption) grpc.StreamServerInterceptor {
|
||||
o := defaultCircuitBreakerOptions()
|
||||
o.apply(opts...)
|
||||
|
||||
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
breaker := o.group.Get(info.FullMethod).(circuitbreaker.CircuitBreaker)
|
||||
if err := breaker.Allow(); err != nil {
|
||||
// NOTE: when client reject request locally,
|
||||
// continue add counter let the drop ratio higher.
|
||||
breaker.MarkFailed()
|
||||
return errcode.StatusServiceUnavailable.ToRPCErr(err.Error())
|
||||
}
|
||||
|
||||
err := handler(srv, ss)
|
||||
if err != nil {
|
||||
// NOTE: need to check internal and service unavailable error
|
||||
s, ok := status.FromError(err)
|
||||
if ok && (s.Code() == codes.Internal || s.Code() == codes.Unavailable) {
|
||||
breaker.MarkFailed()
|
||||
} else {
|
||||
breaker.MarkSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package interceptor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/container/group"
|
||||
"github.com/zhufuyi/sponge/pkg/errcode"
|
||||
|
||||
"github.com/go-kratos/aegis/circuitbreaker/sre"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestUnaryClientCircuitBreaker(t *testing.T) {
|
||||
interceptor := UnaryClientCircuitBreaker(WithGroup(
|
||||
group.NewGroup(func() interface{} {
|
||||
return sre.NewBreaker()
|
||||
}),
|
||||
))
|
||||
assert.NotNil(t, interceptor)
|
||||
|
||||
ivoker := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error {
|
||||
return errcode.StatusInternalServerError.ToRPCErr()
|
||||
}
|
||||
for i := 0; i < 110; i++ {
|
||||
err := interceptor(context.Background(), "/test", nil, nil, nil, ivoker)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
ivoker = func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error {
|
||||
return errcode.StatusInvalidParams.Err()
|
||||
}
|
||||
err := interceptor(context.Background(), "/test", nil, nil, nil, ivoker)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSteamClientCircuitBreaker(t *testing.T) {
|
||||
interceptor := SteamClientCircuitBreaker()
|
||||
assert.NotNil(t, interceptor)
|
||||
|
||||
streamer := func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
return nil, errcode.StatusInternalServerError.ToRPCErr()
|
||||
}
|
||||
for i := 0; i < 110; i++ {
|
||||
_, err := interceptor(context.Background(), nil, nil, "/test", streamer)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
streamer = func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
return nil, errcode.StatusInvalidParams.Err()
|
||||
}
|
||||
_, err := interceptor(context.Background(), nil, nil, "/test", streamer)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUnaryServerCircuitBreaker(t *testing.T) {
|
||||
interceptor := UnaryServerCircuitBreaker()
|
||||
assert.NotNil(t, interceptor)
|
||||
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return nil, errcode.StatusInternalServerError.ToRPCErr()
|
||||
}
|
||||
for i := 0; i < 110; i++ {
|
||||
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "/test"}, handler)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
handler = func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return nil, errcode.StatusInvalidParams.Err()
|
||||
}
|
||||
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "/test"}, handler)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSteamServerCircuitBreaker(t *testing.T) {
|
||||
interceptor := SteamServerCircuitBreaker()
|
||||
assert.NotNil(t, interceptor)
|
||||
|
||||
handler := func(srv interface{}, stream grpc.ServerStream) error {
|
||||
return errcode.StatusInternalServerError.ToRPCErr()
|
||||
}
|
||||
for i := 0; i < 110; i++ {
|
||||
err := interceptor(nil, nil, &grpc.StreamServerInfo{FullMethod: "/test"}, handler)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
handler = func(srv interface{}, stream grpc.ServerStream) error {
|
||||
return errcode.StatusInvalidParams.Err()
|
||||
}
|
||||
err := interceptor(nil, nil, &grpc.StreamServerInfo{FullMethod: "/test"}, handler)
|
||||
assert.Error(t, err)
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package interceptor
|
||||
|
||||
import (
|
||||
"github.com/zhufuyi/sponge/pkg/grpc/hystrix"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// UnaryClientHystrix 客户端熔断器unary拦截器
|
||||
func UnaryClientHystrix(commandName string, opts ...hystrix.Option) grpc.UnaryClientInterceptor {
|
||||
return hystrix.UnaryClientInterceptor(commandName, opts...)
|
||||
}
|
||||
|
||||
// SteamClientHystrix 客户端熔断器stream拦截器
|
||||
func SteamClientHystrix(commandName string, opts ...hystrix.Option) grpc.StreamClientInterceptor {
|
||||
return hystrix.StreamClientInterceptor(commandName, opts...)
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package interceptor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUnaryClientHystrix(t *testing.T) {
|
||||
interceptor := UnaryClientHystrix("demo")
|
||||
assert.NotNil(t, interceptor)
|
||||
}
|
||||
|
||||
func TestSteamClientHystrix(t *testing.T) {
|
||||
interceptor := SteamClientHystrix("demo")
|
||||
assert.NotNil(t, interceptor)
|
||||
}
|
|
@ -1,89 +1,116 @@
|
|||
package interceptor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/ratelimit"
|
||||
"github.com/reugn/equalizer"
|
||||
"github.com/zhufuyi/sponge/pkg/errcode"
|
||||
|
||||
rl "github.com/go-kratos/aegis/ratelimit"
|
||||
"github.com/go-kratos/aegis/ratelimit/bbr"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// ---------------------------------- server interceptor ----------------------------------
|
||||
|
||||
// RateLimitOption 日志设置
|
||||
type RateLimitOption func(*rateLimitOptions)
|
||||
// ErrLimitExceed is returned when the rate limiter is
|
||||
// triggered and the request is rejected due to limit exceeded.
|
||||
var ErrLimitExceed = rl.ErrLimitExceed
|
||||
|
||||
type rateLimitOptions struct {
|
||||
qps int // 允许请求速度
|
||||
capacity int // 重新填充容量
|
||||
refillInterval time.Duration // 填充token速度,refillInterval=time.Second/qps*capacity
|
||||
// RatelimitOption set the rate limits ratelimitOptions.
|
||||
type RatelimitOption func(*ratelimitOptions)
|
||||
|
||||
type ratelimitOptions struct {
|
||||
window time.Duration
|
||||
bucket int
|
||||
cpuThreshold int64
|
||||
cpuQuota float64
|
||||
}
|
||||
|
||||
func defaultRateLimitOptions() *rateLimitOptions {
|
||||
return &rateLimitOptions{
|
||||
qps: 1000,
|
||||
capacity: 50,
|
||||
refillInterval: time.Second / 1000 * 50,
|
||||
func defaultRatelimitOptions() *ratelimitOptions {
|
||||
return &ratelimitOptions{
|
||||
window: time.Second * 10,
|
||||
bucket: 100,
|
||||
cpuThreshold: 800,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *rateLimitOptions) apply(opts ...RateLimitOption) {
|
||||
func (o *ratelimitOptions) apply(opts ...RatelimitOption) {
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
}
|
||||
|
||||
// WithRateLimitQPS 设置请求qps
|
||||
func WithRateLimitQPS(qps int) RateLimitOption {
|
||||
return func(o *rateLimitOptions) {
|
||||
o.qps = qps
|
||||
if qps < 10 {
|
||||
o.capacity = qps
|
||||
} else if qps < 100 {
|
||||
o.capacity = 10
|
||||
} else if qps < 500 {
|
||||
o.capacity = 40
|
||||
} else if qps < 1000 {
|
||||
o.capacity = 80
|
||||
} else if qps < 2000 {
|
||||
o.capacity = 100
|
||||
} else if qps < 4000 {
|
||||
o.capacity = 200
|
||||
} else if qps < 10000 {
|
||||
o.capacity = 400
|
||||
} else {
|
||||
o.capacity = 500
|
||||
// WithWindow with window size.
|
||||
func WithWindow(d time.Duration) RatelimitOption {
|
||||
return func(o *ratelimitOptions) {
|
||||
o.window = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithBucket with bucket size.
|
||||
func WithBucket(b int) RatelimitOption {
|
||||
return func(o *ratelimitOptions) {
|
||||
o.bucket = b
|
||||
}
|
||||
}
|
||||
|
||||
// WithCPUThreshold with cpu threshold
|
||||
func WithCPUThreshold(threshold int64) RatelimitOption {
|
||||
return func(o *ratelimitOptions) {
|
||||
o.cpuThreshold = threshold
|
||||
}
|
||||
}
|
||||
|
||||
// WithCPUQuota with real cpu quota(if it can not collect from process correct);
|
||||
func WithCPUQuota(quota float64) RatelimitOption {
|
||||
return func(o *ratelimitOptions) {
|
||||
o.cpuQuota = quota
|
||||
}
|
||||
}
|
||||
|
||||
// UnaryServerRateLimit server-side unary circuit breaker interceptor
|
||||
func UnaryServerRateLimit(opts ...RatelimitOption) grpc.UnaryServerInterceptor {
|
||||
o := defaultRatelimitOptions()
|
||||
o.apply(opts...)
|
||||
limiter := bbr.NewLimiter(
|
||||
bbr.WithWindow(o.window),
|
||||
bbr.WithBucket(o.bucket),
|
||||
bbr.WithCPUThreshold(o.cpuThreshold),
|
||||
bbr.WithCPUQuota(o.cpuQuota),
|
||||
)
|
||||
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
|
||||
done, err := limiter.Allow()
|
||||
if err != nil {
|
||||
return nil, errcode.StatusLimitExceed.ToRPCErr(err.Error())
|
||||
}
|
||||
o.refillInterval = time.Second / time.Duration(o.qps) * time.Duration(o.capacity)
|
||||
|
||||
reply, err := handler(ctx, req)
|
||||
done(rl.DoneInfo{Err: err})
|
||||
return reply, err
|
||||
}
|
||||
}
|
||||
|
||||
type myLimiter struct {
|
||||
TB *equalizer.TokenBucket // 令牌桶
|
||||
}
|
||||
// StreamServerRateLimit server-side stream circuit breaker interceptor
|
||||
func StreamServerRateLimit(opts ...RatelimitOption) grpc.StreamServerInterceptor {
|
||||
o := defaultRatelimitOptions()
|
||||
o.apply(opts...)
|
||||
limiter := bbr.NewLimiter(
|
||||
bbr.WithWindow(o.window),
|
||||
bbr.WithBucket(o.bucket),
|
||||
bbr.WithCPUThreshold(o.cpuThreshold),
|
||||
bbr.WithCPUQuota(o.cpuQuota),
|
||||
)
|
||||
|
||||
func (m *myLimiter) Limit() bool {
|
||||
if m.TB.Ask() {
|
||||
return false
|
||||
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
done, err := limiter.Allow()
|
||||
if err != nil {
|
||||
return errcode.StatusLimitExceed.ToRPCErr(err.Error())
|
||||
}
|
||||
|
||||
err = handler(srv, ss)
|
||||
done(rl.DoneInfo{Err: err})
|
||||
return err
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// UnaryServerRateLimit 限流unary拦截器
|
||||
func UnaryServerRateLimit(opts ...RateLimitOption) grpc.UnaryServerInterceptor {
|
||||
o := defaultRateLimitOptions()
|
||||
o.apply(opts...)
|
||||
|
||||
limiter := &myLimiter{TB: equalizer.NewTokenBucket(int32(o.capacity), o.refillInterval)}
|
||||
return ratelimit.UnaryServerInterceptor(limiter)
|
||||
}
|
||||
|
||||
// StreamServerRateLimit 限流stream拦截器
|
||||
func StreamServerRateLimit(opts ...RateLimitOption) grpc.StreamServerInterceptor {
|
||||
o := defaultRateLimitOptions()
|
||||
o.apply(opts...)
|
||||
|
||||
limiter := &myLimiter{equalizer.NewTokenBucket(int32(o.capacity), o.refillInterval)}
|
||||
return ratelimit.StreamServerInterceptor(limiter)
|
||||
}
|
||||
|
|
|
@ -1,55 +1,36 @@
|
|||
package interceptor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/reugn/equalizer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUnaryServerRateLimit(t *testing.T) {
|
||||
interceptor := UnaryServerRateLimit(
|
||||
WithWindow(time.Second*10),
|
||||
WithBucket(200),
|
||||
WithCPUThreshold(500),
|
||||
WithCPUQuota(0.5),
|
||||
)
|
||||
assert.NotNil(t, interceptor)
|
||||
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
_, err := interceptor(nil, nil, nil, handler)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestStreamServerRateLimit(t *testing.T) {
|
||||
interceptor := StreamServerRateLimit()
|
||||
assert.NotNil(t, interceptor)
|
||||
}
|
||||
|
||||
func TestUnaryServerRateLimit(t *testing.T) {
|
||||
interceptor := UnaryServerRateLimit()
|
||||
assert.NotNil(t, interceptor)
|
||||
}
|
||||
|
||||
func TestWithRateLimitQPS(t *testing.T) {
|
||||
testData := 1000
|
||||
opt := WithRateLimitQPS(testData)
|
||||
o := new(rateLimitOptions)
|
||||
o.apply(opt)
|
||||
assert.Less(t, time.Duration(testData), o.refillInterval)
|
||||
|
||||
_ = WithRateLimitQPS(5)
|
||||
_ = WithRateLimitQPS(55)
|
||||
_ = WithRateLimitQPS(255)
|
||||
_ = WithRateLimitQPS(555)
|
||||
_ = WithRateLimitQPS(1555)
|
||||
_ = WithRateLimitQPS(2555)
|
||||
_ = WithRateLimitQPS(5555)
|
||||
_ = WithRateLimitQPS(55555)
|
||||
}
|
||||
|
||||
func Test_defaultRateLimitOptions(t *testing.T) {
|
||||
o := defaultRateLimitOptions()
|
||||
assert.NotNil(t, o)
|
||||
}
|
||||
|
||||
func Test_rateLimitOptions_apply(t *testing.T) {
|
||||
testData := 1000
|
||||
opt := WithRateLimitQPS(testData)
|
||||
o := new(rateLimitOptions)
|
||||
o.apply(opt)
|
||||
assert.Less(t, time.Duration(testData), o.refillInterval)
|
||||
}
|
||||
|
||||
func Test_myLimiter_Limit(t *testing.T) {
|
||||
l := &myLimiter{equalizer.NewTokenBucket(100, 50)}
|
||||
actual := l.Limit()
|
||||
assert.Equal(t, false, actual)
|
||||
handler := func(srv interface{}, stream grpc.ServerStream) error {
|
||||
return nil
|
||||
}
|
||||
err := interceptor(nil, nil, nil, handler)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
var (
|
||||
// 默认触发重试的错误码
|
||||
defaultErrCodes = []codes.Code{codes.Unavailable}
|
||||
defaultErrCodes = []codes.Code{codes.Internal}
|
||||
)
|
||||
|
||||
// RetryOption set the retry retryOptions.
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
## jy2struct
|
||||
|
||||
json和yaml转go struct代码。
|
||||
|
||||
<br>
|
||||
|
||||
### 安装
|
||||
|
||||
> go get -u github.com/zhufuyi/pkg/jy2struct
|
||||
|
||||
<br>
|
||||
|
||||
### 使用示例
|
||||
|
||||
主要设置参数:
|
||||
|
||||
```go
|
||||
type Args struct {
|
||||
Format string // 文档格式,json或yaml
|
||||
Data string // json或yaml内容
|
||||
InputFile string // 文件
|
||||
Name string // 结构体名称
|
||||
SubStruct bool // 子结构体是否分开
|
||||
Tags string // 添加额外tag,多个tag用逗号分隔
|
||||
}
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
转换示例:
|
||||
|
||||
```go
|
||||
// json转struct
|
||||
code, err := jy2struct.Covert(&jy2struct.Args{
|
||||
Format: "json",
|
||||
// InputFile: "user.json", // 来源于json文件
|
||||
SubStruct: true,
|
||||
})
|
||||
|
||||
// json转struct
|
||||
code, err := jy2struct.Covert(&jy2struct.Args{
|
||||
Format: "yaml",
|
||||
// InputFile: "user.yaml", // 来源于yaml文件
|
||||
SubStruct: true,
|
||||
})
|
||||
```
|
|
@ -0,0 +1,77 @@
|
|||
package jy2struct
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Args 参数
|
||||
type Args struct {
|
||||
Format string // 文档格式,json或yaml
|
||||
Data string // json或yaml内容
|
||||
InputFile string // 文件
|
||||
Name string // 结构体名称
|
||||
SubStruct bool // 子结构体是否分开
|
||||
Tags string // 字段tag,多个tag用逗号分隔
|
||||
|
||||
tags []string
|
||||
convertFloats bool
|
||||
parser Parser
|
||||
}
|
||||
|
||||
func (j *Args) checkValid() error {
|
||||
switch j.Format {
|
||||
case "json":
|
||||
j.parser = ParseJSON
|
||||
j.convertFloats = true
|
||||
case "yaml":
|
||||
j.parser = ParseYaml
|
||||
default:
|
||||
return errors.New("format must be json or yaml")
|
||||
}
|
||||
|
||||
j.tags = []string{j.Format}
|
||||
tags := strings.Split(j.Tags, ",")
|
||||
for _, tag := range tags {
|
||||
if tag == j.Format || tag == "" {
|
||||
continue
|
||||
}
|
||||
j.tags = append(j.tags, tag)
|
||||
}
|
||||
|
||||
if j.Name == "" {
|
||||
j.Name = "GenerateName"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Covert json或yaml转go struct
|
||||
func Covert(args *Args) (string, error) {
|
||||
err := args.checkValid()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if args.Data != "" {
|
||||
data = []byte(args.Data)
|
||||
} else {
|
||||
// 读取文件
|
||||
data, err = os.ReadFile(args.InputFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
input := bytes.NewReader(data)
|
||||
|
||||
output, err := jyParse(input, args.parser, args.Name, "main", args.tags, args.SubStruct, args.convertFloats)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package jy2struct
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCovert(t *testing.T) {
|
||||
type args struct {
|
||||
args *Args
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "json to struct from data",
|
||||
args: args{args: &Args{
|
||||
Data: `{"name":"foo","age":11}`,
|
||||
Format: "json",
|
||||
}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "yaml to struct from data",
|
||||
args: args{args: &Args{
|
||||
Data: `name: "foo"
|
||||
age: 10`,
|
||||
Format: "yaml",
|
||||
}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json to struct from file",
|
||||
args: args{args: &Args{
|
||||
InputFile: "test.json",
|
||||
Format: "json",
|
||||
SubStruct: true,
|
||||
Tags: "gorm",
|
||||
}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "yaml to struct from file",
|
||||
args: args{args: &Args{
|
||||
InputFile: "test.yaml",
|
||||
Format: "yaml",
|
||||
SubStruct: true,
|
||||
}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json to slice from data",
|
||||
args: args{args: &Args{
|
||||
Data: `[{"name":"foo","age":11},{"name":"foo2","age":22}]`,
|
||||
Format: "json",
|
||||
}},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Covert(tt.args.args)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Covert() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
t.Log(got)
|
||||
})
|
||||
}
|
||||
|
||||
// test Covert error
|
||||
arg := &Args{Format: "unknown"}
|
||||
_, err := Covert(arg)
|
||||
assert.Error(t, err)
|
||||
arg = &Args{Format: "yaml", InputFile: "notfound.yaml"}
|
||||
_, err = Covert(arg)
|
||||
assert.Error(t, err)
|
||||
}
|
|
@ -0,0 +1,493 @@
|
|||
package jy2struct
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ForceFloats whether to force a change to float
|
||||
var ForceFloats bool
|
||||
|
||||
// commonInitialisms is a set of common initialisms.
|
||||
// Only add entries that are highly unlikely to be non-initialisms.
|
||||
// For instance, "ID" is fine (Freudian code is rare), but "AND" is not.
|
||||
var commonInitialisms = map[string]bool{
|
||||
"API": true,
|
||||
"ASCII": true,
|
||||
"CPU": true,
|
||||
"CSS": true,
|
||||
"DNS": true,
|
||||
"EOF": true,
|
||||
"GUID": true,
|
||||
"HTML": true,
|
||||
"HTTP": true,
|
||||
"HTTPS": true,
|
||||
"ID": true,
|
||||
"IP": true,
|
||||
"JSON": true,
|
||||
"LHS": true,
|
||||
"QPS": true,
|
||||
"RAM": true,
|
||||
"RHS": true,
|
||||
"RPC": true,
|
||||
"SLA": true,
|
||||
"SMTP": true,
|
||||
"SSH": true,
|
||||
"TLS": true,
|
||||
"TTL": true,
|
||||
"UI": true,
|
||||
"UID": true,
|
||||
"UUID": true,
|
||||
"URI": true,
|
||||
"URL": true,
|
||||
"UTF8": true,
|
||||
"VM": true,
|
||||
"XML": true,
|
||||
"NTP": true,
|
||||
"DB": true,
|
||||
}
|
||||
|
||||
var intToWordMap = []string{
|
||||
"zero",
|
||||
"one",
|
||||
"two",
|
||||
"three",
|
||||
"four",
|
||||
"five",
|
||||
"six",
|
||||
"seven",
|
||||
"eight",
|
||||
"nine",
|
||||
}
|
||||
|
||||
// Parser parser function
|
||||
type Parser func(io.Reader) (interface{}, error)
|
||||
|
||||
// ParseJSON parse json to struct
|
||||
func ParseJSON(input io.Reader) (interface{}, error) {
|
||||
var result interface{}
|
||||
if err := json.NewDecoder(input).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ParseYaml parse yaml to struct
|
||||
func ParseYaml(input io.Reader) (interface{}, error) {
|
||||
var result interface{}
|
||||
b, err := readFile(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := yaml.Unmarshal(b, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func readFile(input io.Reader) ([]byte, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
_, err := io.Copy(buf, input)
|
||||
if err != nil {
|
||||
return []byte{}, nil
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// json or yaml parse
|
||||
func jyParse(input io.Reader, parser Parser, structName, pkgName string, tags []string, subStruct bool, convertFloats bool) ([]byte, error) {
|
||||
var subStructMap map[string]string = nil
|
||||
if subStruct {
|
||||
subStructMap = make(map[string]string)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
|
||||
iresult, err := parser(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch iresult := iresult.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
result = convertKeysToStrings(iresult)
|
||||
case map[string]interface{}:
|
||||
result = iresult
|
||||
case []interface{}:
|
||||
//src := fmt.Sprintf("package %s\n\ntype %s %s\n", pkgName, structName, typeForValue(iresult, structName, tags, subStructMap, convertFloats))
|
||||
src := fmt.Sprintf("\ntype %s %s\n", structName, typeForValue(iresult, structName, tags, subStructMap, convertFloats))
|
||||
// 补上子结构体
|
||||
for k, v := range subStructMap {
|
||||
src += fmt.Sprintf("\n\ntype %s %s\n\n", v, k)
|
||||
}
|
||||
var formatted []byte
|
||||
formatted, err = format.Source([]byte(src))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error formatting: %s, was formatting\n%s", err, src)
|
||||
}
|
||||
return formatted, err
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type: %T", iresult)
|
||||
}
|
||||
|
||||
//src := fmt.Sprintf("package %s\ntype %s %s}", pkgName, structName, generateTypes(result, structName, tags, 0, subStructMap, convertFloats))
|
||||
src := fmt.Sprintf("\ntype %s %s}", structName, generateTypes(result, structName, tags, 0, subStructMap, convertFloats))
|
||||
|
||||
keys := make([]string, 0, len(subStructMap))
|
||||
for key := range subStructMap {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
src = fmt.Sprintf("%v\n\ntype %v %v", src, subStructMap[k], k)
|
||||
}
|
||||
|
||||
formatted, err := format.Source([]byte(src))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error formatting: %s, was formatting\n%s", err, src)
|
||||
}
|
||||
return formatted, err
|
||||
}
|
||||
|
||||
func convertKeysToStrings(obj map[interface{}]interface{}) map[string]interface{} {
|
||||
res := make(map[string]interface{})
|
||||
|
||||
for k, v := range obj {
|
||||
res[fmt.Sprintf("%v", k)] = v
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// jyParse go struct entries for a map[string]interface{} structure
|
||||
func generateTypes(obj map[string]interface{}, structName string, tags []string, depth int, subStructMap map[string]string, convertFloats bool) string {
|
||||
structure := "struct {"
|
||||
|
||||
keys := make([]string, 0, len(obj))
|
||||
for key := range obj {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
value := obj[key]
|
||||
valueType := typeForValue(value, structName, tags, subStructMap, convertFloats)
|
||||
|
||||
//value = mergeElements(value)
|
||||
//If a nested value, recurse
|
||||
switch value := value.(type) {
|
||||
case []interface{}:
|
||||
if len(value) > 0 {
|
||||
sub := ""
|
||||
if v, ok := value[0].(map[interface{}]interface{}); ok {
|
||||
sub = generateTypes(convertKeysToStrings(v), structName, tags, depth+1, subStructMap, convertFloats) + "}"
|
||||
} else if v, ok := value[0].(map[string]interface{}); ok {
|
||||
sub = generateTypes(v, structName, tags, depth+1, subStructMap, convertFloats) + "}"
|
||||
}
|
||||
|
||||
if sub != "" {
|
||||
subName := sub
|
||||
|
||||
if subStructMap != nil {
|
||||
if val, ok := subStructMap[sub]; ok {
|
||||
subName = val
|
||||
} else {
|
||||
//subName = fmt.Sprintf("%v_sub%v", structName, len(subStructMap)+1)
|
||||
subName = FmtFieldName(key) // 使用字段名字
|
||||
subStructMap[sub] = subName
|
||||
}
|
||||
}
|
||||
|
||||
valueType = "[]" + subName
|
||||
}
|
||||
}
|
||||
case map[interface{}]interface{}:
|
||||
sub := generateTypes(convertKeysToStrings(value), structName, tags, depth+1, subStructMap, convertFloats) + "}"
|
||||
subName := sub
|
||||
|
||||
if subStructMap != nil {
|
||||
if val, ok := subStructMap[sub]; ok {
|
||||
subName = val
|
||||
} else {
|
||||
//subName = fmt.Sprintf("%v_sub%v", structName, len(subStructMap)+1)
|
||||
subName = FmtFieldName(key) // 使用字段名字
|
||||
subStructMap[sub] = subName
|
||||
}
|
||||
}
|
||||
valueType = subName
|
||||
case map[string]interface{}:
|
||||
sub := generateTypes(value, structName, tags, depth+1, subStructMap, convertFloats) + "}"
|
||||
subName := sub
|
||||
|
||||
if subStructMap != nil {
|
||||
if val, ok := subStructMap[sub]; ok {
|
||||
subName = val
|
||||
} else {
|
||||
//subName = fmt.Sprintf("%v_sub%v", structName, len(subStructMap)+1)
|
||||
subName = FmtFieldName(key) // 使用字段名字
|
||||
subStructMap[sub] = subName
|
||||
}
|
||||
}
|
||||
|
||||
valueType = subName
|
||||
}
|
||||
|
||||
fieldName := FmtFieldName(key)
|
||||
|
||||
tagList := make([]string, 0)
|
||||
for _, t := range tags {
|
||||
tagList = append(tagList, fmt.Sprintf("%s:\"%s\"", t, key))
|
||||
}
|
||||
|
||||
structure += fmt.Sprintf("\n%s %s `%s`",
|
||||
fieldName,
|
||||
valueType,
|
||||
strings.Join(tagList, " "))
|
||||
}
|
||||
return structure
|
||||
}
|
||||
|
||||
// FmtFieldName formats a string as a struct key
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// FmtFieldName("foo_id")
|
||||
//
|
||||
// Output: FooID
|
||||
func FmtFieldName(s string) string {
|
||||
runes := []rune(s)
|
||||
for len(runes) > 0 && !unicode.IsLetter(runes[0]) && !unicode.IsDigit(runes[0]) {
|
||||
runes = runes[1:]
|
||||
}
|
||||
if len(runes) == 0 {
|
||||
return "_"
|
||||
}
|
||||
|
||||
s = stringifyFirstChar(string(runes))
|
||||
name := lintFieldName(s)
|
||||
runes = []rune(name)
|
||||
for i, c := range runes {
|
||||
ok := unicode.IsLetter(c) || unicode.IsDigit(c)
|
||||
if i == 0 {
|
||||
ok = unicode.IsLetter(c)
|
||||
}
|
||||
if !ok {
|
||||
runes[i] = '_'
|
||||
}
|
||||
}
|
||||
s = string(runes)
|
||||
s = strings.Trim(s, "_")
|
||||
if len(s) == 0 {
|
||||
return "_"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// nolint
|
||||
func lintFieldName(name string) string {
|
||||
// Fast path for simple cases: "_" and all lowercase.
|
||||
if name == "_" {
|
||||
return name
|
||||
}
|
||||
|
||||
allLower := true
|
||||
for _, r := range name {
|
||||
if !unicode.IsLower(r) {
|
||||
allLower = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allLower {
|
||||
runes := []rune(name)
|
||||
if u := strings.ToUpper(name); commonInitialisms[u] {
|
||||
copy(runes[0:], []rune(u))
|
||||
} else {
|
||||
runes[0] = unicode.ToUpper(runes[0])
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
allUpperWithUnderscore := true
|
||||
for _, r := range name {
|
||||
if !unicode.IsUpper(r) && r != '_' {
|
||||
allUpperWithUnderscore = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allUpperWithUnderscore {
|
||||
name = strings.ToLower(name)
|
||||
}
|
||||
|
||||
// Split camelCase at any lower->upper transition, and split on underscores.
|
||||
// Check each word for common initialisms.
|
||||
runes := []rune(name)
|
||||
w, i := 0, 0 // index of start of word, scan
|
||||
for i+1 <= len(runes) {
|
||||
eow := false // whether we hit the end of a word
|
||||
|
||||
if i+1 == len(runes) {
|
||||
eow = true
|
||||
} else if runes[i+1] == '_' {
|
||||
// underscore; shift the remainder forward over any run of underscores
|
||||
eow = true
|
||||
n := 1
|
||||
for i+n+1 < len(runes) && runes[i+n+1] == '_' {
|
||||
n++
|
||||
}
|
||||
|
||||
// Leave at most one underscore if the underscore is between two digits
|
||||
if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) {
|
||||
n--
|
||||
}
|
||||
|
||||
copy(runes[i+1:], runes[i+n+1:])
|
||||
runes = runes[:len(runes)-n]
|
||||
} else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) {
|
||||
// lower->non-lower
|
||||
eow = true
|
||||
}
|
||||
i++
|
||||
if !eow {
|
||||
continue
|
||||
}
|
||||
|
||||
// [w,i) is a word.
|
||||
word := string(runes[w:i])
|
||||
if u := strings.ToUpper(word); commonInitialisms[u] {
|
||||
// All the common initialisms are ASCII,
|
||||
// so we can replace the bytes exactly.
|
||||
copy(runes[w:], []rune(u))
|
||||
} else if strings.ToLower(word) == word {
|
||||
// already all lowercase, and not the first word, so uppercase the first character.
|
||||
runes[w] = unicode.ToUpper(runes[w])
|
||||
}
|
||||
w = i
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// generate an appropriate struct type entry
|
||||
func typeForValue(value interface{}, structName string, tags []string, subStructMap map[string]string, convertFloats bool) string {
|
||||
//Check if this is an array
|
||||
if objects, ok := value.([]interface{}); ok {
|
||||
types := make(map[reflect.Type]bool, 0)
|
||||
for _, o := range objects {
|
||||
types[reflect.TypeOf(o)] = true
|
||||
}
|
||||
if len(types) == 1 {
|
||||
return "[]" + typeForValue(mergeElements(objects).([]interface{})[0], structName, tags, subStructMap, convertFloats)
|
||||
}
|
||||
return "[]interface{}"
|
||||
} else if object, ok := value.(map[interface{}]interface{}); ok {
|
||||
return generateTypes(convertKeysToStrings(object), structName, tags, 0, subStructMap, convertFloats) + "}"
|
||||
} else if object, ok := value.(map[string]interface{}); ok {
|
||||
return generateTypes(object, structName, tags, 0, subStructMap, convertFloats) + "}"
|
||||
} else if reflect.TypeOf(value) == nil {
|
||||
return "interface{}"
|
||||
}
|
||||
v := reflect.TypeOf(value).Name()
|
||||
if v == "float64" && convertFloats {
|
||||
v = disambiguateFloatInt(value)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// All numbers will initially be read as float64
|
||||
// If the number appears to be an integer value, use int instead
|
||||
func disambiguateFloatInt(value interface{}) string {
|
||||
const epsilon = .0001
|
||||
vfloat := value.(float64)
|
||||
if !ForceFloats && math.Abs(vfloat-math.Floor(vfloat+epsilon)) < epsilon {
|
||||
var tmp int64
|
||||
return reflect.TypeOf(tmp).Name()
|
||||
}
|
||||
return reflect.TypeOf(value).Name()
|
||||
}
|
||||
|
||||
// convert first character ints to strings
|
||||
func stringifyFirstChar(str string) string {
|
||||
first := str[:1]
|
||||
|
||||
i, err := strconv.ParseInt(first, 10, 8)
|
||||
if err != nil {
|
||||
return str
|
||||
}
|
||||
|
||||
return intToWordMap[i] + "_" + str[1:]
|
||||
}
|
||||
|
||||
func mergeElements(i interface{}) interface{} {
|
||||
switch i := i.(type) {
|
||||
default:
|
||||
return i
|
||||
case []interface{}:
|
||||
l := len(i)
|
||||
if l == 0 {
|
||||
return i
|
||||
}
|
||||
for j := 1; j < l; j++ {
|
||||
i[0] = mergeObjects(i[0], i[j])
|
||||
}
|
||||
return i[0:1]
|
||||
}
|
||||
}
|
||||
|
||||
func mergeObjects(o1, o2 interface{}) interface{} {
|
||||
if o1 == nil {
|
||||
return o2
|
||||
}
|
||||
|
||||
if o2 == nil {
|
||||
return o1
|
||||
}
|
||||
|
||||
if reflect.TypeOf(o1) != reflect.TypeOf(o2) {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch i := o1.(type) {
|
||||
default:
|
||||
return o1
|
||||
case []interface{}:
|
||||
if i2, ok := o2.([]interface{}); ok {
|
||||
i3 := append(i, i2...)
|
||||
return mergeElements(i3)
|
||||
}
|
||||
return mergeElements(i)
|
||||
case map[string]interface{}:
|
||||
if i2, ok := o2.(map[string]interface{}); ok {
|
||||
for k, v := range i2 {
|
||||
if v2, ok := i[k]; ok {
|
||||
i[k] = mergeObjects(v2, v)
|
||||
} else {
|
||||
i[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return i
|
||||
case map[interface{}]interface{}:
|
||||
if i2, ok := o2.(map[interface{}]interface{}); ok {
|
||||
for k, v := range i2 {
|
||||
if v2, ok := i[k]; ok {
|
||||
i[k] = mergeObjects(v2, v)
|
||||
} else {
|
||||
i[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package jy2struct
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseError(t *testing.T) {
|
||||
testData := `foo:bar`
|
||||
r := strings.NewReader(testData)
|
||||
_, err := ParseJSON(r)
|
||||
assert.Error(t, err)
|
||||
|
||||
testData = ` foo: bar`
|
||||
r = strings.NewReader(testData)
|
||||
_, err = ParseYaml(r)
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = jyParse(r, ParseYaml, "", "", nil, false, false)
|
||||
assert.Error(t, err)
|
||||
|
||||
v := FmtFieldName("")
|
||||
v = lintFieldName(v)
|
||||
assert.Equal(t, "_", v)
|
||||
|
||||
v = stringifyFirstChar("2foo")
|
||||
assert.Equal(t, "two_foo", v)
|
||||
}
|
||||
|
||||
func Test_convertKeysToStrings(t *testing.T) {
|
||||
testData := map[interface{}]interface{}{"foo": "bar"}
|
||||
v := convertKeysToStrings(testData)
|
||||
assert.NotNil(t, v)
|
||||
}
|
||||
|
||||
func Test_mergeElements(t *testing.T) {
|
||||
testData := "foo"
|
||||
v := mergeElements(testData)
|
||||
assert.Equal(t, testData, v)
|
||||
|
||||
testData2 := []interface{}{}
|
||||
v = mergeElements(testData2)
|
||||
assert.Empty(t, v)
|
||||
testData2 = []interface{}{"foo", "bar"}
|
||||
v = mergeElements(testData2)
|
||||
assert.Equal(t, testData2[0], v.([]interface{})[0])
|
||||
}
|
||||
|
||||
func Test_mergeObjects(t *testing.T) {
|
||||
var (
|
||||
o1 = []interface{}{"foo", "bar"}
|
||||
o2 = map[string]interface{}{"foo": "bar"}
|
||||
o3 = map[interface{}]interface{}{"foo": "bar"}
|
||||
)
|
||||
v := mergeObjects(nil, o2)
|
||||
assert.Equal(t, o2, v)
|
||||
v = mergeObjects(o1, nil)
|
||||
assert.Equal(t, o1, v)
|
||||
v = mergeObjects(o1, o2)
|
||||
assert.Nil(t, v)
|
||||
v = mergeObjects("foo", "bar")
|
||||
assert.Equal(t, "foo", v)
|
||||
|
||||
v = mergeObjects(o1, o1)
|
||||
t.Log(v)
|
||||
v = mergeObjects(o2, o2)
|
||||
t.Log(v)
|
||||
v = mergeObjects(o3, o3)
|
||||
t.Log(v)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
[
|
||||
{
|
||||
"name": "foo",
|
||||
"age": 10,
|
||||
"email": "foo@bar.com",
|
||||
"companies": [
|
||||
{
|
||||
"name":"foo",
|
||||
"address":"foo",
|
||||
"position": "foo"
|
||||
},
|
||||
{
|
||||
"name":"foo2",
|
||||
"address":"foo2",
|
||||
"position": "foo2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
serverName: "demo"
|
||||
serverPort: 8080
|
||||
runMode: "dev"
|
||||
|
||||
# jwt配置
|
||||
jwt:
|
||||
signingKey: "abcd"
|
||||
expire: 86400 # 单位(秒)
|
||||
|
||||
# email配置
|
||||
email:
|
||||
sender: "foo@bar.com"
|
||||
password: "1234"
|
||||
|
||||
# 日志配置
|
||||
log:
|
||||
level: "debug" # 输出日志级别 debug, info, warn, error,默认是debug
|
||||
format: "console" # 输出格式,console或json,默认是console
|
||||
isSave: true # false:输出到终端,true:输出到文件,默认是false
|
||||
|
|
@ -2,8 +2,11 @@ package nacos
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/zhufuyi/sponge/pkg/registry"
|
||||
"testing"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/registry"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newNacosRegistry() *Registry {
|
||||
|
@ -32,3 +35,25 @@ func TestRegistry(t *testing.T) {
|
|||
_, err = r.Watch(context.Background(), "foo")
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func TestRegistry_Register(t *testing.T) {
|
||||
r := newNacosRegistry()
|
||||
instance := registry.NewServiceInstance("", []string{"grpc://127.0.0.1:8282"})
|
||||
err := r.Register(context.Background(), instance)
|
||||
assert.Error(t, err)
|
||||
|
||||
instance = registry.NewServiceInstance("foo", []string{"grpc://127.0.0.1:8282"},
|
||||
registry.WithMetadata(map[string]string{
|
||||
"foo2": "bar2",
|
||||
}))
|
||||
err = r.Register(context.Background(), instance)
|
||||
assert.Error(t, err)
|
||||
|
||||
instance = registry.NewServiceInstance("foo", []string{"127.0.0.1:port"})
|
||||
err = r.Register(context.Background(), instance)
|
||||
assert.Error(t, err)
|
||||
|
||||
instance = registry.NewServiceInstance("foo", []string{"127.0.0.1"})
|
||||
err = r.Register(context.Background(), instance)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
## replace
|
||||
|
||||
一个替换目录下文件内容库,支持本地目录下文件和通过embed嵌入目录文件替换。
|
||||
|
||||
<br>
|
||||
|
||||
### 安装
|
||||
|
||||
> go get -u github.com/zhufuyi/pkg/replacer
|
||||
|
||||
<br>
|
||||
|
||||
### 使用示例
|
||||
|
||||
```go
|
||||
//go:embed dir
|
||||
var fs embed.FS
|
||||
|
||||
func demo(){
|
||||
//r, err := replacer.New("dir")
|
||||
//if err != nil {
|
||||
// panic(err)
|
||||
//}
|
||||
r, err := replacer.NewWithFS("dir", fs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ignoreFiles := []string{}
|
||||
fields := []replacer.Field{
|
||||
{
|
||||
Old: "1234",
|
||||
New: "8080",
|
||||
},
|
||||
{
|
||||
Old: "abcde",
|
||||
New: "hello",
|
||||
IsCaseSensitive: true, // abcde-->hello, Abcde-->Hello
|
||||
},
|
||||
}
|
||||
r.SetSubDirs(subPaths...) // 只处理指定子目录,优先级最高
|
||||
r.SetIgnoreDirs(ignoreDirs...) // 指定子目录下忽略处理的目录
|
||||
r.SetIgnoreFiles(ignoreFiles...) // 指定子目录下忽略处理的文件
|
||||
r.SetReplacementFields(fields) // 设置替换文本
|
||||
r.SetOutPath("", "test") // 设置输出目录,如果为空,根据名称和时间生成文件输出文件夹
|
||||
err = r.SaveFiles() // 保存替换后文件
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("save files successfully, out = %s\n", replacer.GetOutPath())
|
||||
}
|
||||
```
|
|
@ -0,0 +1,383 @@
|
|||
package replacer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zhufuyi/sponge/pkg/gofile"
|
||||
)
|
||||
|
||||
var _ Replacer = (*replacerInfo)(nil)
|
||||
|
||||
// Replacer 接口
|
||||
type Replacer interface {
|
||||
SetReplacementFields(fields []Field)
|
||||
SetIgnoreFiles(filenames ...string)
|
||||
SetIgnoreSubDirs(dirs ...string)
|
||||
SetSubDirs(subDirs ...string)
|
||||
SetOutputDir(absDir string, name ...string) error
|
||||
GetOutputDir() string
|
||||
GetSourcePath() string
|
||||
SaveFiles() error
|
||||
ReadFile(filename string) ([]byte, error)
|
||||
}
|
||||
|
||||
// replacerInfo replacer信息
|
||||
type replacerInfo struct {
|
||||
path string // 模板目录或文件
|
||||
fs embed.FS // 模板目录对应二进制对象
|
||||
isActual bool // fs字段是否来源实际路径,如果为true,使用io操作文件,如果为false使用fs操作文件
|
||||
files []string // 模板文件列表
|
||||
ignoreFiles []string // 忽略替换的文件列表
|
||||
ignoreDirs []string // 忽略处理的子目录
|
||||
replacementFields []Field // 从模板文件转为新文件需要替换的字符
|
||||
outPath string // 输出替换后文件存放目录路径
|
||||
}
|
||||
|
||||
// New 根据指定路径创建replacer
|
||||
func New(path string) (Replacer, error) {
|
||||
files, err := gofile.ListFiles(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path, _ = filepath.Abs(path)
|
||||
return &replacerInfo{
|
||||
path: path,
|
||||
isActual: true,
|
||||
files: files,
|
||||
replacementFields: []Field{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewFS 根据嵌入的路径创建replacer
|
||||
func NewFS(path string, fs embed.FS) (Replacer, error) {
|
||||
files, err := listFiles(path, fs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &replacerInfo{
|
||||
path: path,
|
||||
fs: fs,
|
||||
isActual: false,
|
||||
files: files,
|
||||
replacementFields: []Field{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Field 替换字段信息
|
||||
type Field struct {
|
||||
Old string // 模板字段
|
||||
New string // 新字段
|
||||
IsCaseSensitive bool // 第一个字母是否区分大小写
|
||||
}
|
||||
|
||||
// SetReplacementFields 设置替换字段,注:old字符尽量不要存在包含关系,如果存在,在设置Field时注意先后顺序
|
||||
func (r *replacerInfo) SetReplacementFields(fields []Field) {
|
||||
var newFields []Field
|
||||
for _, field := range fields {
|
||||
if field.IsCaseSensitive && isFirstAlphabet(field.Old) { // 拆分首字母大小写两个字段
|
||||
newFields = append(newFields,
|
||||
Field{ // 把第一个字母转为大写
|
||||
Old: strings.ToUpper(field.Old[:1]) + field.Old[1:],
|
||||
New: strings.ToUpper(field.New[:1]) + field.New[1:],
|
||||
},
|
||||
Field{ // 把第一个字母转为小写
|
||||
Old: strings.ToLower(field.Old[:1]) + field.Old[1:],
|
||||
New: strings.ToLower(field.New[:1]) + field.New[1:],
|
||||
},
|
||||
)
|
||||
} else {
|
||||
newFields = append(newFields, field)
|
||||
}
|
||||
}
|
||||
r.replacementFields = newFields
|
||||
}
|
||||
|
||||
// SetSubDirs 设置处理指定子目录,其他目录下文件忽略处理
|
||||
func (r *replacerInfo) SetSubDirs(subDirs ...string) {
|
||||
if len(subDirs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
subDirs = r.covertPathsDelimiter(subDirs...)
|
||||
|
||||
var files []string
|
||||
isExistFile := make(map[string]struct{})
|
||||
for _, file := range r.files {
|
||||
for _, dir := range subDirs {
|
||||
if isSubPath(file, dir) {
|
||||
// 避免重复文件
|
||||
if _, ok := isExistFile[file]; ok {
|
||||
continue
|
||||
} else {
|
||||
isExistFile[file] = struct{}{}
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return
|
||||
}
|
||||
r.files = files
|
||||
}
|
||||
|
||||
// SetIgnoreFiles 设置忽略处理的文件
|
||||
func (r *replacerInfo) SetIgnoreFiles(filenames ...string) {
|
||||
r.ignoreFiles = append(r.ignoreFiles, filenames...)
|
||||
}
|
||||
|
||||
// SetIgnoreSubDirs 设置忽略处理的子目录
|
||||
func (r *replacerInfo) SetIgnoreSubDirs(dirs ...string) {
|
||||
dirs = r.covertPathsDelimiter(dirs...)
|
||||
r.ignoreDirs = append(r.ignoreDirs, dirs...)
|
||||
}
|
||||
|
||||
// SetOutputDir 设置输出目录,优先使用absPath,如果absPath为空,自动在当前目录根据参数name名称生成输出目录
|
||||
func (r *replacerInfo) SetOutputDir(absPath string, name ...string) error {
|
||||
// 使用指定输出目录
|
||||
if absPath != "" {
|
||||
abs, err := filepath.Abs(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.outPath = abs
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用当前目录
|
||||
subPath := ""
|
||||
if len(name) > 0 && name[0] != "" {
|
||||
subPath = name[0]
|
||||
}
|
||||
r.outPath = gofile.GetRunPath() + gofile.GetPathDelimiter() + subPath + "_" + time.Now().Format("150405")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOutputDir 获取输出目录
|
||||
func (r *replacerInfo) GetOutputDir() string {
|
||||
return r.outPath
|
||||
}
|
||||
|
||||
// GetSourcePath 获取源路径
|
||||
func (r *replacerInfo) GetSourcePath() string {
|
||||
return r.path
|
||||
}
|
||||
|
||||
// ReadFile 读取文件内容
|
||||
func (r *replacerInfo) ReadFile(filename string) ([]byte, error) {
|
||||
filename = r.covertPathDelimiter(filename)
|
||||
|
||||
foundFile := []string{}
|
||||
for _, file := range r.files {
|
||||
if strings.Contains(file, filename) && gofile.GetFilename(file) == gofile.GetFilename(filename) {
|
||||
foundFile = append(foundFile, file)
|
||||
}
|
||||
}
|
||||
if len(foundFile) != 1 {
|
||||
return nil, fmt.Errorf("total %d file named '%s', files=%+v", len(foundFile), filename, foundFile)
|
||||
}
|
||||
|
||||
if r.isActual {
|
||||
return os.ReadFile(foundFile[0])
|
||||
}
|
||||
return r.fs.ReadFile(foundFile[0])
|
||||
}
|
||||
|
||||
// SaveFiles 导出文件
|
||||
func (r *replacerInfo) SaveFiles() error {
|
||||
if r.outPath == "" {
|
||||
r.outPath = gofile.GetRunPath() + gofile.GetPathDelimiter() + "generate_" + time.Now().Format("150405")
|
||||
}
|
||||
|
||||
var existFiles []string
|
||||
var writeData = make(map[string][]byte)
|
||||
|
||||
for _, file := range r.files {
|
||||
if r.isInIgnoreDir(file) || r.isIgnoreFile(file) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 从二进制读取模板文件内容使用embed.FS,如果要从指定目录读取使用os.ReadFile
|
||||
var data []byte
|
||||
var err error
|
||||
if r.isActual {
|
||||
data, err = os.ReadFile(file)
|
||||
} else {
|
||||
data, err = r.fs.ReadFile(file)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 替换文本内容
|
||||
for _, field := range r.replacementFields {
|
||||
data = bytes.ReplaceAll(data, []byte(field.Old), []byte(field.New))
|
||||
}
|
||||
|
||||
// 获取新文件路径
|
||||
newFilePath := r.getNewFilePath(file)
|
||||
dir, filename := filepath.Split(newFilePath)
|
||||
// 替换文件名和文件夹名
|
||||
for _, field := range r.replacementFields {
|
||||
if strings.Contains(dir, field.Old) {
|
||||
dir = strings.ReplaceAll(dir, field.Old, field.New)
|
||||
}
|
||||
if strings.Contains(filename, field.Old) {
|
||||
filename = strings.ReplaceAll(filename, field.Old, field.New)
|
||||
}
|
||||
|
||||
if newFilePath != dir+filename {
|
||||
newFilePath = dir + filename
|
||||
}
|
||||
}
|
||||
|
||||
if gofile.IsExists(newFilePath) {
|
||||
existFiles = append(existFiles, newFilePath)
|
||||
}
|
||||
writeData[newFilePath] = data
|
||||
}
|
||||
|
||||
if len(existFiles) > 0 {
|
||||
return fmt.Errorf("existing files detected\n %s\nCode generation has been cancelled\n", strings.Join(existFiles, "\n "))
|
||||
}
|
||||
|
||||
for file, data := range writeData {
|
||||
// 保存文件
|
||||
err := saveToNewFile(file, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *replacerInfo) isIgnoreFile(file string) bool {
|
||||
isIgnore := false
|
||||
_, filename := filepath.Split(file)
|
||||
for _, v := range r.ignoreFiles {
|
||||
if filename == v {
|
||||
isIgnore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return isIgnore
|
||||
}
|
||||
|
||||
func (r *replacerInfo) isInIgnoreDir(file string) bool {
|
||||
isIgnore := false
|
||||
dir, _ := filepath.Split(file)
|
||||
for _, v := range r.ignoreDirs {
|
||||
if strings.Contains(dir, v) {
|
||||
isIgnore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return isIgnore
|
||||
}
|
||||
|
||||
func (r *replacerInfo) getNewFilePath(file string) string {
|
||||
var newFilePath string
|
||||
if r.isActual {
|
||||
newFilePath = r.outPath + strings.Replace(file, r.path, "", 1)
|
||||
} else {
|
||||
newFilePath = r.outPath + strings.Replace(file, r.path, "", 1)
|
||||
}
|
||||
|
||||
if gofile.IsWindows() {
|
||||
newFilePath = strings.ReplaceAll(newFilePath, "/", "\\")
|
||||
}
|
||||
|
||||
return newFilePath
|
||||
}
|
||||
|
||||
// 如果是windows,转换路径分割符
|
||||
func (r *replacerInfo) covertPathDelimiter(filePath string) string {
|
||||
if r.isActual && gofile.IsWindows() {
|
||||
filePath = strings.ReplaceAll(filePath, "/", "\\")
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
|
||||
// 如果是windows,批量转换路径分割符
|
||||
func (r *replacerInfo) covertPathsDelimiter(filePaths ...string) []string {
|
||||
if r.isActual && gofile.IsWindows() {
|
||||
filePathsTmp := []string{}
|
||||
for _, dir := range filePaths {
|
||||
filePathsTmp = append(filePathsTmp, strings.ReplaceAll(dir, "/", "\\"))
|
||||
}
|
||||
return filePathsTmp
|
||||
}
|
||||
return filePaths
|
||||
}
|
||||
|
||||
func saveToNewFile(filePath string, data []byte) error {
|
||||
// 创建目录
|
||||
dir, _ := filepath.Split(filePath)
|
||||
err := os.MkdirAll(dir, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
err = os.WriteFile(filePath, data, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 遍历嵌入的目录下所有文件,返回文件的绝对路径
|
||||
func listFiles(path string, fs embed.FS) ([]string, error) {
|
||||
files := []string{}
|
||||
err := walkDir(path, &files, fs)
|
||||
return files, err
|
||||
}
|
||||
|
||||
// 通过迭代方式遍历嵌入的目录
|
||||
func walkDir(dirPath string, allFiles *[]string, fs embed.FS) error {
|
||||
files, err := fs.ReadDir(dirPath) // 读取目录下文件
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
deepFile := dirPath + "/" + file.Name()
|
||||
if file.IsDir() {
|
||||
_ = walkDir(deepFile, allFiles, fs)
|
||||
continue
|
||||
}
|
||||
*allFiles = append(*allFiles, deepFile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 判断字符串第一个字符是字母
|
||||
func isFirstAlphabet(str string) bool {
|
||||
if len(str) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if (str[0] >= 'A' && str[0] <= 'Z') || (str[0] >= 'a' && str[0] <= 'z') {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isSubPath(filePath string, subPath string) bool {
|
||||
dir, _ := filepath.Split(filePath)
|
||||
return strings.Contains(dir, subPath)
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package replacer
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//go:embed testDir
|
||||
var fs embed.FS
|
||||
|
||||
func TestNewWithFS(t *testing.T) {
|
||||
type args struct {
|
||||
fn func() Replacer
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "New",
|
||||
args: args{
|
||||
fn: func() Replacer {
|
||||
replacer, err := New("testDir")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return replacer
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "NewFS",
|
||||
args: args{
|
||||
fn: func() Replacer {
|
||||
replacer, err := NewFS("testDir", fs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return replacer
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := tt.args.fn()
|
||||
|
||||
subDirs := []string{"testDir/replace"}
|
||||
ignoreDirs := []string{"testDir/ignore"}
|
||||
ignoreFiles := []string{"test.txt"}
|
||||
fields := []Field{
|
||||
{
|
||||
Old: "1234",
|
||||
New: "....",
|
||||
},
|
||||
{
|
||||
Old: "abcdef",
|
||||
New: "hello_",
|
||||
IsCaseSensitive: true,
|
||||
},
|
||||
}
|
||||
r.SetSubDirs(subDirs...) // 只处理指定子目录,为空时表示指定全部文件
|
||||
r.SetIgnoreFiles(ignoreDirs...) // 忽略替换目录
|
||||
r.SetIgnoreFiles(ignoreFiles...) // 忽略替换文件
|
||||
r.SetReplacementFields(fields) // 设置替换文本
|
||||
_ = r.SetOutputDir("", tt.name+"_test") // 设置输出目录和名称
|
||||
_, err := r.ReadFile("replace.txt")
|
||||
assert.NoError(t, err)
|
||||
err = r.SaveFiles() // 保存替换后文件
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SaveFiles() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
t.Logf("save files successfully, out = %s", r.GetOutputDir())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplacerError(t *testing.T) {
|
||||
_, err := New("/notfound")
|
||||
assert.Error(t, err)
|
||||
_, err = NewFS("/notfound", embed.FS{})
|
||||
assert.Error(t, err)
|
||||
|
||||
r, err := New("testDir")
|
||||
assert.NoError(t, err)
|
||||
r.SetIgnoreFiles()
|
||||
r.SetSubDirs()
|
||||
err = r.SetOutputDir("/tmp/yourServerName")
|
||||
assert.NoError(t, err)
|
||||
path := r.GetSourcePath()
|
||||
assert.NotEmpty(t, path)
|
||||
|
||||
r = &replacerInfo{}
|
||||
err = r.SaveFiles()
|
||||
assert.NoError(t, err)
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
ignore
|
|
@ -0,0 +1 @@
|
|||
change file name
|
|
@ -0,0 +1,3 @@
|
|||
1234567890
|
||||
abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|
@ -0,0 +1,3 @@
|
|||
test1
|
||||
|
||||
to do replace
|
|
@ -0,0 +1,63 @@
|
|||
## sql2code
|
||||
|
||||
根据sql生成不同用途代码,支持生成json、gorm model、dao、handler代码,sql可以从参数、文件、db三种方式获取,优先从高到低。
|
||||
|
||||
<br>
|
||||
|
||||
### 安装
|
||||
|
||||
> go get -u github.com/zhufuyi/pkg/sql2code
|
||||
|
||||
<br>
|
||||
|
||||
### 使用示例
|
||||
|
||||
主要设置参数
|
||||
|
||||
```go
|
||||
type Args struct {
|
||||
SQL string // DDL sql
|
||||
|
||||
DDLFile string // 读取文件的DDL sql
|
||||
|
||||
DBDsn string // 从db获取表的DDL sql
|
||||
DBTable string
|
||||
|
||||
Package string // 生成字段的包名(只有model类型有效)
|
||||
GormType bool // gorm type
|
||||
JSONTag bool // 是否包括json tag
|
||||
JSONNamedType int // json命名类型,0:和列名一致,其他值表示驼峰
|
||||
IsEmbed bool // 是否嵌入gorm.Model
|
||||
CodeType string // 指定生成代码用途,支持4中类型,分别是 model(默认), json, dao, handler
|
||||
}
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
生成代码示例:
|
||||
|
||||
```go
|
||||
// 生成gorm model 代码
|
||||
code, err := sql2code.GenerateOne(&sql2code.Args{
|
||||
SQL: sqlData, // 来源于sql语句
|
||||
// DDLFile: "user.sql", // 来源于sql文件
|
||||
// DBDsn: "root:123456@(127.0.0.1:3306)/account"
|
||||
// DBTable "user"
|
||||
GormType: true,
|
||||
JSONTag: true,
|
||||
IsEmbed: true,
|
||||
CodeType: "model"
|
||||
})
|
||||
|
||||
// 生成json、model、dao、handler代码
|
||||
codes, err := sql2code.Generate(&sql2code.Args{
|
||||
SQL: sqlData, // 来源于sql语句
|
||||
// DDLFile: "user.sql", // 来源于sql文件
|
||||
// DBDsn: "root:123456@(127.0.0.1:3306)/account"
|
||||
// DBTable "user"
|
||||
GormType: true,
|
||||
JSONTag: true,
|
||||
IsEmbed: true,
|
||||
CodeType: "model"
|
||||
})
|
||||
```
|
|
@ -0,0 +1,36 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql" //nolint
|
||||
)
|
||||
|
||||
// GetTableInfo get table info from mysql
|
||||
func GetTableInfo(dsn, tableName string) (string, error) {
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("connect mysql error, %v", err)
|
||||
}
|
||||
defer db.Close() //nolint
|
||||
|
||||
rows, err := db.Query("SHOW CREATE TABLE " + tableName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("query show create table error, %v", err)
|
||||
}
|
||||
|
||||
defer rows.Close() //nolint
|
||||
if !rows.Next() {
|
||||
return "", fmt.Errorf("not found found table '%s'", tableName)
|
||||
}
|
||||
|
||||
var table string
|
||||
var info string
|
||||
err = rows.Scan(&table, &info)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue