Compare commits

..

No commits in common. "main" and "v0.0.7" have entirely different histories.
main ... v0.0.7

32 changed files with 551 additions and 1377 deletions

1
.gitignore vendored
View File

@ -11,7 +11,6 @@ _posts
.vscode
vendor
cmd/
tools/
third_party/
# Go.gitignore

View File

@ -4,99 +4,5 @@
# 环境变量
| 环境变量 | 描述 |
| --- | --- |
| 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
View File

@ -6,7 +6,6 @@ import (
"os/signal"
"reflect"
"runtime"
"strconv"
"sync/atomic"
"syscall"
"time"
@ -36,19 +35,12 @@ func (s *Service) Name() string {
}
func (s *Service) Debug() bool {
if s.opts != nil {
return s.opts.debug
}
return false
}
func (s *Service) Version() string {
if s.opts != nil {
return s.opts.version
}
return ""
}
func (s *Service) Metadata() map[string]string {
if s.service == nil {
return nil
@ -122,7 +114,7 @@ func (s *Service) injectVars(v any) {
}
func (s *Service) preStart(ctx context.Context) (err error) {
s.Logger().Info(ctx, "starting")
s.Logger().Info(s.ctx, "starting")
for _, ptr := range s.opts.servers {
s.refValues = append(s.refValues, reflect.ValueOf(ptr))
}
@ -263,7 +255,6 @@ func New(cbs ...Option) *Service {
registrarTimeout: time.Second * 30,
},
}
s.opts.debug, _ = strconv.ParseBool("AEUS_DEBUG")
s.opts.metadata = make(map[string]string)
for _, cb := range cbs {
cb(s.opts)

3
go.mod
View File

@ -11,7 +11,6 @@ require (
github.com/google/uuid v1.6.0
github.com/mattn/go-runewidth v0.0.16
github.com/peterh/liner v1.2.2
github.com/redis/go-redis/v9 v9.10.0
github.com/spf13/cobra v1.9.1
go.etcd.io/etcd/api/v3 v3.6.0
go.etcd.io/etcd/client/v3 v3.6.0
@ -25,12 +24,10 @@ require (
require (
github.com/bytedance/sonic v1.11.6 // 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/iasm v0.2.0 // indirect
github.com/coreos/go-semver v0.3.1 // 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/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect

10
go.sum
View File

@ -1,13 +1,7 @@
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/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
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/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/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
@ -20,8 +14,6 @@ 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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
@ -94,8 +86,6 @@ 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=

View File

@ -2,7 +2,6 @@ package auth
import (
"context"
"reflect"
"strings"
"git.nobla.cn/golang/aeus/metadata"
@ -30,15 +29,19 @@ const (
type Option func(*options)
type Validate interface {
Validate(ctx context.Context, token string) error
}
// Parser is a jwt parser
type options struct {
allows []string
claims reflect.Type
validate Validate
claims func() jwt.Claims
}
// 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
@ -47,52 +50,27 @@ func WithAllow(paths ...string) Option {
if o.allows == nil {
o.allows = make([]string, 0, 16)
}
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
o.allows = append(o.allows, paths...)
}
}
// isAllowed check if the path is allowed
func isAllowed(uripath string, allows []string) bool {
for _, pattern := range allows {
n := len(pattern)
if pattern == uripath {
return true
for _, str := range allows {
n := len(str)
if n == 0 {
continue
}
if pattern == "*" {
return true
}
if n > 1 && pattern[n-1] == '*' {
if strings.HasPrefix(uripath, pattern[:n-1]) {
if n > 1 && str[n-1] == '*' {
if strings.HasPrefix(uripath, str[:n-1]) {
return true
}
}
if str == uripath {
return true
}
return false
}
return true
}
// JWT auth middleware
@ -112,27 +90,21 @@ func JWT(keyFunc jwt.Keyfunc, cbs ...Option) middleware.Middleware {
}
}
}
token, ok := md.Get(authorizationKey)
authorizationValue, ok := md.Get(authorizationKey)
if !ok {
return errors.ErrAccessDenied
}
if opts.validate != nil {
if err = opts.validate.Validate(ctx, token); err != nil {
return err
if !strings.HasPrefix(authorizationValue, bearerWord) {
return errors.ErrAccessDenied
}
}
token, _ = strings.CutPrefix(token, bearerWord)
var (
ti *jwt.Token
)
token = strings.TrimSpace(token)
authorizationToken := strings.TrimSpace(strings.TrimPrefix(authorizationValue, bearerWord))
if opts.claims != nil {
if claims, ok := reflect.New(opts.claims).Interface().(jwt.Claims); ok {
ti, err = jwt.ParseWithClaims(token, claims, keyFunc)
}
}
if ti == nil {
ti, err = jwt.Parse(token, keyFunc)
ti, err = jwt.ParseWithClaims(authorizationToken, opts.claims(), keyFunc)
} else {
ti, err = jwt.Parse(authorizationToken, keyFunc)
}
if err != nil {
if errors.Is(err, jwt.ErrTokenMalformed) || errors.Is(err, jwt.ErrTokenUnverifiable) {

View File

@ -2,7 +2,6 @@ package aeus
import (
"context"
"maps"
"time"
"git.nobla.cn/golang/aeus/pkg/logger"
@ -19,7 +18,6 @@ type options struct {
servers []Server
endpoints []string
scope Scope
debug bool
registrarTimeout time.Duration
registry registry.Registry
serviceLoader ServiceLoader
@ -43,7 +41,9 @@ func WithMetadata(metadata map[string]string) Option {
if o.metadata == nil {
o.metadata = make(map[string]string)
}
maps.Copy(o.metadata, metadata)
for k, v := range metadata {
o.metadata[k] = v
}
}
}
@ -71,12 +71,6 @@ func WithScope(scope Scope) Option {
}
}
func WithDebug(debug bool) Option {
return func(o *options) {
o.debug = debug
}
}
func WithServiceLoader(loader ServiceLoader) Option {
return func(o *options) {
o.serviceLoader = loader

18
pkg/cache/cache.go vendored
View File

@ -13,29 +13,23 @@ var (
type Cache interface {
// Get gets a cached value by key.
Load(ctx context.Context, key string, val any) error
// Get gets a cached value by key.
Exists(ctx context.Context, key string) (bool, error)
Get(ctx context.Context, key string) (any, time.Time, error)
// Put stores a key-value pair into cache.
Store(ctx context.Context, key string, val any, d time.Duration) error
Put(ctx context.Context, key string, val any, d time.Duration) error
// Delete removes a key from cache.
Delete(ctx context.Context, key string) error
// String returns the name of the implementation.
String() string
}
func Default() Cache {
return std
}
// Get gets a cached value by key.
func Load(ctx context.Context, key string, val any) error {
return std.Load(ctx, key, val)
func Get(ctx context.Context, key string) (any, time.Time, error) {
return std.Get(ctx, key)
}
// Put stores a key-value pair into cache.
func Store(ctx context.Context, key string, val any, d time.Duration) error {
return std.Store(ctx, key, val, d)
func Put(ctx context.Context, key string, val any, d time.Duration) error {
return std.Put(ctx, key, val, d)
}
// String returns the name of the implementation.

View File

@ -2,18 +2,10 @@ package memory
import (
"context"
"fmt"
"reflect"
"sync"
"time"
"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")
"git.nobla.cn/golang/aeus/pkg/errors"
)
type memCache struct {
@ -23,47 +15,22 @@ type memCache struct {
sync.RWMutex
}
func (c *memCache) Load(ctx context.Context, key string, val any) error {
func (c *memCache) Get(ctx context.Context, key string) (interface{}, time.Time, error) {
c.RWMutex.RLock()
defer c.RWMutex.RUnlock()
item, found := c.items[key]
if !found {
return ErrNotExists
return nil, time.Time{}, errors.ErrNotFound
}
if item.Expired() {
return ErrNotExists
return nil, time.Time{}, errors.ErrExpired
}
refValue := reflect.ValueOf(val)
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
return item.Value, time.Unix(0, item.Expiration), nil
}
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 {
func (c *memCache) Put(ctx context.Context, key string, val interface{}, d time.Duration) error {
var e int64
if d == DefaultExpiration {
d = c.opts.Expiration
@ -89,8 +56,9 @@ func (c *memCache) Delete(ctx context.Context, key string) error {
_, found := c.items[key]
if !found {
return ErrNotExists
return errors.ErrNotFound
}
delete(c.items, key)
return nil
}
@ -98,18 +66,3 @@ func (c *memCache) Delete(ctx context.Context, key string) error {
func (m *memCache) String() string {
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,
}
}

View File

@ -4,7 +4,7 @@ import "time"
// Item represents an item stored in the cache.
type Item struct {
Value any
Value interface{}
Expiration int64
}
@ -16,3 +16,18 @@ func (i *Item) Expired() bool {
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,
}
}

View File

@ -1,70 +0,0 @@
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
}

View File

@ -1,46 +0,0 @@
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
}

View File

@ -6,7 +6,6 @@ const (
Invalid = 1001 //payload invalid
Exists = 1002 //already exists
Unavailable = 1003 //service unavailable
Incompatible = 1004 //type incompatible
Timeout = 2001 //timeout
Expired = 2002 //expired
TokenExpired = 4002 //token expired
@ -30,5 +29,4 @@ var (
ErrUnavailable = New(Unavailable, "service unavailable")
ErrNetworkUnreachable = New(NetworkUnreachable, "network unreachable")
ErrConnectionRefused = New(ConnectionRefused, "connection refused")
ErrIncompatible = New(Incompatible, "incompatible")
)

View File

@ -14,14 +14,6 @@ func (e *Error) Error() string {
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 {
return &Error{
Code: code,

View File

@ -1,27 +0,0 @@
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)
}

View File

@ -1,174 +0,0 @@
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
}

View File

@ -1,28 +0,0 @@
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
}

View File

@ -1,162 +0,0 @@
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
}

View File

@ -1,235 +0,0 @@
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
}

View File

@ -1,71 +0,0 @@
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,
}
}

View File

@ -31,16 +31,6 @@ type RestFieldOptions struct {
Format string `protobuf:"bytes,5,opt,name=format,proto3" json:"format,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"`
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
sizeCache protoimpl.SizeCache
}
@ -124,76 +114,6 @@ func (x *RestFieldOptions) GetRule() string {
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 {
state protoimpl.MessageState `protogen:"open.v1"`
Table string `protobuf:"bytes,1,opt,name=table,proto3" json:"table,omitempty"`
@ -278,7 +198,7 @@ var File_rest_proto protoreflect.FileDescriptor
const file_rest_proto_rawDesc = "" +
"\n" +
"\n" +
"rest.proto\x12\x04aeus\x1a google/protobuf/descriptor.proto\"\xc6\x03\n" +
"rest.proto\x12\x04aeus\x1a google/protobuf/descriptor.proto\"\xbc\x01\n" +
"\x10RestFieldOptions\x12\x12\n" +
"\x04gorm\x18\x01 \x01(\tR\x04gorm\x12\x18\n" +
"\acomment\x18\x02 \x01(\tR\acomment\x12\x1c\n" +
@ -286,18 +206,7 @@ const file_rest_proto_rawDesc = "" +
"\bposition\x18\x04 \x01(\tR\bposition\x12\x16\n" +
"\x06format\x18\x05 \x01(\tR\x06format\x12\x14\n" +
"\x05props\x18\x06 \x01(\tR\x05props\x12\x12\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" +
"\x04rule\x18\a \x01(\tR\x04rule\"*\n" +
"\x12RestMessageOptions\x12\x14\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" +

View File

@ -21,16 +21,6 @@ message RestFieldOptions {
string format = 5;
string props = 6;
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 {

View File

@ -0,0 +1,122 @@
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
}

View File

@ -0,0 +1,271 @@
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/
`
)

View File

@ -0,0 +1,16 @@
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
}

69
tools/gen/main.go 100644
View File

@ -0,0 +1,69 @@
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()
}

View File

@ -6,9 +6,7 @@ import (
"math"
"net"
"net/url"
"os"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
@ -245,8 +243,6 @@ func New(cbs ...Option) *Server {
uri: &url.URL{Scheme: "cli"},
router: newRouter(""),
}
port, _ := strconv.Atoi(os.Getenv("CLI_PORT"))
srv.opts.address = fmt.Sprintf(":%d", port)
for _, cb := range cbs {
cb(srv.opts)
}

View File

@ -2,11 +2,8 @@ package grpc
import (
"context"
"fmt"
"net"
"net/url"
"os"
"strconv"
"git.nobla.cn/golang/aeus/metadata"
"git.nobla.cn/golang/aeus/middleware"
@ -139,6 +136,7 @@ func New(cbs ...Option) *Server {
opts: &options{
network: "tcp",
logger: logger.Default(),
address: ":0",
grpcOpts: make([]grpc.ServerOption, 0, 10),
},
uri: &url.URL{
@ -146,8 +144,6 @@ func New(cbs ...Option) *Server {
},
middlewares: make([]middleware.Middleware, 0, 10),
}
port, _ := strconv.Atoi(os.Getenv("GRPC_PORT"))
svr.opts.address = fmt.Sprintf(":%d", port)
for _, cb := range cbs {
cb(svr.opts)
}

View File

@ -22,10 +22,6 @@ func (c *Context) Context() context.Context {
return c.ctx.Request.Context()
}
func (c *Context) Gin() *gin.Context {
return c.ctx
}
func (c *Context) Request() *http.Request {
return c.ctx.Request
}
@ -42,14 +38,6 @@ func (c *Context) Param(key string) string {
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) {
// if params exists, try bind params first
if len(c.ctx.Params) > 0 {

View File

@ -2,18 +2,11 @@ package http
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/http/pprof"
"net/url"
"os"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"time"
@ -110,99 +103,23 @@ func (s *Server) Webroot(prefix string, fs http.FileSystem) {
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) {
if s.fs != nil && ctx.Request.Method == http.MethodGet {
uri := path.Clean(ctx.Request.URL.Path)
if fp, err := s.fs.Open(uri); err == nil {
s.staticHandle(ctx, fp)
http.ServeContent(ctx.Writer, ctx.Request, path.Base(uri), s.fs.modtime, fp)
fp.Close()
ctx.Abort()
return
}
}
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 {
return func(ginCtx *gin.Context) {
ctx := ginCtx.Request.Context()
next := func(ctx context.Context) error {
ginCtx.Request = ginCtx.Request.WithContext(ctx)
ginCtx.Next()
if err := ginCtx.Errors.Last(); err != nil {
return err.Err
@ -222,8 +139,8 @@ func (s *Server) requestInterceptor() gin.HandlerFunc {
}
md.Set(metadata.RequestProtocolKey, Protocol)
md.Set(metadata.RequestPathKey, ginCtx.Request.URL.Path)
md.Set("method", ginCtx.Request.Method)
ctx = metadata.NewContext(ctx, md)
ginCtx.Request = ginCtx.Request.WithContext(ctx)
if err := handler(ctx); err != nil {
if se, ok := err.(*errors.Error); ok {
ginCtx.AbortWithStatusJSON(http.StatusInternalServerError, newResponse(se.Code, se.Message, nil))
@ -296,10 +213,9 @@ func New(cbs ...Option) *Server {
opts: &options{
network: "tcp",
logger: logger.Default(),
address: ":0",
},
}
port, _ := strconv.Atoi(os.Getenv("HTTP_PORT"))
svr.opts.address = fmt.Sprintf(":%d", port)
for _, cb := range cbs {
cb(svr.opts)
}
@ -307,9 +223,6 @@ func New(cbs ...Option) *Server {
gin.SetMode(gin.ReleaseMode)
}
svr.engine = gin.New(svr.opts.ginOptions...)
if svr.opts.enableCORS {
svr.engine.Use(svr.CORSInterceptor())
}
svr.engine.Use(svr.requestInterceptor())
return svr
}

View File

@ -1,14 +1,8 @@
package http
import (
"bufio"
"compress/gzip"
"context"
"errors"
"io"
"net"
"net/http"
"sync"
"git.nobla.cn/golang/aeus/pkg/logger"
"github.com/gin-gonic/gin"
@ -31,7 +25,6 @@ type (
logger logger.Logger
context context.Context
ginOptions []gin.OptionFunc
enableCORS bool
}
HandleFunc func(ctx *Context) (err error)
@ -47,89 +40,12 @@ 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 {
return func(o *options) {
o.network = network
}
}
func WithCORS() Option {
return func(o *options) {
o.enableCORS = true
}
}
func WithAddress(address string) Option {
return func(o *options) {
o.address = address