Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
|
fb585fabe6 | |
|
29d609ce0a | |
|
b0d6e3423d | |
|
4792b21857 | |
|
0d0477d32c | |
|
c027ef22bc | |
|
908931bc01 | |
|
f79d3f28e2 | |
|
9dc6735a61 | |
|
673d22fcff | |
|
4dc5e64dde | |
|
805ad282f5 | |
|
0bc3f9d170 | |
|
f5687a0094 | |
|
59c949f627 | |
|
9393518045 |
|
@ -11,6 +11,7 @@ _posts
|
||||||
.vscode
|
.vscode
|
||||||
vendor
|
vendor
|
||||||
cmd/
|
cmd/
|
||||||
|
tools/
|
||||||
third_party/
|
third_party/
|
||||||
|
|
||||||
# Go.gitignore
|
# Go.gitignore
|
||||||
|
|
94
README.md
94
README.md
|
@ -4,5 +4,99 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 环境变量
|
||||||
|
|
||||||
|
| 环境变量 | 描述 |
|
||||||
|
| --- | --- |
|
||||||
|
| AEUS_DEBUG | 是否开启debug模式 |
|
||||||
|
| HTTP_PORT | http服务端口 |
|
||||||
|
| GRPC_PORT | grpc服务端口 |
|
||||||
|
| CLI_PORT | cli服务端口 |
|
||||||
|
|
||||||
|
|
||||||
# 快速开始
|
# 快速开始
|
||||||
|
|
||||||
|
## 创建一个项目
|
||||||
|
|
||||||
|
创建项目可以使用`aeus`命令行工具进行生成:
|
||||||
|
|
||||||
|
```
|
||||||
|
aeus new github.com/your-username/your-project-name
|
||||||
|
```
|
||||||
|
|
||||||
|
如果需要创建一个带管理后台的应用, 可以使用`--admin`参数:
|
||||||
|
|
||||||
|
```
|
||||||
|
aeus new github.com/your-username/your-project-name --admin
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 生成`Proto`文件
|
||||||
|
|
||||||
|
服务使用`proto3`作为通信协议,因此需要生成`Proto`文件。
|
||||||
|
|
||||||
|
```
|
||||||
|
make proto
|
||||||
|
```
|
||||||
|
|
||||||
|
清理生成的文件使用:
|
||||||
|
|
||||||
|
```
|
||||||
|
make proto-clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## 编译项目
|
||||||
|
|
||||||
|
编译项目可以使用`make`命令进行编译:
|
||||||
|
|
||||||
|
```
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
├── api
|
||||||
|
│ └── v1
|
||||||
|
├── cmd
|
||||||
|
│ ├── main.go
|
||||||
|
├── config
|
||||||
|
│ ├── config.go
|
||||||
|
│ └── config.yaml
|
||||||
|
├── deploy
|
||||||
|
│ └── docker
|
||||||
|
├── go.mod
|
||||||
|
├── go.sum
|
||||||
|
├── internal
|
||||||
|
│ ├── models
|
||||||
|
│ ├── scope
|
||||||
|
│ ├── service
|
||||||
|
├── Makefile
|
||||||
|
├── README.md
|
||||||
|
├── third_party
|
||||||
|
│ ├── aeus
|
||||||
|
│ ├── errors
|
||||||
|
│ ├── google
|
||||||
|
│ ├── openapi
|
||||||
|
│ ├── README.md
|
||||||
|
│ └── validate
|
||||||
|
├── vendor
|
||||||
|
├── version
|
||||||
|
│ └── version.go
|
||||||
|
├── web
|
||||||
|
└── webhook.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
| 目录 | 描述 |
|
||||||
|
| --- | --- |
|
||||||
|
| api | api定义目录 |
|
||||||
|
| cmd | 启动命令目录 |
|
||||||
|
| config | 配置目录 |
|
||||||
|
| deploy | 部署目录 |
|
||||||
|
| internal | 内部文件目录 |
|
||||||
|
| internal.service | 服务定义目录 |
|
||||||
|
| internal.models | 模型定义目录 |
|
||||||
|
| internal.scope | 服务scope定义目录,主要有全局的变量(比如DB,Redis等) |
|
||||||
|
| third_party | 第三方proto文件目录 |
|
||||||
|
| web | 前端资源目录 |
|
11
app.go
11
app.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
@ -35,12 +36,19 @@ func (s *Service) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Debug() bool {
|
func (s *Service) Debug() bool {
|
||||||
|
if s.opts != nil {
|
||||||
|
return s.opts.debug
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Version() string {
|
func (s *Service) Version() string {
|
||||||
|
if s.opts != nil {
|
||||||
return s.opts.version
|
return s.opts.version
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Metadata() map[string]string {
|
func (s *Service) Metadata() map[string]string {
|
||||||
if s.service == nil {
|
if s.service == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -114,7 +122,7 @@ func (s *Service) injectVars(v any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) preStart(ctx context.Context) (err error) {
|
func (s *Service) preStart(ctx context.Context) (err error) {
|
||||||
s.Logger().Info(s.ctx, "starting")
|
s.Logger().Info(ctx, "starting")
|
||||||
for _, ptr := range s.opts.servers {
|
for _, ptr := range s.opts.servers {
|
||||||
s.refValues = append(s.refValues, reflect.ValueOf(ptr))
|
s.refValues = append(s.refValues, reflect.ValueOf(ptr))
|
||||||
}
|
}
|
||||||
|
@ -255,6 +263,7 @@ func New(cbs ...Option) *Service {
|
||||||
registrarTimeout: time.Second * 30,
|
registrarTimeout: time.Second * 30,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
s.opts.debug, _ = strconv.ParseBool("AEUS_DEBUG")
|
||||||
s.opts.metadata = make(map[string]string)
|
s.opts.metadata = make(map[string]string)
|
||||||
for _, cb := range cbs {
|
for _, cb := range cbs {
|
||||||
cb(s.opts)
|
cb(s.opts)
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -11,6 +11,7 @@ require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/mattn/go-runewidth v0.0.16
|
github.com/mattn/go-runewidth v0.0.16
|
||||||
github.com/peterh/liner v1.2.2
|
github.com/peterh/liner v1.2.2
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
go.etcd.io/etcd/api/v3 v3.6.0
|
go.etcd.io/etcd/api/v3 v3.6.0
|
||||||
go.etcd.io/etcd/client/v3 v3.6.0
|
go.etcd.io/etcd/client/v3 v3.6.0
|
||||||
|
@ -24,10 +25,12 @@ require (
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.11.6 // indirect
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/coreos/go-semver v0.3.1 // indirect
|
github.com/coreos/go-semver v0.3.1 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
|
10
go.sum
10
go.sum
|
@ -1,7 +1,13 @@
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
@ -14,6 +20,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
@ -86,6 +94,8 @@ github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
|
||||||
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
|
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
|
|
@ -2,6 +2,7 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.nobla.cn/golang/aeus/metadata"
|
"git.nobla.cn/golang/aeus/metadata"
|
||||||
|
@ -29,48 +30,69 @@ const (
|
||||||
|
|
||||||
type Option func(*options)
|
type Option func(*options)
|
||||||
|
|
||||||
|
type Validate interface {
|
||||||
|
Validate(ctx context.Context, token string) error
|
||||||
|
}
|
||||||
|
|
||||||
// Parser is a jwt parser
|
// Parser is a jwt parser
|
||||||
type options struct {
|
type options struct {
|
||||||
allows []string
|
allows []string
|
||||||
claims func() jwt.Claims
|
claims reflect.Type
|
||||||
}
|
validate Validate
|
||||||
|
|
||||||
// WithClaims with customer claim
|
|
||||||
// If you use it in Server, f needs to return a new jwt.Claims object each time to avoid concurrent write problems
|
|
||||||
// If you use it in Client, f only needs to return a single object to provide performance
|
|
||||||
func WithClaims(f func() jwt.Claims) Option {
|
|
||||||
return func(o *options) {
|
|
||||||
o.claims = f
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAllow with allow path
|
// WithAllow with allow path
|
||||||
func WithAllow(path string) Option {
|
func WithAllow(paths ...string) Option {
|
||||||
return func(o *options) {
|
return func(o *options) {
|
||||||
if o.allows == nil {
|
if o.allows == nil {
|
||||||
o.allows = make([]string, 0, 16)
|
o.allows = make([]string, 0, 16)
|
||||||
}
|
}
|
||||||
o.allows = append(o.allows, path)
|
for _, s := range paths {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if len(s) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
o.allows = append(o.allows, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithClaims(claims any) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
if tv, ok := claims.(reflect.Type); ok {
|
||||||
|
o.claims = tv
|
||||||
|
} else {
|
||||||
|
o.claims = reflect.TypeOf(claims)
|
||||||
|
if o.claims.Kind() == reflect.Ptr {
|
||||||
|
o.claims = o.claims.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithValidate(fn Validate) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.validate = fn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isAllowed check if the path is allowed
|
// isAllowed check if the path is allowed
|
||||||
func isAllowed(uripath string, allows []string) bool {
|
func isAllowed(uripath string, allows []string) bool {
|
||||||
for _, str := range allows {
|
for _, pattern := range allows {
|
||||||
n := len(str)
|
n := len(pattern)
|
||||||
if n == 0 {
|
if pattern == uripath {
|
||||||
continue
|
return true
|
||||||
}
|
}
|
||||||
if n > 1 && str[n-1] == '*' {
|
if pattern == "*" {
|
||||||
if strings.HasPrefix(uripath, str[:n-1]) {
|
return true
|
||||||
|
}
|
||||||
|
if n > 1 && pattern[n-1] == '*' {
|
||||||
|
if strings.HasPrefix(uripath, pattern[:n-1]) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if str == uripath {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
return false
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWT auth middleware
|
// JWT auth middleware
|
||||||
|
@ -82,10 +104,6 @@ func JWT(keyFunc jwt.Keyfunc, cbs ...Option) middleware.Middleware {
|
||||||
return func(next middleware.Handler) middleware.Handler {
|
return func(next middleware.Handler) middleware.Handler {
|
||||||
return func(ctx context.Context) (err error) {
|
return func(ctx context.Context) (err error) {
|
||||||
md := metadata.FromContext(ctx)
|
md := metadata.FromContext(ctx)
|
||||||
authorizationValue, ok := md.Get(authorizationKey)
|
|
||||||
if !ok {
|
|
||||||
return errors.ErrAccessDenied
|
|
||||||
}
|
|
||||||
if len(opts.allows) > 0 {
|
if len(opts.allows) > 0 {
|
||||||
requestPath, ok := md.Get(metadata.RequestPathKey)
|
requestPath, ok := md.Get(metadata.RequestPathKey)
|
||||||
if ok {
|
if ok {
|
||||||
|
@ -94,17 +112,27 @@ func JWT(keyFunc jwt.Keyfunc, cbs ...Option) middleware.Middleware {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(authorizationValue, bearerWord) {
|
token, ok := md.Get(authorizationKey)
|
||||||
|
if !ok {
|
||||||
return errors.ErrAccessDenied
|
return errors.ErrAccessDenied
|
||||||
}
|
}
|
||||||
|
if opts.validate != nil {
|
||||||
|
if err = opts.validate.Validate(ctx, token); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
token, _ = strings.CutPrefix(token, bearerWord)
|
||||||
var (
|
var (
|
||||||
ti *jwt.Token
|
ti *jwt.Token
|
||||||
)
|
)
|
||||||
authorizationToken := strings.TrimSpace(strings.TrimPrefix(authorizationValue, bearerWord))
|
token = strings.TrimSpace(token)
|
||||||
if opts.claims != nil {
|
if opts.claims != nil {
|
||||||
ti, err = jwt.ParseWithClaims(authorizationToken, opts.claims(), keyFunc)
|
if claims, ok := reflect.New(opts.claims).Interface().(jwt.Claims); ok {
|
||||||
} else {
|
ti, err = jwt.ParseWithClaims(token, claims, keyFunc)
|
||||||
ti, err = jwt.Parse(authorizationToken, keyFunc)
|
}
|
||||||
|
}
|
||||||
|
if ti == nil {
|
||||||
|
ti, err = jwt.Parse(token, keyFunc)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, jwt.ErrTokenMalformed) || errors.Is(err, jwt.ErrTokenUnverifiable) {
|
if errors.Is(err, jwt.ErrTokenMalformed) || errors.Is(err, jwt.ErrTokenUnverifiable) {
|
||||||
|
|
12
options.go
12
options.go
|
@ -2,6 +2,7 @@ package aeus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.nobla.cn/golang/aeus/pkg/logger"
|
"git.nobla.cn/golang/aeus/pkg/logger"
|
||||||
|
@ -18,6 +19,7 @@ type options struct {
|
||||||
servers []Server
|
servers []Server
|
||||||
endpoints []string
|
endpoints []string
|
||||||
scope Scope
|
scope Scope
|
||||||
|
debug bool
|
||||||
registrarTimeout time.Duration
|
registrarTimeout time.Duration
|
||||||
registry registry.Registry
|
registry registry.Registry
|
||||||
serviceLoader ServiceLoader
|
serviceLoader ServiceLoader
|
||||||
|
@ -41,9 +43,7 @@ func WithMetadata(metadata map[string]string) Option {
|
||||||
if o.metadata == nil {
|
if o.metadata == nil {
|
||||||
o.metadata = make(map[string]string)
|
o.metadata = make(map[string]string)
|
||||||
}
|
}
|
||||||
for k, v := range metadata {
|
maps.Copy(o.metadata, metadata)
|
||||||
o.metadata[k] = v
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +71,12 @@ func WithScope(scope Scope) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithDebug(debug bool) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.debug = debug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithServiceLoader(loader ServiceLoader) Option {
|
func WithServiceLoader(loader ServiceLoader) Option {
|
||||||
return func(o *options) {
|
return func(o *options) {
|
||||||
o.serviceLoader = loader
|
o.serviceLoader = loader
|
||||||
|
|
|
@ -13,23 +13,29 @@ var (
|
||||||
|
|
||||||
type Cache interface {
|
type Cache interface {
|
||||||
// Get gets a cached value by key.
|
// Get gets a cached value by key.
|
||||||
Get(ctx context.Context, key string) (any, time.Time, error)
|
Load(ctx context.Context, key string, val any) error
|
||||||
|
// Get gets a cached value by key.
|
||||||
|
Exists(ctx context.Context, key string) (bool, error)
|
||||||
// Put stores a key-value pair into cache.
|
// Put stores a key-value pair into cache.
|
||||||
Put(ctx context.Context, key string, val any, d time.Duration) error
|
Store(ctx context.Context, key string, val any, d time.Duration) error
|
||||||
// Delete removes a key from cache.
|
// Delete removes a key from cache.
|
||||||
Delete(ctx context.Context, key string) error
|
Delete(ctx context.Context, key string) error
|
||||||
// String returns the name of the implementation.
|
// String returns the name of the implementation.
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Default() Cache {
|
||||||
|
return std
|
||||||
|
}
|
||||||
|
|
||||||
// Get gets a cached value by key.
|
// Get gets a cached value by key.
|
||||||
func Get(ctx context.Context, key string) (any, time.Time, error) {
|
func Load(ctx context.Context, key string, val any) error {
|
||||||
return std.Get(ctx, key)
|
return std.Load(ctx, key, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put stores a key-value pair into cache.
|
// Put stores a key-value pair into cache.
|
||||||
func Put(ctx context.Context, key string, val any, d time.Duration) error {
|
func Store(ctx context.Context, key string, val any, d time.Duration) error {
|
||||||
return std.Put(ctx, key, val, d)
|
return std.Store(ctx, key, val, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the name of the implementation.
|
// String returns the name of the implementation.
|
||||||
|
|
|
@ -2,10 +2,18 @@ package memory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.nobla.cn/golang/aeus/pkg/errors"
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrWongType = errors.New("val must be a pointer")
|
||||||
|
ErrNotExists = errors.New("not exists")
|
||||||
|
ErrAaddressable = errors.New("cannot set value: val is not addressable")
|
||||||
)
|
)
|
||||||
|
|
||||||
type memCache struct {
|
type memCache struct {
|
||||||
|
@ -15,22 +23,47 @@ type memCache struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *memCache) Get(ctx context.Context, key string) (interface{}, time.Time, error) {
|
func (c *memCache) Load(ctx context.Context, key string, val any) error {
|
||||||
c.RWMutex.RLock()
|
c.RWMutex.RLock()
|
||||||
defer c.RWMutex.RUnlock()
|
defer c.RWMutex.RUnlock()
|
||||||
|
|
||||||
item, found := c.items[key]
|
item, found := c.items[key]
|
||||||
if !found {
|
if !found {
|
||||||
return nil, time.Time{}, errors.ErrNotFound
|
return ErrNotExists
|
||||||
}
|
}
|
||||||
if item.Expired() {
|
if item.Expired() {
|
||||||
return nil, time.Time{}, errors.ErrExpired
|
return ErrNotExists
|
||||||
}
|
}
|
||||||
|
refValue := reflect.ValueOf(val)
|
||||||
return item.Value, time.Unix(0, item.Expiration), nil
|
if refValue.Type().Kind() != reflect.Ptr {
|
||||||
|
return ErrWongType
|
||||||
|
}
|
||||||
|
refElem := refValue.Elem()
|
||||||
|
if !refElem.CanSet() {
|
||||||
|
return ErrAaddressable
|
||||||
|
}
|
||||||
|
targetValue := reflect.Indirect(reflect.ValueOf(item.Value))
|
||||||
|
if targetValue.Type() != refElem.Type() {
|
||||||
|
return fmt.Errorf("type mismatch: expected %v, got %v", refElem.Type(), targetValue.Type())
|
||||||
|
}
|
||||||
|
refElem.Set(targetValue)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *memCache) Put(ctx context.Context, key string, val interface{}, d time.Duration) error {
|
func (c *memCache) Exists(ctx context.Context, key string) (bool, error) {
|
||||||
|
c.RWMutex.RLock()
|
||||||
|
defer c.RWMutex.RUnlock()
|
||||||
|
item, found := c.items[key]
|
||||||
|
if !found {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if item.Expired() {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) Store(ctx context.Context, key string, val any, d time.Duration) error {
|
||||||
var e int64
|
var e int64
|
||||||
if d == DefaultExpiration {
|
if d == DefaultExpiration {
|
||||||
d = c.opts.Expiration
|
d = c.opts.Expiration
|
||||||
|
@ -56,9 +89,8 @@ func (c *memCache) Delete(ctx context.Context, key string) error {
|
||||||
|
|
||||||
_, found := c.items[key]
|
_, found := c.items[key]
|
||||||
if !found {
|
if !found {
|
||||||
return errors.ErrNotFound
|
return ErrNotExists
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(c.items, key)
|
delete(c.items, key)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -66,3 +98,18 @@ func (c *memCache) Delete(ctx context.Context, key string) error {
|
||||||
func (m *memCache) String() string {
|
func (m *memCache) String() string {
|
||||||
return "memory"
|
return "memory"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCache returns a new cache.
|
||||||
|
func NewCache(opts ...Option) *memCache {
|
||||||
|
options := NewOptions(opts...)
|
||||||
|
items := make(map[string]Item)
|
||||||
|
|
||||||
|
if len(options.Items) > 0 {
|
||||||
|
items = options.Items
|
||||||
|
}
|
||||||
|
|
||||||
|
return &memCache{
|
||||||
|
opts: options,
|
||||||
|
items: items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import "time"
|
||||||
|
|
||||||
// Item represents an item stored in the cache.
|
// Item represents an item stored in the cache.
|
||||||
type Item struct {
|
type Item struct {
|
||||||
Value interface{}
|
Value any
|
||||||
Expiration int64
|
Expiration int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,18 +16,3 @@ func (i *Item) Expired() bool {
|
||||||
|
|
||||||
return time.Now().UnixNano() > i.Expiration
|
return time.Now().UnixNano() > i.Expiration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCache returns a new cache.
|
|
||||||
func NewCache(opts ...Option) *memCache {
|
|
||||||
options := NewOptions(opts...)
|
|
||||||
items := make(map[string]Item)
|
|
||||||
|
|
||||||
if len(options.Items) > 0 {
|
|
||||||
items = options.Items
|
|
||||||
}
|
|
||||||
|
|
||||||
return &memCache{
|
|
||||||
opts: options,
|
|
||||||
items: items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.nobla.cn/golang/aeus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type redisCache struct {
|
||||||
|
opts *options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *redisCache) buildKey(key string) string {
|
||||||
|
return c.opts.prefix + key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *redisCache) Load(ctx context.Context, key string, val any) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
buf []byte
|
||||||
|
)
|
||||||
|
if buf, err = c.opts.client.Get(ctx, c.buildKey(key)).Bytes(); err == nil {
|
||||||
|
err = json.Unmarshal(buf, val)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *redisCache) Exists(ctx context.Context, key string) (ok bool, err error) {
|
||||||
|
var n int64
|
||||||
|
n, err = c.opts.client.Exists(ctx, c.buildKey(key)).Result()
|
||||||
|
if n > 0 {
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put stores a key-value pair into cache.
|
||||||
|
func (c *redisCache) Store(ctx context.Context, key string, val any, d time.Duration) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
buf []byte
|
||||||
|
)
|
||||||
|
if buf, err = json.Marshal(val); err == nil {
|
||||||
|
err = c.opts.client.Set(ctx, c.buildKey(key), buf, d).Err()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a key from cache.
|
||||||
|
func (c *redisCache) Delete(ctx context.Context, key string) error {
|
||||||
|
return c.opts.client.Del(ctx, c.buildKey(key)).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the name of the implementation.
|
||||||
|
func (c *redisCache) String() string {
|
||||||
|
return "redis"
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCache(opts ...Option) *redisCache {
|
||||||
|
cache := &redisCache{
|
||||||
|
opts: newOptions(opts...),
|
||||||
|
}
|
||||||
|
app := aeus.FromContext(cache.opts.context)
|
||||||
|
if app != nil {
|
||||||
|
cache.opts.prefix = app.Name() + ":" + cache.opts.prefix
|
||||||
|
}
|
||||||
|
return cache
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
options struct {
|
||||||
|
context context.Context
|
||||||
|
client *redis.Client
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
Option func(*options)
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithClient(client *redis.Client) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.client = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithContext(ctx context.Context) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.context = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPrefix(prefix string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.prefix = prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptions returns a new options struct.
|
||||||
|
func newOptions(opts ...Option) *options {
|
||||||
|
options := &options{
|
||||||
|
prefix: "cache:",
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ const (
|
||||||
Invalid = 1001 //payload invalid
|
Invalid = 1001 //payload invalid
|
||||||
Exists = 1002 //already exists
|
Exists = 1002 //already exists
|
||||||
Unavailable = 1003 //service unavailable
|
Unavailable = 1003 //service unavailable
|
||||||
|
Incompatible = 1004 //type incompatible
|
||||||
Timeout = 2001 //timeout
|
Timeout = 2001 //timeout
|
||||||
Expired = 2002 //expired
|
Expired = 2002 //expired
|
||||||
TokenExpired = 4002 //token expired
|
TokenExpired = 4002 //token expired
|
||||||
|
@ -29,4 +30,5 @@ var (
|
||||||
ErrUnavailable = New(Unavailable, "service unavailable")
|
ErrUnavailable = New(Unavailable, "service unavailable")
|
||||||
ErrNetworkUnreachable = New(NetworkUnreachable, "network unreachable")
|
ErrNetworkUnreachable = New(NetworkUnreachable, "network unreachable")
|
||||||
ErrConnectionRefused = New(ConnectionRefused, "connection refused")
|
ErrConnectionRefused = New(ConnectionRefused, "connection refused")
|
||||||
|
ErrIncompatible = New(Incompatible, "incompatible")
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,6 +14,14 @@ func (e *Error) Error() string {
|
||||||
return fmt.Sprintf("code: %d, message: %s", e.Code, e.Message)
|
return fmt.Sprintf("code: %d, message: %s", e.Code, e.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Error) GetCode() int {
|
||||||
|
return e.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) GetMessage() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
func Warp(code int, err error) error {
|
func Warp(code int, err error) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Code: code,
|
Code: code,
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Authorization interface {
|
||||||
|
Token() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BasicAuth struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BearerAuth struct {
|
||||||
|
AccessToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *BasicAuth) Token() string {
|
||||||
|
return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(auth.Username+":"+auth.Password)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *BearerAuth) Token() string {
|
||||||
|
return fmt.Sprintf("Bearer %s", auth.AccessToken)
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
BeforeRequest func(req *http.Request) (err error)
|
||||||
|
AfterRequest func(req *http.Request, res *http.Response) (err error)
|
||||||
|
|
||||||
|
Client struct {
|
||||||
|
baseUrl string
|
||||||
|
Authorization Authorization
|
||||||
|
client *http.Client
|
||||||
|
cookieJar *cookiejar.Jar
|
||||||
|
interceptorRequest []BeforeRequest
|
||||||
|
interceptorResponse []AfterRequest
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
MaxIdleConns: 64,
|
||||||
|
MaxIdleConnsPerHost: 8,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Timeout: time.Second * 30,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (client *Client) stashUri(urlPath string) string {
|
||||||
|
var (
|
||||||
|
pos int
|
||||||
|
)
|
||||||
|
if len(urlPath) == 0 {
|
||||||
|
return client.baseUrl
|
||||||
|
}
|
||||||
|
if pos = strings.Index(urlPath, "//"); pos == -1 {
|
||||||
|
if client.baseUrl != "" {
|
||||||
|
if urlPath[0] != '/' {
|
||||||
|
urlPath = "/" + urlPath
|
||||||
|
}
|
||||||
|
return client.baseUrl + urlPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urlPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) BeforeRequest(cb BeforeRequest) *Client {
|
||||||
|
client.interceptorRequest = append(client.interceptorRequest, cb)
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) AfterRequest(cb AfterRequest) *Client {
|
||||||
|
client.interceptorResponse = append(client.interceptorResponse, cb)
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) SetBaseUrl(s string) *Client {
|
||||||
|
client.baseUrl = strings.TrimSuffix(s, "/")
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) SetCookieJar(cookieJar *cookiejar.Jar) *Client {
|
||||||
|
client.client.Jar = cookieJar
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) SetClient(httpClient *http.Client) *Client {
|
||||||
|
client.client = httpClient
|
||||||
|
if client.cookieJar != nil {
|
||||||
|
client.client.Jar = client.cookieJar
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) SetTransport(transport http.RoundTripper) *Client {
|
||||||
|
client.client.Transport = transport
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Get(urlPath string) *Request {
|
||||||
|
return newRequest(http.MethodGet, client.stashUri(urlPath), client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Put(urlPath string) *Request {
|
||||||
|
return newRequest(http.MethodPut, client.stashUri(urlPath), client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Post(urlPath string) *Request {
|
||||||
|
return newRequest(http.MethodPost, client.stashUri(urlPath), client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Delete(urlPath string) *Request {
|
||||||
|
return newRequest(http.MethodDelete, client.stashUri(urlPath), client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) execute(r *Request) (res *http.Response, err error) {
|
||||||
|
var (
|
||||||
|
n int
|
||||||
|
reader io.Reader
|
||||||
|
)
|
||||||
|
if r.contentType == "" && r.body != nil {
|
||||||
|
r.contentType = r.detectContentType(r.body)
|
||||||
|
}
|
||||||
|
if r.body != nil {
|
||||||
|
if reader, err = r.readRequestBody(r.contentType, r.body); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.rawRequest, err = http.NewRequest(r.method, r.uri, reader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for k, vs := range r.header {
|
||||||
|
for _, v := range vs {
|
||||||
|
r.rawRequest.Header.Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.contentType != "" {
|
||||||
|
r.rawRequest.Header.Set("Content-Type", r.contentType)
|
||||||
|
}
|
||||||
|
if client.Authorization != nil {
|
||||||
|
r.rawRequest.Header.Set("Authorization", client.Authorization.Token())
|
||||||
|
}
|
||||||
|
if r.context != nil {
|
||||||
|
r.rawRequest = r.rawRequest.WithContext(r.context)
|
||||||
|
}
|
||||||
|
n = len(client.interceptorRequest)
|
||||||
|
for i := n - 1; i >= 0; i-- {
|
||||||
|
if err = client.interceptorRequest[i](r.rawRequest); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.rawResponse, err = client.client.Do(r.rawRequest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n = len(client.interceptorResponse)
|
||||||
|
for i := n - 1; i >= 0; i-- {
|
||||||
|
if err = client.interceptorResponse[i](r.rawRequest, r.rawResponse); err != nil {
|
||||||
|
_ = r.rawResponse.Body.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.rawResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Client {
|
||||||
|
client := &Client{
|
||||||
|
client: DefaultClient,
|
||||||
|
interceptorRequest: make([]BeforeRequest, 0, 10),
|
||||||
|
interceptorResponse: make([]AfterRequest, 0, 10),
|
||||||
|
}
|
||||||
|
client.cookieJar, _ = cookiejar.New(nil)
|
||||||
|
client.client.Jar = client.cookieJar
|
||||||
|
return client
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func encodeBody(data any) (r io.Reader, contentType string, err error) {
|
||||||
|
var (
|
||||||
|
buf []byte
|
||||||
|
)
|
||||||
|
switch v := data.(type) {
|
||||||
|
case string:
|
||||||
|
r = strings.NewReader(v)
|
||||||
|
contentType = "x-www-form-urlencoded"
|
||||||
|
case []byte:
|
||||||
|
r = bytes.NewReader(v)
|
||||||
|
contentType = "x-www-form-urlencoded"
|
||||||
|
default:
|
||||||
|
if buf, err = json.Marshal(v); err == nil {
|
||||||
|
r = bytes.NewReader(buf)
|
||||||
|
contentType = "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func doHttpRequest(req *http.Request, opts *options) (res *http.Response, err error) {
|
||||||
|
if opts.human {
|
||||||
|
if req.Header.Get("User-Agent") == "" {
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.54")
|
||||||
|
}
|
||||||
|
if req.Header.Get("Referer") == "" {
|
||||||
|
req.Header.Set("Referer", req.URL.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opts.client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get performs a GET request to the specified URL with optional parameters and headers.
|
||||||
|
func Get(ctx context.Context, urlString string, cbs ...Option) (res *http.Response, err error) {
|
||||||
|
var (
|
||||||
|
uri *url.URL
|
||||||
|
req *http.Request
|
||||||
|
)
|
||||||
|
opts := newOptions()
|
||||||
|
for _, cb := range cbs {
|
||||||
|
cb(opts)
|
||||||
|
}
|
||||||
|
if uri, err = url.Parse(urlString); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.params != nil {
|
||||||
|
qs := uri.Query()
|
||||||
|
for k, v := range opts.params {
|
||||||
|
qs.Set(k, v)
|
||||||
|
}
|
||||||
|
uri.RawQuery = qs.Encode()
|
||||||
|
}
|
||||||
|
if req, err = http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.header != nil {
|
||||||
|
for k, v := range opts.header {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doHttpRequest(req, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post performs a POST request to the specified URL with optional parameters, headers, and data.
|
||||||
|
func Post(ctx context.Context, urlString string, cbs ...Option) (res *http.Response, err error) {
|
||||||
|
var (
|
||||||
|
uri *url.URL
|
||||||
|
req *http.Request
|
||||||
|
contentType string
|
||||||
|
reader io.Reader
|
||||||
|
)
|
||||||
|
opts := newOptions()
|
||||||
|
for _, cb := range cbs {
|
||||||
|
cb(opts)
|
||||||
|
}
|
||||||
|
if uri, err = url.Parse(urlString); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.params != nil {
|
||||||
|
qs := uri.Query()
|
||||||
|
for k, v := range opts.params {
|
||||||
|
qs.Set(k, v)
|
||||||
|
}
|
||||||
|
uri.RawQuery = qs.Encode()
|
||||||
|
}
|
||||||
|
if opts.body != nil {
|
||||||
|
if reader, contentType, err = encodeBody(opts.body); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req, err = http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), reader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.header != nil {
|
||||||
|
for k, v := range opts.header {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if contentType != "" {
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
}
|
||||||
|
return doHttpRequest(req, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do performs a request to the specified URL with optional parameters, headers, and data.
|
||||||
|
func Do(ctx context.Context, urlString string, result any, cbs ...Option) (err error) {
|
||||||
|
var (
|
||||||
|
contentType string
|
||||||
|
reader io.Reader
|
||||||
|
uri *url.URL
|
||||||
|
res *http.Response
|
||||||
|
req *http.Request
|
||||||
|
)
|
||||||
|
opts := newOptions()
|
||||||
|
for _, cb := range cbs {
|
||||||
|
cb(opts)
|
||||||
|
}
|
||||||
|
if uri, err = url.Parse(urlString); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.params != nil {
|
||||||
|
qs := uri.Query()
|
||||||
|
for k, v := range opts.params {
|
||||||
|
qs.Set(k, v)
|
||||||
|
}
|
||||||
|
uri.RawQuery = qs.Encode()
|
||||||
|
}
|
||||||
|
if opts.body != nil {
|
||||||
|
if reader, contentType, err = encodeBody(opts.body); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req, err = http.NewRequestWithContext(ctx, opts.method, uri.String(), reader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.header != nil {
|
||||||
|
for k, v := range opts.header {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if contentType != "" {
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
}
|
||||||
|
if res, err = doHttpRequest(req, opts); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = res.Body.Close()
|
||||||
|
}()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
err = fmt.Errorf("unexpected status %s(%d)", res.Status, res.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//don't care response
|
||||||
|
if result == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
contentType = strings.ToLower(res.Header.Get("Content-Type"))
|
||||||
|
extName := path.Ext(req.URL.String())
|
||||||
|
if strings.Contains(contentType, JSON) || extName == ".json" {
|
||||||
|
err = json.NewDecoder(res.Body).Decode(result)
|
||||||
|
} else if strings.Contains(contentType, XML) || extName == ".xml" {
|
||||||
|
err = xml.NewDecoder(res.Body).Decode(result)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("unsupported content type: %s", contentType)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,235 @@
|
||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
JSON = "application/json"
|
||||||
|
XML = "application/xml"
|
||||||
|
|
||||||
|
plainTextType = "text/plain; charset=utf-8"
|
||||||
|
jsonContentType = "application/json"
|
||||||
|
formContentType = "application/x-www-form-urlencoded"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
jsonCheck = regexp.MustCompile(`(?i:(application|text)/(json|.*\+json|json\-.*)(;|$))`)
|
||||||
|
xmlCheck = regexp.MustCompile(`(?i:(application|text)/(xml|.*\+xml)(;|$))`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
context context.Context
|
||||||
|
method string
|
||||||
|
uri string
|
||||||
|
url *url.URL
|
||||||
|
body any
|
||||||
|
query url.Values
|
||||||
|
formData url.Values
|
||||||
|
header http.Header
|
||||||
|
contentType string
|
||||||
|
authorization Authorization
|
||||||
|
client *Client
|
||||||
|
rawRequest *http.Request
|
||||||
|
rawResponse *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) detectContentType(body any) string {
|
||||||
|
contentType := plainTextType
|
||||||
|
kind := reflect.Indirect(reflect.ValueOf(body)).Type().Kind()
|
||||||
|
switch kind {
|
||||||
|
case reflect.Struct, reflect.Map:
|
||||||
|
contentType = jsonContentType
|
||||||
|
case reflect.String:
|
||||||
|
contentType = plainTextType
|
||||||
|
default:
|
||||||
|
if b, ok := body.([]byte); ok {
|
||||||
|
contentType = http.DetectContentType(b)
|
||||||
|
} else if kind == reflect.Slice {
|
||||||
|
contentType = jsonContentType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) readRequestBody(contentType string, body any) (reader io.Reader, err error) {
|
||||||
|
var (
|
||||||
|
ok bool
|
||||||
|
s string
|
||||||
|
buf []byte
|
||||||
|
)
|
||||||
|
kind := reflect.Indirect(reflect.ValueOf(body)).Type().Kind()
|
||||||
|
if reader, ok = r.body.(io.Reader); ok {
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
if buf, ok = r.body.([]byte); ok {
|
||||||
|
goto __end
|
||||||
|
}
|
||||||
|
if s, ok = r.body.(string); ok {
|
||||||
|
buf = []byte(s)
|
||||||
|
goto __end
|
||||||
|
}
|
||||||
|
if jsonCheck.MatchString(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) {
|
||||||
|
buf, err = json.Marshal(r.body)
|
||||||
|
goto __end
|
||||||
|
}
|
||||||
|
if xmlCheck.MatchString(contentType) && (kind == reflect.Struct) {
|
||||||
|
buf, err = xml.Marshal(r.body)
|
||||||
|
goto __end
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("unmarshal content type %s", contentType)
|
||||||
|
__end:
|
||||||
|
if err == nil {
|
||||||
|
if len(buf) > 0 {
|
||||||
|
return bytes.NewReader(buf), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) SetContext(ctx context.Context) *Request {
|
||||||
|
r.context = ctx
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) AddQuery(k, v string) *Request {
|
||||||
|
r.query.Add(k, v)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) SetQuery(vs map[string]string) *Request {
|
||||||
|
for k, v := range vs {
|
||||||
|
r.query.Set(k, v)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) AddFormData(k, v string) *Request {
|
||||||
|
r.contentType = formContentType
|
||||||
|
r.formData.Add(k, v)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) SetFormData(vs map[string]string) *Request {
|
||||||
|
r.contentType = formContentType
|
||||||
|
for k, v := range vs {
|
||||||
|
r.formData.Set(k, v)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) SetBody(v any) *Request {
|
||||||
|
r.body = v
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) SetContentType(v string) *Request {
|
||||||
|
r.contentType = v
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) AddHeader(k, v string) *Request {
|
||||||
|
r.header.Add(k, v)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) SetHeader(h http.Header) *Request {
|
||||||
|
r.header = h
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) Do() (res *http.Response, err error) {
|
||||||
|
var s string
|
||||||
|
s = r.formData.Encode()
|
||||||
|
if len(s) > 0 {
|
||||||
|
r.body = s
|
||||||
|
}
|
||||||
|
r.url.RawQuery = r.query.Encode()
|
||||||
|
r.uri = r.url.String()
|
||||||
|
return r.client.execute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) Response(v any) (err error) {
|
||||||
|
var (
|
||||||
|
res *http.Response
|
||||||
|
buf []byte
|
||||||
|
contentType string
|
||||||
|
)
|
||||||
|
if res, err = r.Do(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = res.Body.Close()
|
||||||
|
}()
|
||||||
|
if res.StatusCode/100 != 2 {
|
||||||
|
if buf, err = io.ReadAll(res.Body); err == nil && len(buf) > 0 {
|
||||||
|
err = fmt.Errorf("http response %s(%d): %s", res.Status, res.StatusCode, string(buf))
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("http response %d: %s", res.StatusCode, res.Status)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contentType = strings.ToLower(res.Header.Get("Content-Type"))
|
||||||
|
extName := path.Ext(r.rawRequest.URL.String())
|
||||||
|
if strings.Contains(contentType, JSON) || extName == ".json" {
|
||||||
|
err = json.NewDecoder(res.Body).Decode(v)
|
||||||
|
} else if strings.Contains(contentType, XML) || extName == ".xml" {
|
||||||
|
err = xml.NewDecoder(res.Body).Decode(v)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("unsupported content type: %s", contentType)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) Download(s string) (err error) {
|
||||||
|
var (
|
||||||
|
fp *os.File
|
||||||
|
res *http.Response
|
||||||
|
)
|
||||||
|
if res, err = r.Do(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = res.Body.Close()
|
||||||
|
}()
|
||||||
|
if fp, err = os.OpenFile(s, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = fp.Close()
|
||||||
|
}()
|
||||||
|
_, err = io.Copy(fp, res.Body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequest(method string, uri string, client *Client) *Request {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
r := &Request{
|
||||||
|
context: context.Background(),
|
||||||
|
method: method,
|
||||||
|
uri: uri,
|
||||||
|
header: make(http.Header),
|
||||||
|
formData: make(url.Values),
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
if r.url, err = url.Parse(uri); err == nil {
|
||||||
|
r.query = r.url.Query()
|
||||||
|
} else {
|
||||||
|
r.query = make(url.Values)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maps"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
options struct {
|
||||||
|
url string
|
||||||
|
method string
|
||||||
|
header map[string]string
|
||||||
|
params map[string]string
|
||||||
|
body any
|
||||||
|
human bool
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
Option func(o *options)
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithUrl(s string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.url = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMethod(s string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.method = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHuman() Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.human = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithClient(c *http.Client) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.client = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHeader(h map[string]string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
if o.header == nil {
|
||||||
|
o.header = make(map[string]string)
|
||||||
|
}
|
||||||
|
maps.Copy(o.header, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithParams(h map[string]string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.params = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBody(v any) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.body = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOptions() *options {
|
||||||
|
return &options{
|
||||||
|
client: DefaultClient,
|
||||||
|
method: http.MethodGet,
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,16 @@ type RestFieldOptions struct {
|
||||||
Format string `protobuf:"bytes,5,opt,name=format,proto3" json:"format,omitempty"`
|
Format string `protobuf:"bytes,5,opt,name=format,proto3" json:"format,omitempty"`
|
||||||
Props string `protobuf:"bytes,6,opt,name=props,proto3" json:"props,omitempty"`
|
Props string `protobuf:"bytes,6,opt,name=props,proto3" json:"props,omitempty"`
|
||||||
Rule string `protobuf:"bytes,7,opt,name=rule,proto3" json:"rule,omitempty"`
|
Rule string `protobuf:"bytes,7,opt,name=rule,proto3" json:"rule,omitempty"`
|
||||||
|
Live string `protobuf:"bytes,8,opt,name=live,proto3" json:"live,omitempty"`
|
||||||
|
Dropdown string `protobuf:"bytes,9,opt,name=dropdown,proto3" json:"dropdown,omitempty"`
|
||||||
|
Enum string `protobuf:"bytes,10,opt,name=enum,proto3" json:"enum,omitempty"`
|
||||||
|
Match string `protobuf:"bytes,11,opt,name=match,proto3" json:"match,omitempty"`
|
||||||
|
Invisible string `protobuf:"bytes,12,opt,name=invisible,proto3" json:"invisible,omitempty"`
|
||||||
|
Tooltip string `protobuf:"bytes,13,opt,name=tooltip,proto3" json:"tooltip,omitempty"`
|
||||||
|
Uploaduri string `protobuf:"bytes,14,opt,name=uploaduri,proto3" json:"uploaduri,omitempty"`
|
||||||
|
Description string `protobuf:"bytes,15,opt,name=description,proto3" json:"description,omitempty"`
|
||||||
|
Readonly string `protobuf:"bytes,16,opt,name=readonly,proto3" json:"readonly,omitempty"`
|
||||||
|
Endofnow string `protobuf:"bytes,17,opt,name=endofnow,proto3" json:"endofnow,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
@ -114,6 +124,76 @@ func (x *RestFieldOptions) GetRule() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *RestFieldOptions) GetLive() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Live
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RestFieldOptions) GetDropdown() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Dropdown
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RestFieldOptions) GetEnum() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Enum
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RestFieldOptions) GetMatch() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Match
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RestFieldOptions) GetInvisible() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Invisible
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RestFieldOptions) GetTooltip() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Tooltip
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RestFieldOptions) GetUploaduri() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Uploaduri
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RestFieldOptions) GetDescription() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Description
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RestFieldOptions) GetReadonly() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Readonly
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RestFieldOptions) GetEndofnow() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Endofnow
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type RestMessageOptions struct {
|
type RestMessageOptions struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Table string `protobuf:"bytes,1,opt,name=table,proto3" json:"table,omitempty"`
|
Table string `protobuf:"bytes,1,opt,name=table,proto3" json:"table,omitempty"`
|
||||||
|
@ -198,7 +278,7 @@ var File_rest_proto protoreflect.FileDescriptor
|
||||||
const file_rest_proto_rawDesc = "" +
|
const file_rest_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"rest.proto\x12\x04aeus\x1a google/protobuf/descriptor.proto\"\xbc\x01\n" +
|
"rest.proto\x12\x04aeus\x1a google/protobuf/descriptor.proto\"\xc6\x03\n" +
|
||||||
"\x10RestFieldOptions\x12\x12\n" +
|
"\x10RestFieldOptions\x12\x12\n" +
|
||||||
"\x04gorm\x18\x01 \x01(\tR\x04gorm\x12\x18\n" +
|
"\x04gorm\x18\x01 \x01(\tR\x04gorm\x12\x18\n" +
|
||||||
"\acomment\x18\x02 \x01(\tR\acomment\x12\x1c\n" +
|
"\acomment\x18\x02 \x01(\tR\acomment\x12\x1c\n" +
|
||||||
|
@ -206,7 +286,18 @@ const file_rest_proto_rawDesc = "" +
|
||||||
"\bposition\x18\x04 \x01(\tR\bposition\x12\x16\n" +
|
"\bposition\x18\x04 \x01(\tR\bposition\x12\x16\n" +
|
||||||
"\x06format\x18\x05 \x01(\tR\x06format\x12\x14\n" +
|
"\x06format\x18\x05 \x01(\tR\x06format\x12\x14\n" +
|
||||||
"\x05props\x18\x06 \x01(\tR\x05props\x12\x12\n" +
|
"\x05props\x18\x06 \x01(\tR\x05props\x12\x12\n" +
|
||||||
"\x04rule\x18\a \x01(\tR\x04rule\"*\n" +
|
"\x04rule\x18\a \x01(\tR\x04rule\x12\x12\n" +
|
||||||
|
"\x04live\x18\b \x01(\tR\x04live\x12\x1a\n" +
|
||||||
|
"\bdropdown\x18\t \x01(\tR\bdropdown\x12\x12\n" +
|
||||||
|
"\x04enum\x18\n" +
|
||||||
|
" \x01(\tR\x04enum\x12\x14\n" +
|
||||||
|
"\x05match\x18\v \x01(\tR\x05match\x12\x1c\n" +
|
||||||
|
"\tinvisible\x18\f \x01(\tR\tinvisible\x12\x18\n" +
|
||||||
|
"\atooltip\x18\r \x01(\tR\atooltip\x12\x1c\n" +
|
||||||
|
"\tuploaduri\x18\x0e \x01(\tR\tuploaduri\x12 \n" +
|
||||||
|
"\vdescription\x18\x0f \x01(\tR\vdescription\x12\x1a\n" +
|
||||||
|
"\breadonly\x18\x10 \x01(\tR\breadonly\x12\x1a\n" +
|
||||||
|
"\bendofnow\x18\x11 \x01(\tR\bendofnow\"*\n" +
|
||||||
"\x12RestMessageOptions\x12\x14\n" +
|
"\x12RestMessageOptions\x12\x14\n" +
|
||||||
"\x05table\x18\x01 \x01(\tR\x05table:M\n" +
|
"\x05table\x18\x01 \x01(\tR\x05table:M\n" +
|
||||||
"\x05field\x12\x1d.google.protobuf.FieldOptions\x18\x96\x97\x03 \x01(\v2\x16.aeus.RestFieldOptionsR\x05field:O\n" +
|
"\x05field\x12\x1d.google.protobuf.FieldOptions\x18\x96\x97\x03 \x01(\v2\x16.aeus.RestFieldOptionsR\x05field:O\n" +
|
||||||
|
|
|
@ -21,6 +21,16 @@ message RestFieldOptions {
|
||||||
string format = 5;
|
string format = 5;
|
||||||
string props = 6;
|
string props = 6;
|
||||||
string rule= 7;
|
string rule= 7;
|
||||||
|
string live = 8;
|
||||||
|
string dropdown = 9;
|
||||||
|
string enum = 10;
|
||||||
|
string match = 11;
|
||||||
|
string invisible = 12;
|
||||||
|
string tooltip = 13;
|
||||||
|
string uploaduri = 14;
|
||||||
|
string description = 15;
|
||||||
|
string readonly = 16;
|
||||||
|
string endofnow = 17;
|
||||||
}
|
}
|
||||||
|
|
||||||
extend google.protobuf.MessageOptions {
|
extend google.protobuf.MessageOptions {
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
package generator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"embed"
|
|
||||||
"html/template"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.nobla.cn/golang/aeus/tools/gen/internal/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
fileMap = map[string]string{
|
|
||||||
"cmd/main.go": MainTemp,
|
|
||||||
"internal/scope/scope.go": ScopeTemp,
|
|
||||||
"internal/service/service.go": ServiceLoaderTemp,
|
|
||||||
"api/v1/pb/greeter.proto": GreeterTemp,
|
|
||||||
"version/version.go": VersionTemp,
|
|
||||||
"Makefile": MakefileTemp,
|
|
||||||
".gitignore": GitIgnoreTemp,
|
|
||||||
"README.md": ReadmeTemp,
|
|
||||||
"go.mod": GoModTemp,
|
|
||||||
"webhook.yaml": WebhookTemp,
|
|
||||||
"deploy/docker/deployment.yaml": DeploymentTemp,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
//go:embed third_party
|
|
||||||
protoDir embed.FS
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
TemplateData struct {
|
|
||||||
ShortName string
|
|
||||||
PackageName string
|
|
||||||
Datetime string
|
|
||||||
Version string
|
|
||||||
ImageRegistry string
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func writeFile(file string, buf []byte) (err error) {
|
|
||||||
dirname := path.Dir(file)
|
|
||||||
if _, err = os.Stat(dirname); err != nil {
|
|
||||||
if err = os.MkdirAll(dirname, 0755); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = os.WriteFile(file, buf, 0644)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanDir(s embed.FS, dirname string, callback func(file string) error) (err error) {
|
|
||||||
var (
|
|
||||||
entities []fs.DirEntry
|
|
||||||
)
|
|
||||||
if entities, err = s.ReadDir(dirname); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, entity := range entities {
|
|
||||||
if entity.Name() == "." || entity.Name() == ".." {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := path.Join(dirname, entity.Name())
|
|
||||||
if entity.IsDir() {
|
|
||||||
scanDir(s, name, callback)
|
|
||||||
} else {
|
|
||||||
if err = callback(name); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func Geerate(app *types.Applicetion) (err error) {
|
|
||||||
shortName := app.ShortName()
|
|
||||||
data := TemplateData{
|
|
||||||
ShortName: shortName,
|
|
||||||
PackageName: app.Package,
|
|
||||||
Version: app.Version,
|
|
||||||
ImageRegistry: "{{IMAGE_REGISTRY_URL}}",
|
|
||||||
Datetime: time.Now().Format(time.DateTime),
|
|
||||||
}
|
|
||||||
if data.Version == "" {
|
|
||||||
data.Version = "v0.0.1"
|
|
||||||
}
|
|
||||||
var t *template.Template
|
|
||||||
writer := bytes.NewBuffer(nil)
|
|
||||||
for name, tmpl := range fileMap {
|
|
||||||
if t, err = template.New(name).Parse(tmpl); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = t.Execute(writer, data); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = writeFile(path.Join(shortName, name), writer.Bytes()); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writer.Reset()
|
|
||||||
}
|
|
||||||
if err = writeFile(shortName+".go", []byte("package "+shortName)); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = scanDir(protoDir, "third_party", func(filename string) error {
|
|
||||||
if fp, openerr := protoDir.Open(filename); openerr != nil {
|
|
||||||
return openerr
|
|
||||||
} else {
|
|
||||||
if buf, readerr := io.ReadAll(fp); readerr == nil {
|
|
||||||
writeFile(path.Join(shortName, filename), buf)
|
|
||||||
}
|
|
||||||
fp.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,271 +0,0 @@
|
||||||
package generator
|
|
||||||
|
|
||||||
var (
|
|
||||||
MainTemp = `
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"flag"
|
|
||||||
"git.nobla.cn/golang/aeus"
|
|
||||||
"git.nobla.cn/golang/aeus/transport/cli"
|
|
||||||
"git.nobla.cn/golang/aeus/transport/grpc"
|
|
||||||
"git.nobla.cn/golang/aeus/transport/http"
|
|
||||||
|
|
||||||
"{{.PackageName}}/version"
|
|
||||||
"{{.PackageName}}/internal/scope"
|
|
||||||
"{{.PackageName}}/internal/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
versionFlag = flag.Bool("version", false, "Show version")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
flag.Parse()
|
|
||||||
if *versionFlag{
|
|
||||||
fmt.Println(version.Info())
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
app := aeus.New(
|
|
||||||
aeus.WithName(version.ProductName),
|
|
||||||
aeus.WithVersion(version.Version),
|
|
||||||
aeus.WithServer(
|
|
||||||
http.New(),
|
|
||||||
grpc.New(),
|
|
||||||
cli.New(),
|
|
||||||
),
|
|
||||||
aeus.WithScope(scope.NewScope()),
|
|
||||||
aeus.WithServiceLoader(service.NewLoader()),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = app.Run(); err != nil {
|
|
||||||
fmt.Println("app run error:", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
ScopeTemp = `
|
|
||||||
package scope
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.nobla.cn/golang/aeus/transport/cli"
|
|
||||||
"git.nobla.cn/golang/aeus/transport/grpc"
|
|
||||||
"git.nobla.cn/golang/aeus/transport/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ScopeContext struct {
|
|
||||||
ctx context.Context
|
|
||||||
Http *http.Server
|
|
||||||
Grpc *grpc.Server
|
|
||||||
Cli *cli.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ScopeContext) Init(ctx context.Context) (err error) {
|
|
||||||
s.ctx = ctx
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewScope() *ScopeContext {
|
|
||||||
return &ScopeContext{}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
ServiceLoaderTemp = `
|
|
||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"{{.PackageName}}/internal/scope"
|
|
||||||
"git.nobla.cn/golang/aeus/transport/cli"
|
|
||||||
"git.nobla.cn/golang/aeus/transport/grpc"
|
|
||||||
"git.nobla.cn/golang/aeus/transport/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type serviceLoader struct {
|
|
||||||
Sope *scope.ScopeContext
|
|
||||||
Http *http.Server
|
|
||||||
Grpc *grpc.Server
|
|
||||||
Cli *cli.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *serviceLoader) Init(ctx context.Context) (err error) {
|
|
||||||
// bind services here
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *serviceLoader) Run(ctx context.Context) (err error) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLoader() *serviceLoader {
|
|
||||||
return &serviceLoader{}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
MakefileTemp = `
|
|
||||||
GOHOSTOS:=$(shell go env GOHOSTOS)
|
|
||||||
GOPATH:=$(shell go env GOPATH)
|
|
||||||
VERSION=$(shell git describe --tags --always)
|
|
||||||
DATETIME:=$(shell date "+%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
|
|
||||||
PROTO_DIR="api/v1/pb"
|
|
||||||
PROTO_OUT_DIR="api/v1/pb"
|
|
||||||
PROTO_FILES=$(shell find api -name *.proto)
|
|
||||||
|
|
||||||
.PHONY: proto
|
|
||||||
proto:
|
|
||||||
protoc --proto_path=$(PROTO_DIR) \
|
|
||||||
--proto_path=./third_party \
|
|
||||||
--go_out=paths=source_relative:$(PROTO_OUT_DIR) \
|
|
||||||
--go-grpc_out=paths=source_relative:$(PROTO_OUT_DIR) \
|
|
||||||
--go-aeus_out=paths=source_relative:$(PROTO_OUT_DIR) \
|
|
||||||
--validate_out=paths=source_relative,lang=go:$(PROTO_OUT_DIR) \
|
|
||||||
$(PROTO_FILES)
|
|
||||||
|
|
||||||
.PHONY: proto-clean
|
|
||||||
proto-clean:
|
|
||||||
rm -rf $(PROTO_OUT_DIR)/*.pb.go
|
|
||||||
rm -rf $(PROTO_OUT_DIR)/*.pb.validate.go
|
|
||||||
|
|
||||||
.PHONY: docker
|
|
||||||
docker:
|
|
||||||
docker build . -t $(IMAGE_REGISTRY_URL)
|
|
||||||
|
|
||||||
.PHONY: deploy
|
|
||||||
deploy:
|
|
||||||
dkctl apply -f deployment.yaml
|
|
||||||
|
|
||||||
.PHONY: build
|
|
||||||
build:
|
|
||||||
go mod tidy
|
|
||||||
go mod vendor
|
|
||||||
CGO_ENABLED=0 go build -a -installsuffix cgo -ldflags "-s -w -X '{{.PackageName}}/version.Version=$(VERSION)' -X '{{.PackageName}}/version.BuildDate=$(DATETIME)'" -o bin/{{.ShortName}} cmd/main.go
|
|
||||||
`
|
|
||||||
|
|
||||||
GreeterTemp = `
|
|
||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package greeter;
|
|
||||||
|
|
||||||
import "google/api/annotations.proto";
|
|
||||||
import "aeus/command.proto";
|
|
||||||
import "aeus/rest.proto";
|
|
||||||
import "validate/validate.proto";
|
|
||||||
|
|
||||||
option go_package = "{{.PackageName}}/api/v1/pb;pb";
|
|
||||||
|
|
||||||
// The greeting service definition.
|
|
||||||
service Greeter {
|
|
||||||
// Sends a greeting
|
|
||||||
rpc SayHello (HelloRequest) returns (HelloReply) {
|
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/helloworld/{name}"
|
|
||||||
};
|
|
||||||
option (aeus.command) = {
|
|
||||||
path: "/helloworld/:name",
|
|
||||||
description: "Greeter"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The request message containing the user's name.
|
|
||||||
message HelloRequest {
|
|
||||||
option (aeus.rest) = {
|
|
||||||
table: "users"
|
|
||||||
};
|
|
||||||
int64 id = 1 [(aeus.field)={gorm:"primary_key"},(validate.rules).int64.gt = 999];
|
|
||||||
string name = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The response message containing the greetings
|
|
||||||
message HelloReply {
|
|
||||||
string message = 1;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
VersionTemp = `
|
|
||||||
package version
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
var (
|
|
||||||
Version = "{{.Version}}"
|
|
||||||
BuildDate = "{{.Datetime}}"
|
|
||||||
ProductName = "{{.ShortName}}"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Info() string {
|
|
||||||
return fmt.Sprintf("%s version: %s (built at %s)", ProductName, Version, BuildDate)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
GitIgnoreTemp = `
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
|
|
||||||
bin/
|
|
||||||
.svn/
|
|
||||||
.godeps
|
|
||||||
./build
|
|
||||||
.cover/
|
|
||||||
*.dat
|
|
||||||
vendor
|
|
||||||
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Folders
|
|
||||||
_obj
|
|
||||||
_test
|
|
||||||
`
|
|
||||||
ReadmeTemp = ``
|
|
||||||
|
|
||||||
GoModTemp = `
|
|
||||||
module {{.PackageName}}
|
|
||||||
|
|
||||||
go 1.23.0
|
|
||||||
`
|
|
||||||
WebhookTemp = `
|
|
||||||
name: {{.ShortName}}
|
|
||||||
steps:
|
|
||||||
- name: build
|
|
||||||
run: "make build"
|
|
||||||
- name: docker
|
|
||||||
run: "make docker"
|
|
||||||
- name: deploy
|
|
||||||
run: "make deploy"
|
|
||||||
replacements:
|
|
||||||
- src: deploy/docker/deployment.yaml
|
|
||||||
dst: deployment.yaml
|
|
||||||
`
|
|
||||||
DeploymentTemp = `
|
|
||||||
name: {{.ShortName}}
|
|
||||||
image: {{.ImageRegistry}}
|
|
||||||
command: ["{{.ShortName}}"]
|
|
||||||
|
|
||||||
network:
|
|
||||||
name: employ
|
|
||||||
ip: 10.5.10.2
|
|
||||||
|
|
||||||
env:
|
|
||||||
- name: TZ
|
|
||||||
value: "Asia/Shanghai"
|
|
||||||
- name: APP_NAME
|
|
||||||
value: "{{.ShortName}}"
|
|
||||||
|
|
||||||
volume:
|
|
||||||
- name: config
|
|
||||||
path: /etc/{{.ShortName}}/
|
|
||||||
hostPath: /apps/{{.ShortName}}/conf/
|
|
||||||
`
|
|
||||||
)
|
|
|
@ -1,16 +0,0 @@
|
||||||
package types
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
type Applicetion struct {
|
|
||||||
Package string
|
|
||||||
Version string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *Applicetion) ShortName() string {
|
|
||||||
pos := strings.LastIndex(app.Package, "/")
|
|
||||||
if pos > -1 {
|
|
||||||
return app.Package[pos+1:]
|
|
||||||
}
|
|
||||||
return app.Package
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"git.nobla.cn/golang/aeus/tools/gen/internal/generator"
|
|
||||||
"git.nobla.cn/golang/aeus/tools/gen/internal/types"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func waitingSignal(ctx context.Context, cancelFunc context.CancelFunc) {
|
|
||||||
ch := make(chan os.Signal, 1)
|
|
||||||
signals := []os.Signal{syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL}
|
|
||||||
signal.Notify(ch, signals...)
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case <-ch:
|
|
||||||
cancelFunc()
|
|
||||||
close(ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createCommand() *cobra.Command {
|
|
||||||
var (
|
|
||||||
version string
|
|
||||||
)
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "new",
|
|
||||||
Short: "Create microservice application",
|
|
||||||
Long: "Create microservice application",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return fmt.Errorf("Please specify the package name")
|
|
||||||
}
|
|
||||||
if version, err = cmd.Flags().GetString("version"); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return generator.Geerate(&types.Applicetion{
|
|
||||||
Package: args[0],
|
|
||||||
Version: version,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
cmd.Flags().StringP("version", "v", "v0.0.1", "Application version")
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "aeus",
|
|
||||||
Short: "aeus is a tool for manager microservices",
|
|
||||||
Long: "aeus is a tool for manager microservice application",
|
|
||||||
SilenceErrors: true,
|
|
||||||
}
|
|
||||||
go waitingSignal(ctx, cancelFunc)
|
|
||||||
cmd.AddCommand(createCommand())
|
|
||||||
if err = cmd.ExecuteContext(ctx); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
cancelFunc()
|
|
||||||
}
|
|
|
@ -6,7 +6,9 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
@ -243,6 +245,8 @@ func New(cbs ...Option) *Server {
|
||||||
uri: &url.URL{Scheme: "cli"},
|
uri: &url.URL{Scheme: "cli"},
|
||||||
router: newRouter(""),
|
router: newRouter(""),
|
||||||
}
|
}
|
||||||
|
port, _ := strconv.Atoi(os.Getenv("CLI_PORT"))
|
||||||
|
srv.opts.address = fmt.Sprintf(":%d", port)
|
||||||
for _, cb := range cbs {
|
for _, cb := range cbs {
|
||||||
cb(srv.opts)
|
cb(srv.opts)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,11 @@ package grpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"git.nobla.cn/golang/aeus/metadata"
|
"git.nobla.cn/golang/aeus/metadata"
|
||||||
"git.nobla.cn/golang/aeus/middleware"
|
"git.nobla.cn/golang/aeus/middleware"
|
||||||
|
@ -136,7 +139,6 @@ func New(cbs ...Option) *Server {
|
||||||
opts: &options{
|
opts: &options{
|
||||||
network: "tcp",
|
network: "tcp",
|
||||||
logger: logger.Default(),
|
logger: logger.Default(),
|
||||||
address: ":0",
|
|
||||||
grpcOpts: make([]grpc.ServerOption, 0, 10),
|
grpcOpts: make([]grpc.ServerOption, 0, 10),
|
||||||
},
|
},
|
||||||
uri: &url.URL{
|
uri: &url.URL{
|
||||||
|
@ -144,6 +146,8 @@ func New(cbs ...Option) *Server {
|
||||||
},
|
},
|
||||||
middlewares: make([]middleware.Middleware, 0, 10),
|
middlewares: make([]middleware.Middleware, 0, 10),
|
||||||
}
|
}
|
||||||
|
port, _ := strconv.Atoi(os.Getenv("GRPC_PORT"))
|
||||||
|
svr.opts.address = fmt.Sprintf(":%d", port)
|
||||||
for _, cb := range cbs {
|
for _, cb := range cbs {
|
||||||
cb(svr.opts)
|
cb(svr.opts)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,10 @@ func (c *Context) Context() context.Context {
|
||||||
return c.ctx.Request.Context()
|
return c.ctx.Request.Context()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) Gin() *gin.Context {
|
||||||
|
return c.ctx
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Context) Request() *http.Request {
|
func (c *Context) Request() *http.Request {
|
||||||
return c.ctx.Request
|
return c.ctx.Request
|
||||||
}
|
}
|
||||||
|
@ -38,6 +42,14 @@ func (c *Context) Param(key string) string {
|
||||||
return c.ctx.Param(key)
|
return c.ctx.Param(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) Query(key string) string {
|
||||||
|
qs := c.ctx.Request.URL.Query()
|
||||||
|
if qs != nil {
|
||||||
|
return qs.Get(key)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Context) Bind(val any) (err error) {
|
func (c *Context) Bind(val any) (err error) {
|
||||||
// if params exists, try bind params first
|
// if params exists, try bind params first
|
||||||
if len(c.ctx.Params) > 0 {
|
if len(c.ctx.Params) > 0 {
|
||||||
|
|
|
@ -2,11 +2,18 @@ package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -90,6 +97,12 @@ func (s *Server) Use(middlewares ...middleware.Middleware) {
|
||||||
s.middlewares = append(s.middlewares, middlewares...)
|
s.middlewares = append(s.middlewares, middlewares...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) Handle(method string, uri string, handler http.HandlerFunc) {
|
||||||
|
s.engine.Handle(method, uri, func(ctx *gin.Context) {
|
||||||
|
handler(ctx.Writer, ctx.Request)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) Webroot(prefix string, fs http.FileSystem) {
|
func (s *Server) Webroot(prefix string, fs http.FileSystem) {
|
||||||
s.fs = newFS(time.Now(), fs)
|
s.fs = newFS(time.Now(), fs)
|
||||||
s.fs.SetPrefix(prefix)
|
s.fs.SetPrefix(prefix)
|
||||||
|
@ -97,23 +110,99 @@ func (s *Server) Webroot(prefix string, fs http.FileSystem) {
|
||||||
s.fs.SetIndexFile("/index.html")
|
s.fs.SetIndexFile("/index.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) shouldCompress(req *http.Request) bool {
|
||||||
|
if !strings.Contains(req.Header.Get(headerAcceptEncoding), "gzip") ||
|
||||||
|
strings.Contains(req.Header.Get("Connection"), "Upgrade") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the request path is excluded from compression
|
||||||
|
extension := filepath.Ext(req.URL.Path)
|
||||||
|
if slices.Contains(assetsExtensions, extension) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) staticHandle(ctx *gin.Context, fp http.File) {
|
||||||
|
uri := path.Clean(ctx.Request.URL.Path)
|
||||||
|
fi, err := fp.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !fi.IsDir() {
|
||||||
|
//https://github.com/gin-contrib/gzip
|
||||||
|
if s.shouldCompress(ctx.Request) && fi.Size() > 8192 {
|
||||||
|
gzWriter := newGzipWriter()
|
||||||
|
gzWriter.Reset(ctx.Writer)
|
||||||
|
ctx.Header(headerContentEncoding, "gzip")
|
||||||
|
ctx.Writer.Header().Add(headerVary, headerAcceptEncoding)
|
||||||
|
originalEtag := ctx.GetHeader("ETag")
|
||||||
|
if originalEtag != "" && !strings.HasPrefix(originalEtag, "W/") {
|
||||||
|
ctx.Header("ETag", "W/"+originalEtag)
|
||||||
|
}
|
||||||
|
ctx.Writer = &gzipWriter{ctx.Writer, gzWriter}
|
||||||
|
defer func() {
|
||||||
|
if ctx.Writer.Size() < 0 {
|
||||||
|
gzWriter.Reset(io.Discard)
|
||||||
|
}
|
||||||
|
gzWriter.Close()
|
||||||
|
if ctx.Writer.Size() > -1 {
|
||||||
|
ctx.Header("Content-Length", strconv.Itoa(ctx.Writer.Size()))
|
||||||
|
}
|
||||||
|
putGzipWriter(gzWriter)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http.ServeContent(ctx.Writer, ctx.Request, path.Base(uri), s.fs.modtime, fp)
|
||||||
|
ctx.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) notFoundHandle(ctx *gin.Context) {
|
func (s *Server) notFoundHandle(ctx *gin.Context) {
|
||||||
if s.fs != nil && ctx.Request.Method == http.MethodGet {
|
if s.fs != nil && ctx.Request.Method == http.MethodGet {
|
||||||
uri := path.Clean(ctx.Request.URL.Path)
|
uri := path.Clean(ctx.Request.URL.Path)
|
||||||
if fp, err := s.fs.Open(uri); err == nil {
|
if fp, err := s.fs.Open(uri); err == nil {
|
||||||
http.ServeContent(ctx.Writer, ctx.Request, path.Base(uri), s.fs.modtime, fp)
|
s.staticHandle(ctx, fp)
|
||||||
fp.Close()
|
fp.Close()
|
||||||
ctx.Abort()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusNotFound, newResponse(errors.NotFound, "Not Found", nil))
|
ctx.JSON(http.StatusNotFound, newResponse(errors.NotFound, "Not Found", nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) CORSInterceptor() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.Writer.Header().Add("Vary", "Origin")
|
||||||
|
c.Writer.Header().Add("Vary", "Access-Control-Request-Method")
|
||||||
|
c.Writer.Header().Add("Vary", "Access-Control-Request-Headers")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE")
|
||||||
|
h := c.Request.Header.Get("Access-Control-Request-Headers")
|
||||||
|
if h != "" {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Headers", h)
|
||||||
|
}
|
||||||
|
c.AbortWithStatus(204)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
c.Writer.Header().Add("Vary", "Origin")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
h := c.Request.Header.Get("Access-Control-Request-Headers")
|
||||||
|
if h != "" {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Headers", h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) requestInterceptor() gin.HandlerFunc {
|
func (s *Server) requestInterceptor() gin.HandlerFunc {
|
||||||
return func(ginCtx *gin.Context) {
|
return func(ginCtx *gin.Context) {
|
||||||
ctx := ginCtx.Request.Context()
|
ctx := ginCtx.Request.Context()
|
||||||
next := func(ctx context.Context) error {
|
next := func(ctx context.Context) error {
|
||||||
|
ginCtx.Request = ginCtx.Request.WithContext(ctx)
|
||||||
ginCtx.Next()
|
ginCtx.Next()
|
||||||
if err := ginCtx.Errors.Last(); err != nil {
|
if err := ginCtx.Errors.Last(); err != nil {
|
||||||
return err.Err
|
return err.Err
|
||||||
|
@ -133,8 +222,8 @@ func (s *Server) requestInterceptor() gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
md.Set(metadata.RequestProtocolKey, Protocol)
|
md.Set(metadata.RequestProtocolKey, Protocol)
|
||||||
md.Set(metadata.RequestPathKey, ginCtx.Request.URL.Path)
|
md.Set(metadata.RequestPathKey, ginCtx.Request.URL.Path)
|
||||||
|
md.Set("method", ginCtx.Request.Method)
|
||||||
ctx = metadata.NewContext(ctx, md)
|
ctx = metadata.NewContext(ctx, md)
|
||||||
ginCtx.Request = ginCtx.Request.WithContext(ctx)
|
|
||||||
if err := handler(ctx); err != nil {
|
if err := handler(ctx); err != nil {
|
||||||
if se, ok := err.(*errors.Error); ok {
|
if se, ok := err.(*errors.Error); ok {
|
||||||
ginCtx.AbortWithStatusJSON(http.StatusInternalServerError, newResponse(se.Code, se.Message, nil))
|
ginCtx.AbortWithStatusJSON(http.StatusInternalServerError, newResponse(se.Code, se.Message, nil))
|
||||||
|
@ -207,9 +296,10 @@ func New(cbs ...Option) *Server {
|
||||||
opts: &options{
|
opts: &options{
|
||||||
network: "tcp",
|
network: "tcp",
|
||||||
logger: logger.Default(),
|
logger: logger.Default(),
|
||||||
address: ":0",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
port, _ := strconv.Atoi(os.Getenv("HTTP_PORT"))
|
||||||
|
svr.opts.address = fmt.Sprintf(":%d", port)
|
||||||
for _, cb := range cbs {
|
for _, cb := range cbs {
|
||||||
cb(svr.opts)
|
cb(svr.opts)
|
||||||
}
|
}
|
||||||
|
@ -217,6 +307,9 @@ func New(cbs ...Option) *Server {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
svr.engine = gin.New(svr.opts.ginOptions...)
|
svr.engine = gin.New(svr.opts.ginOptions...)
|
||||||
|
if svr.opts.enableCORS {
|
||||||
|
svr.engine.Use(svr.CORSInterceptor())
|
||||||
|
}
|
||||||
svr.engine.Use(svr.requestInterceptor())
|
svr.engine.Use(svr.requestInterceptor())
|
||||||
return svr
|
return svr
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"git.nobla.cn/golang/aeus/pkg/logger"
|
"git.nobla.cn/golang/aeus/pkg/logger"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
@ -25,6 +31,7 @@ type (
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
context context.Context
|
context context.Context
|
||||||
ginOptions []gin.OptionFunc
|
ginOptions []gin.OptionFunc
|
||||||
|
enableCORS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleFunc func(ctx *Context) (err error)
|
HandleFunc func(ctx *Context) (err error)
|
||||||
|
@ -40,12 +47,89 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
headerAcceptEncoding = "Accept-Encoding"
|
||||||
|
headerContentEncoding = "Content-Encoding"
|
||||||
|
headerVary = "Vary"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
gzPool sync.Pool
|
||||||
|
assetsExtensions = []string{".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".otf"}
|
||||||
|
)
|
||||||
|
|
||||||
|
type gzipWriter struct {
|
||||||
|
gin.ResponseWriter
|
||||||
|
writer *gzip.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gzipWriter) WriteString(s string) (int, error) {
|
||||||
|
g.Header().Del("Content-Length")
|
||||||
|
return g.writer.Write([]byte(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gzipWriter) Write(data []byte) (int, error) {
|
||||||
|
g.Header().Del("Content-Length")
|
||||||
|
return g.writer.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gzipWriter) Flush() {
|
||||||
|
_ = g.writer.Flush()
|
||||||
|
g.ResponseWriter.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix: https://github.com/mholt/caddy/issues/38
|
||||||
|
func (g *gzipWriter) WriteHeader(code int) {
|
||||||
|
g.Header().Del("Content-Length")
|
||||||
|
g.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ http.Hijacker = (*gzipWriter)(nil)
|
||||||
|
|
||||||
|
// Hijack allows the caller to take over the connection from the HTTP server.
|
||||||
|
// After a call to Hijack, the HTTP server library will not do anything else with the connection.
|
||||||
|
// It becomes the caller's responsibility to manage and close the connection.
|
||||||
|
//
|
||||||
|
// It returns the underlying net.Conn, a buffered reader/writer for the connection, and an error
|
||||||
|
// if the ResponseWriter does not support the Hijacker interface.
|
||||||
|
func (g *gzipWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
hijacker, ok := g.ResponseWriter.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface")
|
||||||
|
}
|
||||||
|
return hijacker.Hijack()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGzipWriter() (writer *gzip.Writer) {
|
||||||
|
v := gzPool.Get()
|
||||||
|
if v == nil {
|
||||||
|
writer, _ = gzip.NewWriterLevel(io.Discard, gzip.DefaultCompression)
|
||||||
|
} else {
|
||||||
|
if w, ok := v.(*gzip.Writer); ok {
|
||||||
|
return w
|
||||||
|
} else {
|
||||||
|
writer, _ = gzip.NewWriterLevel(io.Discard, gzip.DefaultCompression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func putGzipWriter(writer *gzip.Writer) {
|
||||||
|
gzPool.Put(writer)
|
||||||
|
}
|
||||||
|
|
||||||
func WithNetwork(network string) Option {
|
func WithNetwork(network string) Option {
|
||||||
return func(o *options) {
|
return func(o *options) {
|
||||||
o.network = network
|
o.network = network
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithCORS() Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.enableCORS = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithAddress(address string) Option {
|
func WithAddress(address string) Option {
|
||||||
return func(o *options) {
|
return func(o *options) {
|
||||||
o.address = address
|
o.address = address
|
||||||
|
|
Loading…
Reference in New Issue