Compare commits

..

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

33 changed files with 570 additions and 1473 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 | 前端资源目录 |

28
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 ""
return s.opts.version
}
func (s *Service) Metadata() map[string]string {
if s.service == nil {
return nil
@ -109,14 +101,10 @@ func (s *Service) injectVars(v any) {
continue
}
fieldType := refType.Field(i)
if !(fieldType.Type.Kind() != reflect.Ptr || fieldType.Type.Kind() != reflect.Interface) {
if fieldType.Type.Kind() != reflect.Ptr {
continue
}
for _, rv := range s.refValues {
if fieldType.Type.Kind() == reflect.Interface && rv.Type().Implements(fieldType.Type) {
refValue.Field(i).Set(rv)
break
}
if fieldType.Type == rv.Type() {
refValue.Field(i).Set(rv)
break
@ -126,11 +114,8 @@ func (s *Service) injectVars(v any) {
}
func (s *Service) preStart(ctx context.Context) (err error) {
s.Logger().Info(ctx, "starting")
s.refValues = append(s.refValues, s.opts.injectVars...)
s.refValues = append(s.refValues, reflect.ValueOf(s.Logger()))
s.Logger().Info(s.ctx, "starting")
for _, ptr := range s.opts.servers {
s.injectVars(ptr)
s.refValues = append(s.refValues, reflect.ValueOf(ptr))
}
if s.opts.registry != nil {
@ -192,7 +177,7 @@ func (s *Service) preStart(ctx context.Context) (err error) {
o.Context = ctx
o.TTL = s.opts.registrarTimeout
}); err != nil {
s.Logger().Warnf(ctx, "service register error: %v", err)
s.Logger().Warn(ctx, "service register error: %v", err)
}
}
}
@ -213,14 +198,14 @@ func (s *Service) preStop() (err error) {
}()
for _, srv := range s.opts.servers {
if err = srv.Stop(ctx); err != nil {
s.Logger().Warnf(ctx, "server stop error: %v", err)
s.Logger().Warn(ctx, "server stop error: %v", err)
}
}
if s.opts.registry != nil {
if err = s.opts.registry.Deregister(s.service, func(o *registry.DeregisterOptions) {
o.Context = ctx
}); err != nil {
s.Logger().Warnf(ctx, "server deregister error: %v", err)
s.Logger().Warn(ctx, "server deregister error: %v", err)
}
}
s.Logger().Info(ctx, "stopped")
@ -270,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
allows []string
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,8 +2,6 @@ package aeus
import (
"context"
"maps"
"reflect"
"time"
"git.nobla.cn/golang/aeus/pkg/logger"
@ -20,12 +18,10 @@ type options struct {
servers []Server
endpoints []string
scope Scope
debug bool
registrarTimeout time.Duration
registry registry.Registry
serviceLoader ServiceLoader
stopTimeout time.Duration
injectVars []reflect.Value
}
func WithName(name string) Option {
@ -45,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
}
}
}
@ -73,20 +71,6 @@ func WithScope(scope Scope) Option {
}
}
func WithDebug(debug bool) Option {
return func(o *options) {
o.debug = debug
}
}
func WithInjectVars(vars ...any) Option {
return func(o *options) {
for _, v := range vars {
o.injectVars = append(o.injectVars, reflect.ValueOf(v))
}
}
}
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

@ -11,34 +11,18 @@ type logger struct {
}
func (l *logger) Debug(ctx context.Context, msg string, args ...any) {
l.log.DebugContext(ctx, msg, args...)
}
func (l *logger) Debugf(ctx context.Context, msg string, args ...any) {
l.log.DebugContext(ctx, fmt.Sprintf(msg, args...))
}
func (l *logger) Info(ctx context.Context, msg string, args ...any) {
l.log.InfoContext(ctx, msg, args...)
}
func (l *logger) Infof(ctx context.Context, msg string, args ...any) {
l.log.InfoContext(ctx, fmt.Sprintf(msg, args...))
}
func (l *logger) Warn(ctx context.Context, msg string, args ...any) {
l.log.WarnContext(ctx, msg, args...)
}
func (l *logger) Warnf(ctx context.Context, msg string, args ...any) {
l.log.WarnContext(ctx, fmt.Sprintf(msg, args...))
}
func (l *logger) Error(ctx context.Context, msg string, args ...any) {
l.log.ErrorContext(ctx, msg, args...)
}
func (l *logger) Errorf(ctx context.Context, msg string, args ...any) {
l.log.ErrorContext(ctx, fmt.Sprintf(msg, args...))
}

View File

@ -3,7 +3,6 @@ package logger
import (
"context"
"log/slog"
"os"
)
var (
@ -11,56 +10,32 @@ var (
)
func init() {
log = NewLogger(slog.New(
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}),
))
log = NewLogger(slog.Default())
}
type Logger interface {
Debug(ctx context.Context, format string, args ...any) //Structured context as loosely typed key-value pairs.
Debugf(ctx context.Context, format string, args ...any)
Debug(ctx context.Context, format string, args ...any)
Info(ctx context.Context, format string, args ...any)
Infof(ctx context.Context, format string, args ...any)
Warn(ctx context.Context, format string, args ...any)
Warnf(ctx context.Context, format string, args ...any)
Error(ctx context.Context, format string, args ...any)
Errorf(ctx context.Context, format string, args ...any)
}
func Debug(ctx context.Context, format string, args ...any) {
log.Debug(ctx, format, args...)
}
func Debugf(ctx context.Context, format string, args ...any) {
log.Debugf(ctx, format, args...)
}
func Info(ctx context.Context, format string, args ...any) {
log.Info(ctx, format, args...)
}
func Infof(ctx context.Context, format string, args ...any) {
log.Infof(ctx, format, args...)
log.Debug(ctx, format, args...)
}
func Warn(ctx context.Context, format string, args ...any) {
log.Warn(ctx, format, args...)
}
func Warnf(ctx context.Context, format string, args ...any) {
log.Warnf(ctx, format, args...)
log.Debug(ctx, format, args...)
}
func Error(ctx context.Context, format string, args ...any) {
log.Debug(ctx, format, args...)
}
func Errorf(ctx context.Context, format string, args ...any) {
log.Errorf(ctx, format, args...)
}
func Default() Logger {
return log
}

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"
@ -32,7 +30,6 @@ type Server struct {
uri *url.URL
exitFlag int32
middleware []middleware.Middleware
Logger logger.Logger
}
func (svr *Server) Use(middlewares ...middleware.Middleware) {
@ -135,7 +132,7 @@ func (s *Server) execute(ctx *Context, frame *Frame) (err error) {
func (svr *Server) nextSequence() int64 {
svr.sequenceLocker.Lock()
defer svr.sequenceLocker.Unlock()
if svr.sequence == math.MaxInt64 {
if svr.sequence >= math.MaxInt64 {
svr.sequence = 1
}
svr.sequence++
@ -209,16 +206,10 @@ func (s *Server) serve() (err error) {
func (s *Server) Start(ctx context.Context) (err error) {
s.ctx = ctx
if s.opts.logger != nil {
s.Logger = s.opts.logger
}
if s.Logger == nil {
s.Logger = logger.Default()
}
if err = s.createListener(); err != nil {
return
}
s.Logger.Infof(ctx, "cli server listen on: %s", s.uri.Host)
s.opts.logger.Info(ctx, "cli server listen on: %s", s.uri.Host)
s.Handle("/help", "Display help information", func(ctx *Context) (err error) {
return ctx.Success(s.router.String())
})
@ -231,9 +222,7 @@ func (s *Server) Stop(ctx context.Context) (err error) {
return
}
if s.listener != nil {
if err = s.listener.Close(); err != nil {
s.Logger.Warnf(ctx, "cli listener close error: %v", err)
}
err = s.listener.Close()
}
s.ctxMap.Range(func(key, value any) bool {
if ctx, ok := value.(*Context); ok {
@ -241,7 +230,6 @@ func (s *Server) Stop(ctx context.Context) (err error) {
}
return true
})
s.Logger.Info(ctx, "cli server stopped")
return
}
@ -250,12 +238,11 @@ func New(cbs ...Option) *Server {
opts: &options{
network: "tcp",
address: ":0",
logger: logger.Default(),
},
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"
@ -25,7 +22,6 @@ type Server struct {
serve *grpc.Server
listener net.Listener
middlewares []middleware.Middleware
Logger logger.Logger
}
func (s *Server) createListener() (err error) {
@ -108,16 +104,11 @@ func (s *Server) Use(middlewares ...middleware.Middleware) {
func (s *Server) Start(ctx context.Context) (err error) {
s.ctx = ctx
if s.opts.logger != nil {
s.Logger = s.opts.logger
}
if s.Logger == nil {
s.Logger = logger.Default()
}
if err = s.createListener(); err != nil {
return
}
s.Logger.Infof(ctx, "grpc server listen on: %s", s.uri.Host)
s.opts.logger.Info(ctx, "grpc server listen on: %s", s.uri.Host)
reflection.Register(s.serve)
s.serve.Serve(s.listener)
return
@ -136,7 +127,7 @@ func (s *Server) RegisterService(sd *grpc.ServiceDesc, ss any) {
func (s *Server) Stop(ctx context.Context) (err error) {
s.serve.GracefulStop()
s.Logger.Infof(s.ctx, "grpc server stopped")
s.opts.logger.Info(s.ctx, "grpc server stopped")
return
}
@ -144,6 +135,8 @@ func New(cbs ...Option) *Server {
svr := &Server{
opts: &options{
network: "tcp",
logger: logger.Default(),
address: ":0",
grpcOpts: make([]grpc.ServerOption, 0, 10),
},
uri: &url.URL{
@ -151,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"
@ -36,7 +29,6 @@ type Server struct {
listener net.Listener
fs *filesystem
middlewares []middleware.Middleware
Logger logger.Logger
}
func (s *Server) Endpoint(ctx context.Context) (string, error) {
@ -111,98 +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()
}
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))
@ -255,12 +172,6 @@ func (s *Server) Start(ctx context.Context) (err error) {
Addr: s.opts.address,
Handler: s.engine,
}
if s.opts.logger != nil {
s.Logger = s.opts.logger
}
if s.Logger == nil {
s.Logger = logger.Default()
}
s.ctx = ctx
if s.opts.debug {
s.engine.Handle(http.MethodGet, "/debug/pprof/", s.wrapHandle(pprof.Index))
@ -277,7 +188,7 @@ func (s *Server) Start(ctx context.Context) (err error) {
return
}
s.engine.NoRoute(s.notFoundHandle)
s.Logger.Infof(ctx, "http server listen on: %s", s.uri.Host)
s.opts.logger.Info(ctx, "http server listen on: %s", s.uri.Host)
if s.opts.certFile != "" && s.opts.keyFile != "" {
s.uri.Scheme = "https"
err = s.serve.ServeTLS(s.listener, s.opts.certFile, s.opts.keyFile)
@ -292,7 +203,7 @@ func (s *Server) Start(ctx context.Context) (err error) {
func (s *Server) Stop(ctx context.Context) (err error) {
err = s.serve.Shutdown(ctx)
s.Logger.Infof(ctx, "http server stopped")
s.opts.logger.Info(ctx, "http server stopped")
return
}
@ -301,10 +212,10 @@ func New(cbs ...Option) *Server {
uri: &url.URL{Scheme: "http"},
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)
}
@ -312,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