1. Split the converter into a request converter and a response converter.

2. Added a protobuf converter.
3. Removed all sub-`go.mod` files and switched to a `go package` modular structure.
4. Migrated the module prefix from `github.com` to `go.drunkce.com`.
This commit is contained in:
Drunk
2025-03-17 18:18:10 +08:00
parent 743d1e2619
commit 56c024fc6b
60 changed files with 1179 additions and 474 deletions

View File

@@ -1,28 +1,259 @@
中文 | [English](README.md)
DCE-GO是一个通用路由库除了能路由http协议外还能路由cli、websocket、tcp/udp等非标准可路由协议的协议。
---
DCE-GO按功能类型不同,划分了[路由器](router)、[可路由协议](proto)、[转换器](converter)、[会话管理器](session)及[工具](util)等模块。
**DCE-GO** 是一个功能强大的通用路由库,不仅支持 HTTP 协议,还能路由 CLI、WebSocket、TCP/UDP 等非标准协议。它采用模块化设计,按功能划分为以下核心模块:
路由器模块下定义了api、上下文及路由器库以及转换器、可路由协议等接口它是DCE的核心模块。
1. **路由器模块**
作为 DCE 的核心模块,定义了 API、上下文及路由器库同时提供了转换器、可路由协议等接口确保灵活性和扩展性。
可路由协议模块实现了一些常见协议的可路由协议封装包括http、cli、websocket、tcp、udp、quic等。
2. **可路由协议模块**
封装了多种常见协议的可路由实现,包括 HTTP、CLI、WebSocket、TCP、UDP、QUIC 等,满足多样化场景需求。
转换器内置实现了json与template转换器用于序列化及反序列化串行数据、双向转换传输对象与实体对象等。
3. **转换器模块**
内置 JSON 和模板转换器,支持串行数据的序列化与反序列化,以及传输对象与实体对象的双向转换。
会话管理器模块定义了基础会话、用户会话、连接会话及自重生会话接口并内置了Redis、共享内存版这些接口的实现类库。
4. **会话管理器模块**
定义了基础会话、用户会话、连接会话及自重生会话接口,并提供了 Redis 和共享内存的实现类库,方便开发者快速集成。
所有功能特性,都有相应用例,位于[_examples](_examples)目录下。它的路由性能非常高与gin相当可在[这里](_examples/attachs/report/ab-test-result.txt)查看ab测试报告端口`2046`的为DCE结果。
5. **工具模块**
提供了一系列实用工具,简化开发流程。
DCE-GO源自于[DCE-RUST](https://github.com/idrunk/dce-rust),它们都源自于[DCE-PHP](https://github.com/idrunk/dce-php)。DCE-PHP是一个完整的网络编程框架已停止更新它的核心路由模块已升级并迁移到DCE-RUST与DCE-GO中目前DCE-GO功能版本较新后续会同步两个语言的功能版本
DCE-GO 的所有功能特性均配有详细用例,位于 [_examples](_examples) 目录下。其路由性能与 Gin 相当,具体性能测试报告可查看 [ab 测试结果](_examples/attachs/report/ab-test-result.txt),其中端口 `2046` 为 DCE 的测试结果
DCE致力于成为一个高效、开放、安全的通用路由库,欢迎你来贡献,使其更加高效、开放、安全
DCE-GO 源自 [DCE-RUST](https://github.com/idrunk/dce-rust),而两者均基于 [DCE-PHP](https://github.com/idrunk/dce-php) 的核心路由模块升级而来。DCE-PHP 是一个完整的网络编程框架,现已停止更新,其核心功能已迁移至 DCE-RUST 和 DCE-GO。目前DCE-GO 的功能版本较新,未来 DCE-RUST 将与之同步
TODO
- 优化JS版Websocket可路由协议客户端完善各协议通信GOLANG版客户端
- 升级控制器前后置事件接口,可与程序接口绑定
- 完善数字路径支持
- 尝试调整弹性数字函数,改为结构方法式
- 研究可路由协议中支持自定义业务属性的可能性
- 同步DCE-RUST
- 逐步替换AI生成的文档为人工文档
DCE 致力于打造一个高效、开放、安全的通用路由库,欢迎社区贡献,共同推动其发展。
---
**TODO**
- [ ] 优化 JS 版 WebSocket 可路由协议客户端。
- [ ] 升级控制器前后置事件接口,支持与程序接口绑定。
- [ ] 完善数字路径支持。
- [ ] 调整弹性数字函数为结构方法式。
- [ ] 研究可路由协议中支持自定义业务属性的可能性。
- [ ] 支持文件上传等超大数据包的可路由协议。
- [ ] 升级 DCE-RUST 功能版本。
- [ ] 完善各协议的 Golang 客户端实现。
- [ ] 逐步替换 AI 生成的文档为人工编写文档。
---
**使用示例**
```golang
package main
import (
"bufio"
"encoding/json"
"fmt"
"log/slog"
"net"
"os"
"slices"
"strings"
"go.drunkce.com/dce/converter"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/proto/flex"
"go.drunkce.com/dce/router"
"go.drunkce.com/dce/session"
"go.drunkce.com/dce/util"
)
func main() {
// go run main.go tcp start
proto.CliRouter.Push("tcp/start/{address?}", func(c *proto.Cli) {
bindServer()
addr := c.ParamOr("address", ":2048")
listener, err := net.Listen("tcp", addr)
if err != nil {
panic(err.Error())
}
defer listener.Close()
fmt.Printf("tcp server start at %s\n", addr)
for {
conn, err := listener.Accept()
if err != nil {
slog.Warn(fmt.Sprintf("accept error: %s", err))
continue
}
go func(conn net.Conn) {
defer conn.Close()
// Connection sessions are used to store the connection information for sending message across hosts to clients in a distributed environment.
shadow, err := session.NewShmSession[*Member](nil, session.DefaultTtlMinutes)
if err != nil {
slog.Warn(fmt.Sprintf("new session error: %s", err))
return
}
shadow.Connect(conn.LocalAddr().String(), conn.RemoteAddr().String())
defer shadow.Disconnect()
for {
if !flex.TcpRouter.Route(conn, map[string]any{"$shadowSession": shadow}) {
break
}
}
}(conn)
}
})
bindClient()
proto.CliRoute(1)
}
func bindServer() {
flex.TcpRouter.Push("sign", func(c *flex.Tcp) {
signInfo, ok := converter.JsonRawRequester[*flex.TcpProtocol, *Member](c).Parse()
jsr := converter.JsonStatusResponser(c)
if !ok {
return
}
if (len(signInfo.Name) == 0 || len(signInfo.Password) == 0) && jsr.Fail("name or password is empty", 0) {
return
}
member, ok := members[signInfo.Name]
if !ok {
// Notfound then auto register
memberId++
member = signInfo
member.Id = memberId
member.Role = 1
members[member.Name] = member
}
if member.Password != signInfo.Password && jsr.Fail("password error", 0) {
return
}
// Must be have a session obj after `BeforeController` event, so we no need to check nil
se := c.Rp.Session().(*session.ShmSession[*Member])
if err := se.Login(member, 0); err != nil && jsr.Fail(err.Error(), 0) {
return
}
// Must be have a new session id after `UserSession.Login()`
c.Rp.SetRespSid(se.Id())
jsr.Success(nil)
})
// Bind an api with Path: signer, roles: [1]
flex.TcpRouter.PushApi(router.Path("signer").Append("roles", 1), func(c *flex.Tcp) {
jc := converter.JsonResponser[*flex.TcpProtocol, *Member, *Signer](c)
sess := c.Rp.Session().(*session.ShmSession[*Member])
// Member info can be obtained here, so there is no need to check
member, _ := sess.User()
// Response the member, it can be convert to Signer struct automatically
jc.Response(member)
})
flex.TcpRouter.SetEventHandler(func(c *flex.Tcp) error {
shadow, _ := c.Rp.CtxData("$shadowSession")
rs := shadow.(*session.ShmSession[*Member])
cloned, err := rs.CloneForRequest(c.Rp.Sid())
if err != nil {
return err
}
se := cloned.(*session.ShmSession[*Member])
if roles := util.MapSeqFrom[any, uint16](c.Api.ExtrasBy("roles")).Map(func(i any) uint16 {
return uint16(i.(int))
}).Collect(); len(roles) > 0 {
// Roles configured means need to login
if member, ok := se.User(); !ok {
return util.Openly(401, "need to login")
} else if !slices.Contains(roles, member.Role) {
return util.Openly(403, "no permission")
} else if newer, err := session.NewAutoRenew(se).TryRenew(); err != nil {
return err
} else if newer {
// Logged session need to auto renew to enhance security
c.Rp.SetRespSid(se.Id())
}
}
c.Rp.SetSession(se)
return nil
}, nil)
}
func bindClient() {
// go run main.go sign
proto.CliRouter.Push("sign", func(c *proto.Cli) {
reader := bufio.NewReader(os.Stdin)
signInfo := Member{}
fmt.Print("Enter username: ")
username, _ := reader.ReadString('\n')
signInfo.Name = strings.TrimSpace(username)
fmt.Print("Enter password: ")
password, _ := reader.ReadString('\n')
signInfo.Password = strings.TrimSpace(password)
reqBody, err := json.Marshal(signInfo)
if err != nil && c.SetError(err) {
return
} else if resp := request(c, "sign", reqBody, ""); resp != nil {
c.Rp.SetRespSid(resp.Sid)
c.WriteString("Signed in successfully")
}
})
// go run main.go signer $SESSION_ID
proto.CliRouter.Push("signer/{sid?}", func(c *proto.Cli) {
sid := c.Param("sid")
if len(sid) == 0 {
panic("Session ID is required")
}
if resp := request(c, "signer", nil, sid); resp == nil {
c.SetError(util.Closed0("Request failed"))
} else if resp.Code == 0 {
var signer Signer
if err := json.Unmarshal(resp.Body, &signer); err != nil && c.SetError(err) {
return
} else {
// Just response the signer info if the session is logged in
c.WriteString(fmt.Sprintf("Signer: %v", signer))
}
} else {
c.SetError(util.Openly(int(resp.Code), resp.Message))
}
})
}
// It's a simple example, need to mapping request id and the response callback if the server is async
func request(c *proto.Cli, path string, reqBody []byte, sid string) *flex.Package {
pkg := flex.NewPackage(path, reqBody, sid, -1)
conn, _ := net.Dial("tcp", "127.0.0.1:"+c.Rp.ArgOr("port", "2048"))
defer conn.Close()
if _, err := conn.Write(pkg.Serialize()); err != nil && c.SetError(err) {
return nil
}
resp, err := flex.PackageDeserialize(bufio.NewReader(conn))
if err != nil && c.SetError(err) {
return nil
}
return resp
}
var memberId uint64 = 0
var members map[string]*Member = make(map[string]*Member)
type Member struct {
Id uint64
Role uint16
Name string `json:"name"`
Password string `json:"password"`
}
func (m Member) Uid() uint64 {
return m.Id
}
type Signer struct {
Name string `json:"name"`
}
// Member entity converted to transfer object desensitization
func (m *Signer) From(member *Member) (*Signer, error) {
m = util.NewStruct[*Signer]()
m.Name = member.Name
return m, nil
}
```

265
README.md
View File

@@ -1,28 +1,259 @@
[中文](README-zh.md) | English
DCE-GO is a universal routing library that can route not only HTTP protocols but also non-standard routable protocols such as CLI, WebSocket, TCP/UDP, and more.
---
DCE-GO is divided into several modules based on functional types, including [Router](router), [Routable Protocols](proto), [Converters](converter), [Session Managers](session), and [Utilities](util).
**DCE-GO** is a powerful universal routing library that not only supports the HTTP protocol but also routes non-standard protocols such as CLI, WebSocket, TCP/UDP, and more. It adopts a modular design, divided into the following core modules based on functionality:
The Router module defines APIs, contexts, and router libraries, as well as interfaces for converters and routable protocols. It is the core module of DCE.
1. **Router Module**
As the core module of DCE, it defines APIs, contexts, and the router library, while providing interfaces for converters and routable protocols, ensuring flexibility and extensibility.
The Routable Protocols module implements routable protocol encapsulations for common protocols, including HTTP, CLI, WebSocket, TCP, UDP, QUIC, and more.
2. **Routable Protocol Module**
Encapsulates routable implementations of various common protocols, including HTTP, CLI, WebSocket, TCP, UDP, QUIC, etc., to meet diverse scenario requirements.
The Converter module includes built-in implementations of JSON and template converters, used for serializing and deserializing data, as well as bidirectional conversion between transport objects and entity objects.
3. **Converter Module**
Built-in JSON and template converters support serialization and deserialization of serial data, as well as bidirectional conversion between transport objects and entity objects.
The Session Manager module defines interfaces for basic sessions, user sessions, connection sessions, and self-regenerating sessions. It also includes built-in implementations of these interfaces using Redis and shared memory.
4. **Session Manager Module**
Defines interfaces for basic sessions, user sessions, connection sessions, and self-regenerating sessions, and provides implementation libraries for Redis and shared memory, facilitating rapid integration for developers.
All features come with corresponding examples, located in the [_examples](_examples) directory. Its routing performance is very high, comparable to Gin. You can view the ab test report [here](_examples/attachs/report/ab-test-result.txt). The result for port `2046` is from DCE.
5. **Utility Module**
Provides a series of practical tools to simplify the development process.
DCE-GO originates from [DCE-RUST](https://github.com/idrunk/dce-rust), and both originate from [DCE-PHP](https://github.com/idrunk/dce-php). DCE-PHP was a complete network programming framework that has been discontinued. Its core routing module has been upgraded and migrated to DCE-RUST and DCE-GO. Currently, DCE-GO has a more advanced feature set, and future updates will synchronize the features between the two languages.
All features of DCE-GO come with detailed usage examples, located in the [_examples](_examples) directory. Its routing performance is comparable to Gin, and specific performance test reports can be viewed in the [ab test results](_examples/attachs/report/ab-test-result.txt), where port `2046` represents DCE's test results.
DCE aims to be an efficient, open, and secure universal routing library. Contributions are welcome to make it even more efficient, open, and secure.
DCE-GO originates from [DCE-RUST](https://github.com/idrunk/dce-rust), and both are based on the core routing module of [DCE-PHP](https://github.com/idrunk/dce-php). DCE-PHP is a complete network programming framework that has ceased updates, with its core functionalities migrated to DCE-RUST and DCE-GO. Currently, DCE-GO has a newer feature version, and DCE-RUST will be synchronized with it in the future.
TODO:
- Optimize the JS version of the WebSocket routable protocol client and improve the communication clients for various protocols in the Golang version.
- Upgrade the pre- and post-event interfaces of the controller to bind with program interfaces.
- Improve support for numeric paths.
- Attempt to refactor the elastic numeric function into a structural method style.
- Explore the possibility of supporting custom business attributes in routable protocols.
- Synchronize to DCE-RUST.
- Gradually replace AI-generated documentation with human-written documentation.
DCE is committed to building an efficient, open, and secure universal routing library, and welcomes community contributions to drive its development.
---
**TODO**:
- [ ] Optimize the JS version of the WebSocket routable protocol client.
- [ ] Upgrade the controller pre- and post-event interfaces to support binding with program interfaces.
- [ ] Enhance support for digital paths.
- [ ] Refactor elastic numeric functions into structural method styles.
- [ ] Investigate the possibility of supporting custom business attributes in routable protocols.
- [ ] Routable protocol support very large data packets such as file uploads.
- [ ] Upgrade the feature version of DCE-RUST.
- [ ] Improve the Golang client implementations for various protocols.
- [ ] Gradually replace AI-generated documentation with manually written documentation.
---
**Usage Example**
```golang
package main
import (
"bufio"
"encoding/json"
"fmt"
"log/slog"
"net"
"os"
"slices"
"strings"
"go.drunkce.com/dce/converter"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/proto/flex"
"go.drunkce.com/dce/router"
"go.drunkce.com/dce/session"
"go.drunkce.com/dce/util"
)
func main() {
// go run main.go tcp start
proto.CliRouter.Push("tcp/start/{address?}", func(c *proto.Cli) {
bindServer()
addr := c.ParamOr("address", ":2048")
listener, err := net.Listen("tcp", addr)
if err != nil {
panic(err.Error())
}
defer listener.Close()
fmt.Printf("tcp server start at %s\n", addr)
for {
conn, err := listener.Accept()
if err != nil {
slog.Warn(fmt.Sprintf("accept error: %s", err))
continue
}
go func(conn net.Conn) {
defer conn.Close()
// Connection sessions are used to store the connection information for sending message across hosts to clients in a distributed environment.
shadow, err := session.NewShmSession[*Member](nil, session.DefaultTtlMinutes)
if err != nil {
slog.Warn(fmt.Sprintf("new session error: %s", err))
return
}
shadow.Connect(conn.LocalAddr().String(), conn.RemoteAddr().String())
defer shadow.Disconnect()
for {
if !flex.TcpRouter.Route(conn, map[string]any{"$shadowSession": shadow}) {
break
}
}
}(conn)
}
})
bindClient()
proto.CliRoute(1)
}
func bindServer() {
flex.TcpRouter.Push("sign", func(c *flex.Tcp) {
signInfo, ok := converter.JsonRawRequester[*flex.TcpProtocol, *Member](c).Parse()
jsr := converter.JsonStatusResponser(c)
if !ok {
return
}
if (len(signInfo.Name) == 0 || len(signInfo.Password) == 0) && jsr.Fail("name or password is empty", 0) {
return
}
member, ok := members[signInfo.Name]
if !ok {
// Notfound then auto register
memberId++
member = signInfo
member.Id = memberId
member.Role = 1
members[member.Name] = member
}
if member.Password != signInfo.Password && jsr.Fail("password error", 0) {
return
}
// Must be have a session obj after `BeforeController` event, so we no need to check nil
se := c.Rp.Session().(*session.ShmSession[*Member])
if err := se.Login(member, 0); err != nil && jsr.Fail(err.Error(), 0) {
return
}
// Must be have a new session id after `UserSession.Login()`
c.Rp.SetRespSid(se.Id())
jc.Success(nil)
})
// Bind an api with Path: signer, roles: [1]
flex.TcpRouter.PushApi(router.Path("signer").Append("roles", 1), func(c *flex.Tcp) {
jc := converter.JsonResponser[*flex.TcpProtocol, *Member, *Signer](c)
sess := c.Rp.Session().(*session.ShmSession[*Member])
// Member info can be obtained here, so there is no need to check
member, _ := sess.User()
// Response the member, it can be convert to Signer struct automatically
jc.Response(member)
})
flex.TcpRouter.SetEventHandler(func(c *flex.Tcp) error {
shadow, _ := c.Rp.CtxData("$shadowSession")
rs := shadow.(*session.ShmSession[*Member])
cloned, err := rs.CloneForRequest(c.Rp.Sid())
if err != nil {
return err
}
se := cloned.(*session.ShmSession[*Member])
if roles := util.MapSeqFrom[any, uint16](c.Api.ExtrasBy("roles")).Map(func(i any) uint16 {
return uint16(i.(int))
}).Collect(); len(roles) > 0 {
// Roles configured means need to login
if member, ok := se.User(); !ok {
return util.Openly(401, "need to login")
} else if !slices.Contains(roles, member.Role) {
return util.Openly(403, "no permission")
} else if newer, err := session.NewAutoRenew(se).TryRenew(); err != nil {
return err
} else if newer {
// Logged session need to auto renew to enhance security
c.Rp.SetRespSid(se.Id())
}
}
c.Rp.SetSession(se)
return nil
}, nil)
}
func bindClient() {
// go run main.go sign
proto.CliRouter.Push("sign", func(c *proto.Cli) {
reader := bufio.NewReader(os.Stdin)
signInfo := Member{}
fmt.Print("Enter username: ")
username, _ := reader.ReadString('\n')
signInfo.Name = strings.TrimSpace(username)
fmt.Print("Enter password: ")
password, _ := reader.ReadString('\n')
signInfo.Password = strings.TrimSpace(password)
reqBody, err := json.Marshal(signInfo)
if err != nil && c.SetError(err) {
return
} else if resp := request(c, "sign", reqBody, ""); resp != nil {
c.Rp.SetRespSid(resp.Sid)
c.WriteString("Signed in successfully")
}
})
// go run main.go signer $SESSION_ID
proto.CliRouter.Push("signer/{sid?}", func(c *proto.Cli) {
sid := c.Param("sid")
if len(sid) == 0 {
panic("Session ID is required")
}
if resp := request(c, "signer", nil, sid); resp == nil {
c.SetError(util.Closed0("Request failed"))
} else if resp.Code == 0 {
var signer Signer
if err := json.Unmarshal(resp.Body, &signer); err != nil && c.SetError(err) {
return
} else {
// Just response the signer info if the session is logged in
c.WriteString(fmt.Sprintf("Signer: %v", signer))
}
} else {
c.SetError(util.Openly(int(resp.Code), resp.Message))
}
})
}
// It's a simple example, need to mapping request id and the response callback if the server is async
func request(c *proto.Cli, path string, reqBody []byte, sid string) *flex.Package {
pkg := flex.NewPackage(path, reqBody, sid, -1)
conn, _ := net.Dial("tcp", "127.0.0.1:"+c.Rp.ArgOr("port", "2048"))
defer conn.Close()
if _, err := conn.Write(pkg.Serialize()); err != nil && c.SetError(err) {
return nil
}
resp, err := flex.PackageDeserialize(bufio.NewReader(conn))
if err != nil && c.SetError(err) {
return nil
}
return resp
}
var memberId uint64 = 0
var members map[string]*Member = make(map[string]*Member)
type Member struct {
Id uint64
Role uint16
Name string `json:"name"`
Password string `json:"password"`
}
func (m Member) Uid() uint64 {
return m.Id
}
type Signer struct {
Name string `json:"name"`
}
// Member entity converted to transfer object desensitization
func (m *Signer) From(member *Member) (*Signer, error) {
m = util.NewStruct[*Signer]()
m.Name = member.Name
return m, nil
}
```

View File

@@ -3,8 +3,8 @@ package apis
import (
"fmt"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/router"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/router"
)
func BindCli() {

View File

@@ -7,16 +7,17 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/proto/flex"
"github.com/idrunk/dce-go/router"
"github.com/quic-go/quic-go"
"log/slog"
"math/rand/v2"
"os"
"strconv"
"strings"
"time"
"github.com/quic-go/quic-go"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/proto/flex"
"go.drunkce.com/dce/router"
)
const alpn = "dce-quic-example"

View File

@@ -4,9 +4,6 @@ import (
"bufio"
"crypto/sha256"
"fmt"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/proto/flex"
"github.com/idrunk/dce-go/router"
"log"
"log/slog"
"math/rand/v2"
@@ -14,6 +11,10 @@ import (
"os"
"strconv"
"strings"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/proto/flex"
"go.drunkce.com/dce/router"
)
func FlexTcpStart(c *proto.Cli) {

View File

@@ -4,15 +4,16 @@ import (
"bufio"
"crypto/sha256"
"fmt"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/proto/flex"
"github.com/idrunk/dce-go/router"
"log"
"log/slog"
"math/rand/v2"
"net"
"strconv"
"strings"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/proto/flex"
"go.drunkce.com/dce/router"
)
func FlexUdpStart(c *proto.Cli) {

View File

@@ -5,12 +5,6 @@ import (
"crypto/sha256"
"encoding/json"
"fmt"
"github.com/coder/websocket"
"github.com/idrunk/dce-go/converter"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/proto/flex"
"github.com/idrunk/dce-go/session"
"github.com/idrunk/dce-go/util"
"log"
"log/slog"
"math/rand/v2"
@@ -18,9 +12,17 @@ import (
"os"
"slices"
"time"
"github.com/coder/websocket"
"go.drunkce.com/dce/converter"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/proto/flex"
"go.drunkce.com/dce/session"
"go.drunkce.com/dce/util"
)
func init() {
// go run . websocket start
proto.CliRouter.Push("websocket/start", FlexWebsocketStart)
}
@@ -36,8 +38,8 @@ func flexWebsocketBind(port string) {
converter.TplConfig.SetRoot(wd + "/apis/")
proto.HttpRouter.Get("", func(h *proto.Http) {
t := converter.FileTemplate[*proto.HttpProtocol, struct{ ServerAddr string }](h, "flex-websocket.html")
t.Response(struct{ ServerAddr string }{
t := converter.FileTemplate[*proto.HttpProtocol, *struct{ ServerAddr string }](h, "flex-websocket.html")
t.Response(&struct{ ServerAddr string }{
ServerAddr: "ws://127.0.0.1:" + port + "/ws",
})
})
@@ -48,7 +50,7 @@ func flexWebsocketBind(port string) {
slog.Warn(err.Error())
return
}
shadowSess, err := session.NewShmSession[*session.SimpleUser]([]string{h.Param("sid")}, session.DefaultTtlMinutes)
shadowSess, _ := session.NewShmSession[*session.SimpleUser]([]string{h.Param("sid")}, session.DefaultTtlMinutes)
flex.WebsocketRouter.SetMapping(h.Rp.Req.RemoteAddr, c)
defer func() {
flex.WebsocketRouter.Unmapping(h.Rp.Req.RemoteAddr)
@@ -56,7 +58,7 @@ func flexWebsocketBind(port string) {
go syncUserList(shadowSess, h, nil)
}()
if shadowSess != nil {
shadowSess.Connect(":2047", h.Rp.Req.RemoteAddr, true)
shadowSess.Connect(":2047", h.Rp.Req.RemoteAddr)
defer shadowSess.Disconnect()
if _, ok := shadowSess.User(); !ok {
// auto register and login
@@ -125,7 +127,7 @@ func flexWebsocketBind(port string) {
}
func syncUserList(sess *session.ShmSession[*session.SimpleUser], h *proto.Http, user *session.SimpleUser) {
var userList []session.SimpleUser
var userList []*session.SimpleUser
connList := make(map[string]*websocket.Conn)
for addr, uid := range flex.WebsocketRouter.UidMapping() {
if uses, _ := sess.ListByUid(uid); len(uses) > 0 {
@@ -133,16 +135,16 @@ func syncUserList(sess *session.ShmSession[*session.SimpleUser], h *proto.Http,
if u, o := us.User(); o {
conn, _ := flex.WebsocketRouter.ConnBy(addr)
connList[addr] = conn
if !slices.ContainsFunc(userList, func(ru session.SimpleUser) bool {
if !slices.ContainsFunc(userList, func(ru *session.SimpleUser) bool {
return ru.Id == u.Id
}) {
userList = append(userList, *u)
userList = append(userList, u)
}
}
}
}
resp := struct {
UserList []session.SimpleUser `json:"userList"`
UserList []*session.SimpleUser `json:"userList"`
SessionUser *session.SimpleUser `json:"sessionUser,omitempty"`
}{
UserList: userList,

View File

@@ -1,5 +0,0 @@
module github.com/idrunk/dce-go/_examples/apis
go 1.23.3
require github.com/coder/websocket v1.8.12

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"net/http"
"github.com/idrunk/dce-go/proto"
"github.com/quic-go/quic-go/http3"
"go.drunkce.com/dce/proto"
)
func init() {

View File

@@ -4,10 +4,10 @@ import (
"fmt"
"net/http"
"github.com/idrunk/dce-go/converter"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/router"
"github.com/idrunk/dce-go/util"
"go.drunkce.com/dce/converter"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/router"
"go.drunkce.com/dce/util"
)
func init() {
@@ -52,13 +52,13 @@ func HttpStart(c *proto.Cli) {
func var1(c *proto.Http) {
user := c.Param("var1")
c.Rp.Req.Header.Set("Content-Type", "text/xml")
te := converter.TextTemplate[*proto.HttpProtocol, Greeting](c, `<?xml version="1.0" encoding="UTF-8"?>
te := converter.TextTemplate[*proto.HttpProtocol, *Greeting](c, `<?xml version="1.0" encoding="UTF-8"?>
<greeting>
<user>{{.User}}</user>
<age>{{.Age}}</age>
<welcome>{{.Welcome}}</welcome>
</greeting>`)
te.Response(Greeting{
te.Response(&Greeting{
User: user,
Age: 0,
Welcome: "Welcome to DCE-GO",
@@ -98,7 +98,7 @@ func var6(c *proto.Http) {
// curl http://127.0.0.1:2046/session/drunk
// curl -I http://127.0.0.1:2046/session
func sessionApi(c *proto.Http) {
t := converter.EmptyTemplate(c)
t := converter.StatusTemplate(c)
if username := c.Param("username"); username == "dce" {
msg, _ := c.Rp.CtxData("hello")
fmt.Println(msg)
@@ -117,21 +117,21 @@ func hello(c *proto.Http) {
// curl -H "Content-Type: application/json" -d "{""user"":""Drunk"",""age"":18}" http://127.0.0.1:2046/hello
func helloPost(c *proto.Http) {
var legalAge uint8 = 18
jc := converter.JsonConverter[*proto.HttpProtocol, GreetingReq, Greeting, Greeting, GreetingResp](c)
body, _ := jc.Parse()
jc := converter.JsonResponser[*proto.HttpProtocol, *Greeting, *GreetingResp](c)
body, _ := converter.JsonRequester[*proto.HttpProtocol, *GreetingReq, *Greeting](c).Parse()
fmt.Println(body)
if body.Age >= legalAge {
body.Welcome = fmt.Sprintf("Hello %s, welcome", body.User)
jc.Response(body)
} else {
jc.Fail(fmt.Sprintf("Sorry, only service for over %d years old peoples", legalAge), 0)
jc.Fail(fmt.Sprintf("Sorry, only service for over %d years old peoples", legalAge), 403)
}
}
// curl http://127.0.0.1:2046/
func home(c *proto.Http) {
jc := converter.JsonConverterNoParse[*proto.HttpProtocol, Greeting, GreetingResp](c)
jc.Response(Greeting{
jc := converter.JsonResponser[*proto.HttpProtocol, *Greeting, *GreetingResp](c)
jc.Response(&Greeting{
User: "Dce",
Age: 18,
Welcome: "Welcome to Golang",
@@ -153,16 +153,16 @@ type GreetingResp struct {
Welcome string `json:"welcome"`
}
func (gr *GreetingReq) Into() (Greeting, error) {
return Greeting{
func (gr *GreetingReq) Into() (*Greeting, error) {
return &Greeting{
User: gr.User,
Age: gr.Age,
Welcome: "",
}, nil
}
func (gr *GreetingResp) From(g Greeting) (GreetingResp, error) {
return GreetingResp{
func (gr *GreetingResp) From(g *Greeting) (*GreetingResp, error) {
return &GreetingResp{
Welcome: g.Welcome,
}, nil
}

View File

@@ -4,10 +4,6 @@ import (
"bufio"
"crypto/sha256"
"fmt"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/proto/flex"
"github.com/idrunk/dce-go/proto/json"
"github.com/idrunk/dce-go/router"
"log"
"log/slog"
"math/rand/v2"
@@ -15,6 +11,11 @@ import (
"os"
"strconv"
"strings"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/proto/flex"
"go.drunkce.com/dce/proto/json"
"go.drunkce.com/dce/router"
)
func JsonTcpStart(c *proto.Cli) {

View File

@@ -4,10 +4,6 @@ import (
"bufio"
"crypto/sha256"
"fmt"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/proto/flex"
"github.com/idrunk/dce-go/proto/pb"
"github.com/idrunk/dce-go/router"
"log"
"log/slog"
"math/rand/v2"
@@ -15,6 +11,11 @@ import (
"os"
"strconv"
"strings"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/proto/flex"
"go.drunkce.com/dce/proto/pb"
"go.drunkce.com/dce/router"
)
func PbTcpStart(c *proto.Cli) {

View File

@@ -2,16 +2,17 @@ package apis
import (
"fmt"
"github.com/idrunk/dce-go/converter"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/router"
"github.com/idrunk/dce-go/session"
"github.com/idrunk/dce-go/session/redises"
"github.com/idrunk/dce-go/util"
"github.com/redis/go-redis/v9"
"net/http"
"slices"
"strings"
"github.com/redis/go-redis/v9"
"go.drunkce.com/dce/converter"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/router"
"go.drunkce.com/dce/session"
"go.drunkce.com/dce/session/redises"
"go.drunkce.com/dce/util"
)
func init() {
@@ -37,8 +38,8 @@ func bind() {
// curl http://127.0.0.1:2050/login -d "{""name"":""Drunk""}" // role 1
// curl http://127.0.0.1:2050/login -d "{""name"":""Dce""}" // role 2
proto.HttpRouter.Post("login", func(h *proto.Http) {
jc := converter.JsonMapConverter(h)
u, ok := jc.Parse()
jc := converter.JsonMapResponser(h)
u, ok := converter.JsonMapRequester(h).Parse()
if !ok {
return
}
@@ -47,7 +48,7 @@ func bind() {
return
}
member, ok := util.SeqFrom(members()).Find(func(m Member) bool {
return strings.ToLower(m.Name) == strings.ToLower(name)
return strings.EqualFold(m.Name, name)
})
if !ok && jc.Fail("Wrong name", 1001) {
return
@@ -68,7 +69,7 @@ func bind() {
// curl http://127.0.0.1:2050/manage/profile -H "X-Session-Id: $session_id" // pass sid on header, can access if sid is valid
// curl http://127.0.0.1:2050/manage/profile -b "session_id=$session_id" // pass sid in cookies, can access if sid is valid
// curl http://127.0.0.1:2050/manage/profile?autologin=1 -H "X-Session-Id: $session_id" // use long life sid to do auto login, will get new sid and the old will destroy
proto.HttpRouter.PushApi(router.Path("manage/profile").ByMethod(proto.HttpGet).Append("roles", 1).BindHosts("2050"), func(h *proto.Http) {
proto.HttpRouter.PushApi(router.Path("manage/profile").ByMethod(proto.HttpGet).Append("roles", 1, 2).BindHosts("2050"), func(h *proto.Http) {
se := h.Rp.Session()
if se == nil && h.SetError(util.Openly0("Invalid session")) {
return
@@ -81,8 +82,8 @@ func bind() {
// curl -X PATCH http://127.0.0.1:2050/manage/profile -H "X-Session-Id: $session_id" -d "{}" // none required fields, got openly err response
// curl -X PATCH http://127.0.0.1:2050/manage/profile -H "X-Session-Id: $session_id" -d "{""name"":""Foo"",""role_id"":2}" // with required, curren session user will update to role 2
proto.HttpRouter.PushApi(router.Path("manage/profile").ByMethod(proto.HttpPatch).Append("roles", 1), func(h *proto.Http) {
jc := converter.JsonMapConverter(h)
u, ok := jc.Parse()
jc := converter.JsonMapResponser(h)
u, ok := converter.JsonMapRequester(h).Parse()
if !ok {
return
}
@@ -221,7 +222,7 @@ type Member struct {
RoleId uint16
}
func (m *Member) Uid() uint64 {
func (m Member) Uid() uint64 {
return m.Id
}

View File

@@ -1,3 +0,0 @@
module github.com/idrunk/dce-go/_examples
go 1.23.3

View File

@@ -1,8 +1,8 @@
package main
import (
"github.com/idrunk/dce-go/_examples/apis"
"github.com/idrunk/dce-go/proto"
"go.drunkce.com/dce/_examples/apis"
"go.drunkce.com/dce/proto"
)
func main() {

View File

@@ -1,3 +0,0 @@
module github.com/idrunk/dce-go/converter
go 1.23.3

View File

@@ -2,103 +2,54 @@ package converter
import (
"encoding/json"
"github.com/idrunk/dce-go/router"
"github.com/idrunk/dce-go/util"
"go.drunkce.com/dce/router"
)
type JsonRequestProcessor[Rp router.RoutableProtocol, ReqDto, Req, Resp, RespDto any] struct {
*router.Context[Rp]
func JsonRequester[Rp router.RoutableProtocol, ReqDto, Req any](ctx *router.Context[Rp]) *router.Requester[Rp, ReqDto, Req] {
return &router.Requester[Rp, ReqDto, Req]{Context: ctx, Deserializer: JsonDeserializer[ReqDto](0)}
}
func (j *JsonRequestProcessor[Rp, ReqDto, Req, Resp, RespDto]) Serialize(dto RespDto) ([]byte, error) {
return json.Marshal(dto)
func JsonRawRequester[Rp router.RoutableProtocol, Req any](ctx *router.Context[Rp]) *router.Requester[Rp, Req, Req] {
return JsonRequester[Rp, Req, Req](ctx)
}
func (j *JsonRequestProcessor[Rp, ReqDto, Req, Resp, RespDto]) Deserialize(seq []byte) (ReqDto, error) {
var obj ReqDto
func JsonStatusRequester[Rp router.RoutableProtocol](ctx *router.Context[Rp]) *router.Requester[Rp, *router.Status, *router.Status] {
return JsonRawRequester[Rp, *router.Status](ctx)
}
func JsonMapRequester[Rp router.RoutableProtocol](ctx *router.Context[Rp]) *router.Requester[Rp, map[string]any, map[string]any] {
return JsonRawRequester[Rp, map[string]any](ctx)
}
func JsonResponser[Rp router.RoutableProtocol, Resp, RespDto any](ctx *router.Context[Rp]) *router.Responser[Rp, Resp, RespDto] {
return &router.Responser[Rp, Resp, RespDto]{Context: ctx, Serializer: JsonSerializer[RespDto](0)}
}
func JsonRawResponser[Rp router.RoutableProtocol, Resp any](ctx *router.Context[Rp]) *router.Responser[Rp, Resp, Resp] {
return JsonResponser[Rp, Resp, Resp](ctx)
}
func JsonStatusResponser[Rp router.RoutableProtocol](ctx *router.Context[Rp]) *router.Responser[Rp, *router.Status, *router.Status] {
return JsonRawResponser[Rp, *router.Status](ctx)
}
func JsonMapResponser[Rp router.RoutableProtocol](ctx *router.Context[Rp]) *router.Responser[Rp, map[string]any, map[string]any] {
return JsonRawResponser[Rp, map[string]any](ctx)
}
type JsonDeserializer[T any] uint8
func (s JsonDeserializer[T]) Deserialize(seq []byte) (T, error) {
var obj T
err := json.Unmarshal(seq, &obj)
return obj, err
}
func (j *JsonRequestProcessor[Rp, ReqDto, Req, Resp, RespDto]) Parse() (Req, bool) {
bytes, err := j.Rp.Body()
if err == nil {
dto, err2 := j.Deserialize(bytes)
if err2 == nil {
obj, err3 := router.DtoInto[ReqDto, Req](dto)
if err3 == nil {
return obj, true
}
err = err3
} else {
err = err2
}
}
j.Rp.SetError(err)
var obj Req
return obj, false
}
type JsonSerializer[T any] uint8
func (j *JsonRequestProcessor[Rp, ReqDto, Req, Resp, RespDto]) Response(obj Resp) bool {
if dto, err := router.DtoFrom[Resp, RespDto](obj); err != nil {
j.Rp.SetError(err)
} else if seq, err := j.Serialize(dto); err != nil {
j.Rp.SetError(err)
} else if _, err := j.Rp.Write(seq); err != nil {
j.Rp.SetError(err)
}
j.Rp.SetCtxData(router.HttpContentTypeKey, "application/json; charset=utf-8")
return true
}
func (j *JsonRequestProcessor[Rp, ReqDto, Req, Resp, RespDto]) Error(err error) bool {
j.Rp.SetError(err)
code, msg := util.ResponseUnits(err)
return j.Status(false, msg, code, nil)
}
func (j *JsonRequestProcessor[Rp, ReqDto, Req, Resp, RespDto]) Success(data any) bool {
return j.Status(true, "", 0, data)
}
func (j *JsonRequestProcessor[Rp, ReqDto, Req, Resp, RespDto]) Fail(msg string, code int) bool {
return j.Status(false, msg, code, nil)
}
func (j *JsonRequestProcessor[Rp, ReqDto, Req, Resp, RespDto]) Status(status bool, msg string, code int, data any) bool {
if seq, err := json.Marshal(router.Status{Status: status, Msg: msg, Code: code, Data: data}); err != nil {
j.Rp.SetError(err)
} else if _, err := j.Rp.Write(seq); err != nil {
j.Rp.SetError(err)
}
j.Rp.SetCtxData(router.HttpContentTypeKey, "application/json; charset=utf-8")
return true
}
func JsonConverterNoConvert[Rp router.RoutableProtocol](c *router.Context[Rp]) JsonRequestProcessor[Rp, router.DoNotConvert, router.DoNotConvert, router.DoNotConvert, router.DoNotConvert] {
return JsonRequestProcessor[Rp, router.DoNotConvert, router.DoNotConvert, router.DoNotConvert, router.DoNotConvert]{c}
}
func JsonConverterNoParseSame[Rp router.RoutableProtocol, Resp any](c *router.Context[Rp]) JsonRequestProcessor[Rp, router.DoNotConvert, router.DoNotConvert, Resp, Resp] {
return JsonRequestProcessor[Rp, router.DoNotConvert, router.DoNotConvert, Resp, Resp]{c}
}
func JsonConverterNoParse[Rp router.RoutableProtocol, Resp, RespDto any](c *router.Context[Rp]) JsonRequestProcessor[Rp, router.DoNotConvert, router.DoNotConvert, Resp, RespDto] {
return JsonRequestProcessor[Rp, router.DoNotConvert, router.DoNotConvert, Resp, RespDto]{c}
}
func JsonConverterSame[Rp router.RoutableProtocol, Req, Resp any](c *router.Context[Rp]) JsonRequestProcessor[Rp, Req, Req, Resp, Resp] {
return JsonRequestProcessor[Rp, Req, Req, Resp, Resp]{c}
}
func JsonMapConverter[Rp router.RoutableProtocol](c *router.Context[Rp]) JsonRequestProcessor[Rp, map[string]any, map[string]any, map[string]any, map[string]any] {
return JsonRequestProcessor[Rp, map[string]any, map[string]any, map[string]any, map[string]any]{c}
}
func JsonMapConverterNoParse[Rp router.RoutableProtocol](c *router.Context[Rp]) JsonRequestProcessor[Rp, router.DoNotConvert, router.DoNotConvert, map[string]any, map[string]any] {
return JsonRequestProcessor[Rp, router.DoNotConvert, router.DoNotConvert, map[string]any, map[string]any]{c}
}
func JsonConverter[Rp router.RoutableProtocol, ReqDto, Req, Resp, RespDto any](c *router.Context[Rp]) JsonRequestProcessor[Rp, ReqDto, Req, Resp, RespDto] {
return JsonRequestProcessor[Rp, ReqDto, Req, Resp, RespDto]{c}
func (s JsonSerializer[T]) Serialize(obj T) ([]byte, error) {
return json.Marshal(obj)
}

113
converter/protobuf.go Normal file
View File

@@ -0,0 +1,113 @@
package converter
import (
"go.drunkce.com/dce/router"
"go.drunkce.com/dce/util"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
func PbRequester[Rp router.RoutableProtocol, ReqDto proto.Message, Req any](ctx *router.Context[Rp]) *router.Requester[Rp, ReqDto, Req] {
return &router.Requester[Rp, ReqDto, Req]{Context: ctx, Deserializer: ProtobufDeserializer[ReqDto](0)}
}
func PbRawRequester[Rp router.RoutableProtocol, Req proto.Message](ctx *router.Context[Rp]) *router.Requester[Rp, Req, Req] {
return PbRequester[Rp, Req, Req](ctx)
}
func PbStatusRequester[Rp router.RoutableProtocol](ctx *router.Context[Rp]) *router.Requester[Rp, *Status, router.Status] {
return PbRequester[Rp, *Status, router.Status](ctx)
}
func PbResponser[Rp router.RoutableProtocol, Resp any, RespDto proto.Message](ctx *router.Context[Rp]) *router.Responser[Rp, Resp, RespDto] {
return &router.Responser[Rp, Resp, RespDto]{Context: ctx, Serializer: ProtobufSerializer[RespDto](0)}
}
func PbRawResponser[Rp router.RoutableProtocol, Resp proto.Message](ctx *router.Context[Rp]) *router.Responser[Rp, Resp, Resp] {
return PbResponser[Rp, Resp, Resp](ctx)
}
func PbStatusResponser[Rp router.RoutableProtocol](ctx *router.Context[Rp]) *router.Responser[Rp, router.Status, *Status] {
return PbResponser[Rp, router.Status, *Status](ctx)
}
type ProtobufDeserializer[T proto.Message] uint8
func (p ProtobufDeserializer[T]) Deserialize(seq []byte) (T, error) {
t := util.NewStruct[T]()
err := proto.Unmarshal(seq, t)
return t, err
}
type ProtobufSerializer[T proto.Message] uint8
func (p ProtobufSerializer[T]) Serialize(t T) ([]byte, error) {
return proto.Marshal(t)
}
func PbJsonRequester[Rp router.RoutableProtocol, ReqDto proto.Message, Req any](ctx *router.Context[Rp]) *router.Requester[Rp, ReqDto, Req] {
return &router.Requester[Rp, ReqDto, Req]{Context: ctx, Deserializer: ProtobufJsonDeserializer[ReqDto](0)}
}
func PbJsonRawRequester[Rp router.RoutableProtocol, Req proto.Message](ctx *router.Context[Rp]) *router.Requester[Rp, Req, Req] {
return PbJsonRequester[Rp, Req, Req](ctx)
}
func PbJsonStatusRequester[Rp router.RoutableProtocol](ctx *router.Context[Rp]) *router.Requester[Rp, *Status, *router.Status] {
return PbJsonRequester[Rp, *Status, *router.Status](ctx)
}
func PbJsonResponser[Rp router.RoutableProtocol, Resp any, RespDto proto.Message](ctx *router.Context[Rp]) *router.Responser[Rp, Resp, RespDto] {
return &router.Responser[Rp, Resp, RespDto]{Context: ctx, Serializer: ProtobufJsonSerializer[RespDto](0)}
}
func PbJsonRawResponser[Rp router.RoutableProtocol, Resp proto.Message](ctx *router.Context[Rp]) *router.Responser[Rp, Resp, Resp] {
return PbJsonResponser[Rp, Resp, Resp](ctx)
}
func PbJsonStatusResponser[Rp router.RoutableProtocol](ctx *router.Context[Rp]) *router.Responser[Rp, *router.Status, *Status] {
return PbJsonResponser[Rp, *router.Status, *Status](ctx)
}
type ProtobufJsonDeserializer[T proto.Message] uint8
func (p ProtobufJsonDeserializer[T]) Deserialize(seq []byte) (T, error) {
t := util.NewStruct[T]()
err := protojson.Unmarshal(seq, t)
return t, err
}
type ProtobufJsonSerializer[T proto.Message] uint8
func (p ProtobufJsonSerializer[T]) Serialize(t T) ([]byte, error) {
return protojson.Marshal(t)
}
func (ps *Status) Into() (*router.Status, error) {
s := router.Status{Data: ps.Data}
if ps.Status != nil {
s.Status = *ps.Status
}
if ps.Code != nil {
s.Code = int(*ps.Code)
}
if ps.Msg != nil {
s.Msg = *ps.Msg
}
return &s, nil
}
func (ps *Status) From(s *router.Status) (*Status, error) {
p := &Status{}
p.Status = &s.Status
p.Code = util.Ref(int64(s.Code))
p.Msg = &s.Msg
p.Data = s.Data
return p, nil
}

156
converter/status.pb.go Normal file
View File

@@ -0,0 +1,156 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.1
// protoc v5.29.2
// source: status.proto
package converter
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Status struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status *bool `protobuf:"varint,1,opt,name=status,proto3,oneof" json:"status,omitempty"`
Code *int64 `protobuf:"varint,2,opt,name=code,proto3,oneof" json:"code,omitempty"`
Msg *string `protobuf:"bytes,3,opt,name=msg,proto3,oneof" json:"msg,omitempty"`
Data *string `protobuf:"bytes,4,opt,name=data,proto3,oneof" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Status) Reset() {
*x = Status{}
mi := &file_status_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Status) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Status) ProtoMessage() {}
func (x *Status) ProtoReflect() protoreflect.Message {
mi := &file_status_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Status.ProtoReflect.Descriptor instead.
func (*Status) Descriptor() ([]byte, []int) {
return file_status_proto_rawDescGZIP(), []int{0}
}
func (x *Status) GetStatus() bool {
if x != nil && x.Status != nil {
return *x.Status
}
return false
}
func (x *Status) GetCode() int64 {
if x != nil && x.Code != nil {
return *x.Code
}
return 0
}
func (x *Status) GetMsg() string {
if x != nil && x.Msg != nil {
return *x.Msg
}
return ""
}
func (x *Status) GetData() string {
if x != nil && x.Data != nil {
return *x.Data
}
return ""
}
var File_status_proto protoreflect.FileDescriptor
var file_status_proto_rawDesc = []byte{
0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x93,
0x01, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x06, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x88, 0x01, 0x01, 0x12, 0x17, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x03, 0x48, 0x01, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x12,
0x15, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x03,
0x6d, 0x73, 0x67, 0x88, 0x01, 0x01, 0x12, 0x17, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04,
0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x42,
0x09, 0x0a, 0x07, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x63,
0x6f, 0x64, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x6d, 0x73, 0x67, 0x42, 0x07, 0x0a, 0x05, 0x5f,
0x64, 0x61, 0x74, 0x61, 0x42, 0x0d, 0x5a, 0x0b, 0x2e, 0x2f, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x72,
0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_status_proto_rawDescOnce sync.Once
file_status_proto_rawDescData = file_status_proto_rawDesc
)
func file_status_proto_rawDescGZIP() []byte {
file_status_proto_rawDescOnce.Do(func() {
file_status_proto_rawDescData = protoimpl.X.CompressGZIP(file_status_proto_rawDescData)
})
return file_status_proto_rawDescData
}
var file_status_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_status_proto_goTypes = []any{
(*Status)(nil), // 0: Status
}
var file_status_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_status_proto_init() }
func file_status_proto_init() {
if File_status_proto != nil {
return
}
file_status_proto_msgTypes[0].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_status_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_status_proto_goTypes,
DependencyIndexes: file_status_proto_depIdxs,
MessageInfos: file_status_proto_msgTypes,
}.Build()
File_status_proto = out.File
file_status_proto_rawDesc = nil
file_status_proto_goTypes = nil
file_status_proto_depIdxs = nil
}

10
converter/status.proto Normal file
View File

@@ -0,0 +1,10 @@
syntax = "proto3";
option go_package = "./converter";
message Status {
optional bool status = 1;
optional int64 code = 2;
optional string msg = 3;
optional string data = 4;
}

View File

@@ -4,29 +4,29 @@ import (
"bytes"
"crypto/md5"
"fmt"
"github.com/idrunk/dce-go/router"
"github.com/idrunk/dce-go/util"
"io/fs"
"os"
"path/filepath"
"text/template"
"go.drunkce.com/dce/router"
"go.drunkce.com/dce/util"
)
type TemplateEngine[Rp router.RoutableProtocol, Resp any] struct {
*router.Context[Rp]
tpl *template.Template
func TemplateResponser[Rp router.RoutableProtocol, D any](ctx *router.Context[Rp], tmpl *template.Template) *router.Responser[Rp, D, D] {
return &router.Responser[Rp, D, D]{ Context: ctx, Serializer: TemplateEngine[D]{tmpl}}
}
func FileTemplate[Rp router.RoutableProtocol, Resp any](c *router.Context[Rp], tplPath string) TemplateEngine[Rp, Resp] {
return TemplateEngine[Rp, Resp]{c, fileTemplate(tplPath, "")}
func FileTemplate[Rp router.RoutableProtocol, D any](c *router.Context[Rp], tplPath string) *router.Responser[Rp, D, D] {
return TemplateResponser[Rp, D](c, fileTemplate(tplPath, ""))
}
func TextTemplate[Rp router.RoutableProtocol, Resp any](c *router.Context[Rp], text string) TemplateEngine[Rp, Resp] {
return TemplateEngine[Rp, Resp]{c, textTemplate(text, "")}
func TextTemplate[Rp router.RoutableProtocol, D any](c *router.Context[Rp], text string) *router.Responser[Rp, D, D] {
return TemplateResponser[Rp, D](c, textTemplate(text, ""))
}
func EmptyTemplate[Rp router.RoutableProtocol](c *router.Context[Rp]) TemplateEngine[Rp, router.DoNotConvert] {
return TemplateEngine[Rp, router.DoNotConvert]{Context: c}
func StatusTemplate[Rp router.RoutableProtocol](c *router.Context[Rp]) *router.Responser[Rp, *router.Status, *router.Status] {
return TemplateResponser[Rp, *router.Status](c, nil)
}
func fileTemplate(tplPath string, key string) *template.Template {
@@ -61,56 +61,29 @@ func textTemplate(text string, key string) *template.Template {
})
}
func (t *TemplateEngine[Rp, Resp]) Serialize(resp Resp) ([]byte, error) {
type TemplateEngine[D any] struct {
*template.Template
}
func (t TemplateEngine[D]) Serialize(resp D) ([]byte, error) {
tpl := t.Template
if status, ok := any(resp).(*router.Status); ok {
if status.Code == 0 {
status.Code = util.ServiceUnavailable
}
if status.Code == 404 {
tpl = statusTemplate(NotfoundTplId)
} else {
tpl = statusTemplate(StatusTplId)
}
}
buff := new(bytes.Buffer)
if err := t.tpl.Execute(buff, resp); err != nil {
if err := tpl.Execute(buff, resp); err != nil {
return nil, err
}
return buff.Bytes(), nil
}
func (t *TemplateEngine[Rp, Resp]) Response(resp Resp) bool {
if bs, err := t.Serialize(resp); err != nil {
t.Rp.SetError(err)
} else if _, err := t.Rp.Write(bs); err != nil {
t.Rp.SetError(err)
}
return true
}
func (t *TemplateEngine[Rp, Resp]) Error(err error) bool {
code, msg := util.ResponseUnits(err)
return t.Status(false, msg, code, nil)
}
func (t *TemplateEngine[Rp, Resp]) Success(data any) bool {
return t.Status(true, "", 0, data)
}
func (t *TemplateEngine[Rp, Resp]) Fail(msg string, code int) bool {
return t.Status(false, msg, code, nil)
}
func (t *TemplateEngine[Rp, Resp]) Status(status bool, msg string, code int, data any) bool {
if code == 0 {
code = util.ServiceUnavailable
}
s := router.Status{Status: status, Msg: msg, Code: code, Data: data}
var tpl *template.Template
if code == 404 {
tpl = statusTemplate(NotfoundTplId)
} else {
tpl = statusTemplate(StatusTplId)
}
buff := new(bytes.Buffer)
if err := tpl.Execute(buff, s); err != nil {
t.Rp.SetError(err)
} else if _, err := t.Write(buff.Bytes()); err != nil {
t.Rp.SetError(err)
}
return true
}
func statusTemplate(tplId string) *template.Template {
if tplId == StatusTplId {
if path, text := TplConfig.statusTpl(); len(path) > 0 {

26
go.mod Normal file
View File

@@ -0,0 +1,26 @@
module go.drunkce.com/dce
go 1.23.3
require (
github.com/coder/websocket v1.8.12
github.com/quic-go/quic-go v0.49.0
github.com/redis/go-redis/v9 v9.7.0
google.golang.org/protobuf v1.36.5
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/tools v0.22.0 // indirect
)

14
go.work
View File

@@ -1,15 +1,3 @@
go 1.23.3
use (
./converter
./_examples
./_examples/apis
./proto
./proto/flex
./proto/json
./proto/pb
./router
./session
./session/redises
./util
)
use .

View File

@@ -3,12 +3,13 @@ package proto
import (
"bytes"
"fmt"
"github.com/idrunk/dce-go/router"
"io"
"log/slog"
"os"
"strings"
"time"
"go.drunkce.com/dce/router"
)
const (

View File

@@ -8,12 +8,12 @@ Protocol format:
0 1 2 3 4 5 6 7 0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
+-+-+-+-+-+-+-+-+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|I|P|S|C|M|L|N|R| LEN of| LEN of| LEN of| LEN of| ID | CODE |NumPath| same |
|D|A|I|O|S|O|P|S| Path | Sid | Msg |Payload|FlexNum|FlexNum|FlexNum| order |
|D|A|I|O|S|O|P|S| Path | Sid | Msg | Body |FlexNum|FlexNum|FlexNum| order |
|E|T|D|D|G|A|A|V|FlexNum|FlexNum|FlexNum|FlexNum| HEAD | HEAD | HEAD |FlexNum|
|N|H| |E| |D|T| | HEAD | HEAD | HEAD | HEAD | | | | BODY |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - - - - - - - - - - - - - - - - - - - - - - |
| | | | |
| Path | Sid | Msg | Payload Data ... |
| Path | Sid | Msg | Body Data ... |
| | | | |
+-+-------------+-+-------------+-+-------------+-------------------------------+

View File

@@ -2,14 +2,15 @@ package flex
import (
"bufio"
"github.com/idrunk/dce-go/router"
"github.com/idrunk/dce-go/util"
"io"
"math"
"math/bits"
"net"
"slices"
"sync/atomic"
"go.drunkce.com/dce/router"
"go.drunkce.com/dce/util"
)
type PackageProtocol[Req any] struct {

View File

@@ -1,5 +0,0 @@
module github.com/idrunk/dce-go/proto/flex
go 1.23.3
require github.com/quic-go/quic-go v0.48.2

View File

@@ -2,9 +2,10 @@ package flex
import (
"bufio"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/router"
"github.com/quic-go/quic-go"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/router"
)
type Quic = router.Context[*QuicProtocol]

View File

@@ -2,10 +2,11 @@ package flex
import (
"bufio"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/router"
"log/slog"
"net"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/router"
)
type Tcp = router.Context[*TcpProtocol]

View File

@@ -4,8 +4,9 @@ import (
"bufio"
"bytes"
"fmt"
"github.com/idrunk/dce-go/router"
"net"
"go.drunkce.com/dce/router"
)
type Udp = router.Context[*UdpProtocol]

View File

@@ -2,11 +2,12 @@ package flex
import (
"bufio"
"github.com/coder/websocket"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/router"
"log/slog"
"net/http"
"github.com/coder/websocket"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/router"
)
type Websocket = router.Context[*WebsocketProtocol]

View File

@@ -1,3 +0,0 @@
module github.com/idrunk/dce-go/proto
go 1.23.3

View File

@@ -2,14 +2,15 @@ package proto
import (
"errors"
"github.com/idrunk/dce-go/router"
"github.com/idrunk/dce-go/util"
"io"
"net/http"
"slices"
"strconv"
"strings"
"time"
"go.drunkce.com/dce/router"
"go.drunkce.com/dce/util"
)
var (
@@ -26,7 +27,7 @@ var (
type Http = router.Context[*HttpProtocol]
const headerSidKey = "X-Session-Id"
const HeaderSidKey = "X-Session-Id"
type HttpProtocol struct {
router.Meta[*http.Request]
@@ -65,13 +66,15 @@ func (h *HttpProtocol) Body() ([]byte, error) {
return io.ReadAll(h.Req.Body)
}
var headerSidKey string = strings.ToLower(HeaderSidKey)
func (h *HttpProtocol) Sid() string {
if headerSid := h.Req.Header.Get(headerSidKey); len(headerSid) > 0 {
if headerSid := h.Req.Header.Get(HeaderSidKey); len(headerSid) > 0 {
return headerSid
} else if cookies := h.Req.Cookies(); len(cookies) > 0 {
if cookie, ok := util.SeqFrom(cookies).Find(func(c *http.Cookie) bool {
lower := strings.ToLower((*c).Name)
return lower == "session_id" || lower == "session-id" || lower == "x-session-sid"
return lower == "session_id" || lower == "session-id" || lower == headerSidKey
}); ok {
return (*cookie).Value
}
@@ -153,10 +156,7 @@ func (h *WrappedHttpRouter) PushApi(api router.Api, controller func(c *Http)) *W
}
func (h *WrappedHttpRouter) Route(writer http.ResponseWriter, request *http.Request) {
hp := &HttpProtocol{
Meta: router.NewMeta(request, nil, false),
Writer: writer,
}
hp := NewHttpProtocol(writer, request)
context := router.NewContext(hp)
h.Raw().Route(context)
hp.TryPrintErr()
@@ -165,14 +165,14 @@ func (h *WrappedHttpRouter) Route(writer http.ResponseWriter, request *http.Requ
}
if hp.Error() != nil {
var e util.Error
if errors.As(hp.Error(), &e) && e.IsOpenly() && e.Code < 600 {
hp.Writer.WriteHeader(e.Code)
} else {
if !errors.As(hp.Error(), &e) {
hp.Writer.WriteHeader(util.ServiceUnavailable)
} else if ! e.IsOpenly() || hp.ResponseEmpty() {
hp.Writer.WriteHeader(util.Iif(e.Code > 0 && e.Code < 600, e.Code, util.ServiceUnavailable))
}
}
if sid := hp.RespSid(); sid != "" {
hp.Writer.Header().Set(headerSidKey, sid)
hp.Writer.Header().Set(HeaderSidKey, sid)
}
bytes := hp.ClearBuffer()
if _, err := hp.Writer.Write(bytes); err != nil {
@@ -180,6 +180,13 @@ func (h *WrappedHttpRouter) Route(writer http.ResponseWriter, request *http.Requ
}
}
func NewHttpProtocol(writer http.ResponseWriter, request *http.Request) *HttpProtocol {
return &HttpProtocol{
Meta: router.NewMeta(request, nil, false),
Writer: writer,
}
}
var HttpRouter *WrappedHttpRouter
func init() {

View File

@@ -1,3 +0,0 @@
module github.com/idrunk/dce-go/proto/json
go 1.23.3

View File

@@ -2,9 +2,10 @@ package json
import (
"encoding/json"
"github.com/idrunk/dce-go/router"
"math"
"sync/atomic"
"go.drunkce.com/dce/router"
)
type PackageProtocol[Req any] struct {

View File

@@ -1,11 +1,12 @@
package json
import (
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/proto/flex"
"github.com/idrunk/dce-go/router"
"log/slog"
"net"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/proto/flex"
"go.drunkce.com/dce/router"
)
type Tcp = router.Context[*TcpProtocol]

View File

@@ -2,8 +2,9 @@ package json
import (
"fmt"
"github.com/idrunk/dce-go/router"
"net"
"go.drunkce.com/dce/router"
)
type Udp = router.Context[*UdpProtocol]

View File

@@ -1,11 +1,12 @@
package json
import (
"github.com/coder/websocket"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/router"
"log/slog"
"net/http"
"github.com/coder/websocket"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/router"
)
type Websocket = router.Context[*WebsocketProtocol]

View File

@@ -1,5 +0,0 @@
module github.com/idrunk/dce-go/proto/pb
go 1.23.3
require google.golang.org/protobuf v1.36.1

View File

@@ -1,11 +1,12 @@
package pb
import (
"github.com/idrunk/dce-go/router"
"google.golang.org/protobuf/proto"
"log/slog"
"math"
"sync/atomic"
"go.drunkce.com/dce/router"
"google.golang.org/protobuf/proto"
)
type PackageProtocol[Req any] struct {

View File

@@ -1,11 +1,12 @@
package pb
import (
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/proto/flex"
"github.com/idrunk/dce-go/router"
"log/slog"
"net"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/proto/flex"
"go.drunkce.com/dce/router"
)
type Tcp = router.Context[*TcpProtocol]

View File

@@ -2,8 +2,9 @@ package pb
import (
"fmt"
"github.com/idrunk/dce-go/router"
"net"
"go.drunkce.com/dce/router"
)
type Udp = router.Context[*UdpProtocol]

View File

@@ -1,11 +1,12 @@
package pb
import (
"github.com/coder/websocket"
"github.com/idrunk/dce-go/proto"
"github.com/idrunk/dce-go/router"
"log/slog"
"net/http"
"github.com/coder/websocket"
"go.drunkce.com/dce/proto"
"go.drunkce.com/dce/router"
)
type Websocket = router.Context[*WebsocketProtocol]

View File

@@ -1,10 +1,12 @@
package proto
import (
"github.com/idrunk/dce-go/router"
"github.com/idrunk/dce-go/util"
"fmt"
"log/slog"
"sync"
"go.drunkce.com/dce/router"
"go.drunkce.com/dce/util"
)
func NewConnectorMappingManager[Rp router.RoutableProtocol, C any](routerId string) ConnectorMappingManager[Rp, C] {
@@ -28,7 +30,7 @@ func (w *ConnectorMappingManager[Rp, C]) Unmapping(addr string) {
func (w *ConnectorMappingManager[Rp, C]) Except(addr string, err error) bool {
w.Unmapping(addr)
slog.Debug("Client disconnected with: %s", err.Error())
slog.Debug(fmt.Sprintf("Client disconnected with: %s", err.Error()))
return false
}

View File

@@ -4,7 +4,7 @@ import (
"log"
"strings"
"github.com/idrunk/dce-go/util"
"go.drunkce.com/dce/util"
)
const (
@@ -80,7 +80,7 @@ type Api struct {
Responsive bool
Redirect string
Name string
Extras map[string]any
extras map[string]any
}
func (a Api) ByMethod(method Method) Api {
@@ -88,27 +88,17 @@ func (a Api) ByMethod(method Method) Api {
return a
}
func (a Api) BySuffix(suffixes Suffix) Api {
a.Suffixes = append(a.Suffixes, suffixes)
return a
}
func (a Api) BySuffixes(suffixes ...Suffix) Api {
a.Suffixes = suffixes
return a
}
func (a Api) IsOmission() Api {
func (a Api) AsOmission() Api {
a.Omission = true
return a
}
func (a Api) IsResponsive() Api {
func (a Api) AsResponsive() Api {
a.Responsive = true
return a
}
func (a Api) Unresponsive() Api {
func (a Api) AsUnresponsive() Api {
a.Responsive = false
return a
}
@@ -134,10 +124,10 @@ func (a Api) ByName(name string) Api {
// Returns:
// - The modified `Api` instance with the updated `Extras` map.
func (a Api) With(key string, val any) Api {
if a.Extras == nil {
a.Extras = make(map[string]interface{})
if a.extras == nil {
a.extras = make(map[string]interface{})
}
a.Extras[key] = val
a.extras[key] = val
return a
}
@@ -153,15 +143,15 @@ func (a Api) With(key string, val any) Api {
// Returns:
// - The modified `Api` instance with the updated `Extras` map.
func (a Api) Append(key string, items ...any) Api {
if val := a.Extras[key]; val == nil {
if a.Extras == nil {
a.Extras = make(map[string]interface{})
if val := a.extras[key]; val == nil {
if a.extras == nil {
a.extras = make(map[string]interface{})
}
a.Extras[key] = new([]any)
a.extras[key] = new([]any)
}
val := a.Extras[key]
if vec, ok := val.([]any); ok || vec == nil {
a.Extras[key] = append(vec, items...)
val := a.extras[key]
if vec, ok := val.([]any); ok || len(vec) == 0 {
a.extras[key] = append(vec, items...)
} else {
log.Panicf("Api with path \"%s\" was already has an extra keyd by \"%s\", but is not a slice value.", a.Path, key)
}
@@ -169,14 +159,14 @@ func (a Api) Append(key string, items ...any) Api {
}
func (a Api) ExtraBy(key string) any {
if val, ok := a.Extras[key]; ok {
if val, ok := a.extras[key]; ok {
return val
}
return nil
}
func (a Api) ExtrasBy(key string) []any {
if val, ok := a.Extras[key]; ok {
if val, ok := a.extras[key]; ok {
if vec, ok := val.([]any); ok {
return vec
}
@@ -206,7 +196,7 @@ func (a Api) Hosts() []string {
}
func Path(path string) Api {
return Api{Path: path}
return Api{Path: path, Responsive: true}
}
const extraServeAddrKey = "$#BIND-HOSTS#"

View File

@@ -4,7 +4,7 @@ import (
"strings"
"time"
"github.com/idrunk/dce-go/util"
"go.drunkce.com/dce/util"
)
// Context is a generic struct that encapsulates the state and functionality for handling RoutableProtocol requests.
@@ -49,6 +49,13 @@ func (c *Context[Rp]) Param(key string) string {
return ""
}
func (c *Context[Rp]) ParamOr(key string, def string) string {
if param, ok := c.params[key]; ok {
return param.Value()
}
return def
}
func (c *Context[Rp]) Params(key string) []string {
if param, ok := c.params[key]; ok {
return param.Values()

View File

@@ -3,47 +3,97 @@ package router
import (
"reflect"
"github.com/idrunk/dce-go/util"
"go.drunkce.com/dce/util"
)
// RequestProcessor is a generic interface that defines the contract for processing requests.
// It is parameterized with two types: Obj and Dto.
// - Obj represents the type of the object that will be processed.
// - Dto represents the type of the Data Transfer Object (DTO) that will be used for serialization or deserialization.
// Implementations of this interface are expected to handle the processing logic for converting between Obj and Dto,
// and for managing the request lifecycle, including error handling and response generation.
type RequestProcessor[Obj, Dto any] interface {
// Response processes the given response object of type Obj and returns a boolean indicating success or failure.
// This method is typically used to handle the final output of a request processing pipeline.
Response(resp Obj) bool
// Error handles an error encountered during request processing and returns a boolean indicating whether the error was successfully handled.
// This method is used to manage error states and ensure proper error reporting.
Error(err error) bool
// Success processes a successful response with the provided data and returns a boolean indicating success.
// This method is used to handle successful outcomes and generate appropriate responses.
Success(data any) bool
// Fail processes a failure response with the provided error message and status code, and returns a boolean indicating failure.
// This method is used to handle failed outcomes and generate appropriate error responses.
Fail(msg string, code int) bool
// Status processes a response with the provided status, message, status code, and data, and returns a boolean indicating success or failure.
// This method is a more generalized version of Success and Fail, allowing for custom status handling.
Status(status bool, msg string, code int, data any) bool
type Requester[Rp RoutableProtocol, ReqDto, Req any] struct {
Context *Context[Rp]
Deserializer Deserializer[ReqDto]
}
type Parser[Obj any] interface {
Parse() (Obj, bool)
func (r *Requester[Rp, ReqDto, Req]) Deserialize(seq []byte) (Req, error) {
dto, err := r.Deserializer.Deserialize(seq)
if err != nil {
return util.NewStruct[Req](), err
}
return DtoInto[ReqDto, Req](dto)
}
type Serializer[Dto any] interface {
Serialize(dto Dto) ([]byte, error)
func (r *Requester[Rp, ReqDto, Req]) Parse() (Req, bool) {
if body, err := r.Context.Body(); err != nil {
r.Context.Rp.SetError(err)
} else if req, err := r.Deserialize(body); err != nil {
r.Context.Rp.SetError(err)
} else {
return req, true
}
return util.NewStruct[Req](), false
}
type Deserializer[Dto any] interface {
Deserialize(bytes []byte) (Dto, error)
type Responser[Rp RoutableProtocol, Resp, RespDto any] struct {
Context *Context[Rp]
Serializer Serializer[RespDto]
}
func (r *Responser[Rp, Resp, RespDto]) Serialize(obj Resp) ([]byte, error) {
dto, err := DtoFrom[Resp, RespDto](obj)
if err != nil {
return nil, err
}
return r.Serializer.Serialize(dto)
}
func (r *Responser[Rp, Resp, RespDto]) Response(obj Resp) bool {
if seq, err := r.Serialize(obj); err != nil {
r.Context.Rp.SetError(err)
} else if _, err := r.Context.Write(seq); err != nil {
r.Context.Rp.SetError(err)
}
return true
}
func (r *Responser[Rp, Resp, RespDto]) Error(err error) bool {
r.Context.Rp.SetError(err)
code, msg := util.ResponseUnits(err)
return r.Status(false, msg, code, nil)
}
func (r *Responser[Rp, Resp, RespDto]) Success(data *string) bool {
return r.Status(true, "", 0, data)
}
func (r *Responser[Rp, Resp, RespDto]) Fail(msg string, code int) bool {
return r.Status(false, msg, code, nil)
}
func (r *Responser[Rp, Resp, RespDto]) Status(status bool, msg string, code int, data *string) bool {
if ! status && r.Context.Rp.Error() == nil {
r.Context.Rp.SetError(util.Openly(code, "%s", msg))
}
if reflect.TypeFor[Resp]() == reflect.TypeFor[*Status]() {
obj := &Status{Status: status, Msg: msg, Code: code, Data: data}
return r.Response(any(obj).(Resp))
} else if data != nil {
r.Context.WriteString(*data)
}
return true
}
type Status struct {
Status bool `json:"status,omitempty"`
Code int `json:"code,omitempty"`
Msg string `json:"msg,omitempty"`
Data *string `json:"data,omitempty"`
}
type Serializer[T any] interface {
Serialize(obj T) ([]byte, error)
}
type Deserializer[T any] interface {
Deserialize(bytes []byte) (T, error)
}
type Into[T any] interface {
@@ -55,30 +105,21 @@ type From[S, T any] interface {
}
func DtoInto[Dto, Obj any](dto Dto) (Obj, error) {
if d, ok := any(dto).(Obj); ok {
return d, nil
} else if d2, ok2 := any(&dto).(Into[Obj]); ok2 {
return d2.Into()
if obj, ok := any(dto).(Obj); ok {
return obj, nil
} else if dto, ok := any(dto).(Into[Obj]); ok {
return dto.Into()
}
var obj Obj
return obj, util.Closed0(`Type "%s" doesn't implement the "%s" interface`, reflect.TypeFor[Dto](), reflect.TypeFor[Into[Obj]]())
return util.NewStruct[Obj](), util.Closed0(`Type "%s" doesn't implement the "%s" interface`, reflect.TypeFor[Dto](), reflect.TypeFor[Into[Obj]]())
}
func DtoFrom[Obj, Dto any](obj Obj) (Dto, error) {
dto := new(Dto)
if d, ok := any(obj).(Dto); ok {
return d, nil
} else if d2, ok2 := any(dto).(From[Obj, Dto]); ok2 {
return d2.From(obj)
if dto, ok := any(obj).(Dto); ok {
return dto, nil
}
return *dto, util.Closed0(`Type "%s" doesn't implement the "%s" interface`, reflect.TypeFor[Dto](), reflect.TypeFor[From[Obj, Dto]]())
}
type DoNotConvert uint8
type Status struct {
Status bool `json:"status,omitempty"`
Code int `json:"code,omitempty"`
Msg string `json:"msg,omitempty"`
Data any `json:"data,omitempty"`
var emp Dto
if dto, ok := any(emp).(From[Obj, Dto]); ok {
return dto.From(obj)
}
return emp, util.Closed0(`Type "%s" doesn't implement the "%s" interface`, reflect.TypeFor[Dto](), reflect.TypeFor[From[Obj, Dto]]())
}

View File

@@ -1,3 +0,0 @@
module github.com/idrunk/dce-go/router
go 1.23.3

View File

@@ -6,8 +6,8 @@ import (
"sync"
"time"
"github.com/idrunk/dce-go/session"
"github.com/idrunk/dce-go/util"
"go.drunkce.com/dce/session"
"go.drunkce.com/dce/util"
)
const (
@@ -56,7 +56,7 @@ func (m *Meta[Req]) ClearBuffer() []byte {
return bs
}
func (m *Meta[Req]) NotResponse() bool {
func (m *Meta[Req]) ResponseEmpty() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.respBuffer.Len() == 0

View File

@@ -9,7 +9,7 @@ import (
"strings"
"sync"
"github.com/idrunk/dce-go/util"
"go.drunkce.com/dce/util"
)
const CodeNotFound = 404
@@ -167,7 +167,7 @@ func (r *Router[Rp]) suffixPath(path string, suffix *Suffix) string {
if suffix == nil || len(*suffix) == 0 {
return path
}
return fmt.Sprintf("%s%s%s", path, MarkSuffixBoundary, suffix)
return fmt.Sprintf("%s%s%s", path, MarkSuffixBoundary, *suffix)
}
func (r *Router[Rp]) buildTree() {
@@ -202,8 +202,15 @@ func (r *Router[Rp]) buildTree() {
fills = append(fills, util.NewTuple2(path, newApiBranch(path, []*RpApi[Rp]{})))
}
}
// original remain should directly insert
fills = append(fills, util.NewTuple2(remain.Path, remain))
// If the API already exists in `fills` and `.Apis` is empty, then need to replace with the valid API.
if index := slices.IndexFunc(fills, func(tuple util.Tuple2[string, ApiBranch[Rp]]) bool {
return tuple.A == remain.Path && len(tuple.B.Apis) == 0
}); index > -1 {
fills[index] = util.NewTuple2(remain.Path, remain)
} else {
// Original remain should directly insert
fills = append(fills, util.NewTuple2(remain.Path, remain))
}
}
for _, fill := range fills {
_, _ = tree.SetByPath(strings.Split(fill.A, MarkPathPartSeparator), fill.B)
@@ -268,13 +275,13 @@ func (r *Router[Rp]) locate(path string, apiFinder func([]*RpApi[Rp]) (*RpApi[Rp
}
return nil, nil, nil, util.Openly(CodeNotFound, `path "%s" route failed, could not matched by Router`, path)
}
slog.Debug(`%s: path "%s" matched api "%s"`, reflect.TypeFor[Rp](), reqPath, api.Path)
slog.Debug(fmt.Sprintf(`%s: path "%s" matched api "%s"`, reflect.TypeFor[Rp](), reqPath, api.Path))
return api, pathParams, suffix, nil
}
func (r *Router[Rp]) matchVarPath(path string) (string, map[string]Param, *Suffix, bool) {
pathParts := strings.Split(path, r.pathPartSeparator)
loopItems := []util.Tuple2[*util.Tree[ApiBranch[Rp], string], int]{{&r.apisTree, 0}}
loopItems := []util.Tuple2[*util.Tree[ApiBranch[Rp], string], int]{util.NewTuple2(&r.apisTree, 0)}
pathParams := map[string]Param{}
var targetApiBranch *util.Tree[ApiBranch[Rp], string]
var suffix *Suffix
@@ -423,7 +430,7 @@ func (r *Router[Rp]) routedHandle(api *RpApi[Rp], pathParams map[string]Param, s
func (r *Router[Rp]) idLocate(id string) (*RpApi[Rp], error) {
if api, ok := r.idApiMapping[id]; ok {
slog.Debug(`%s: Uid "%s" matched api "%s"`, reflect.TypeFor[Rp](), id, api.Path)
slog.Debug(fmt.Sprintf(`%s: Uid "%s" matched api "%s"`, reflect.TypeFor[Rp](), id, api.Path))
return api, nil
}
return nil, util.Openly(CodeNotFound, `Uid "%s" route failed, could not matched by Router`, id)

View File

@@ -4,7 +4,7 @@ import (
"math"
"time"
"github.com/idrunk/dce-go/util"
"go.drunkce.com/dce/util"
)
const DefaultNewSidField = "$newid"

View File

@@ -1,18 +1,10 @@
package session
import "maps"
const (
DefaultServerField = "$server"
DefaultClientField = "$client"
)
const (
// typeInfoShadow = 0
typeEntryShadow = iota + 1
typeRequest
)
// ConnectionSession represents a session associated with a connection. It extends the BasicSession
// and implements the IfConnection interface. This struct is used to manage session information
// for both server and client connections, including handling session updates, disconnections,
@@ -23,7 +15,7 @@ type ConnectionSession struct {
// shadow is a connection level session used to update the connection session through this attribute when the sid
// is updated under the request session, in order to update session information when the connection is disconnected
shadow *ConnectionSession
ty int
request bool
serverField string
clientField string
// log the connection info to let the request session to store it
@@ -42,12 +34,10 @@ func (c *ConnectionSession) CloneConnection(cloned IfConnection, basic *BasicSes
return &connection
}
func (c *ConnectionSession) Connect(server string, client string, entry bool) *ConnectionSession {
// Connect bind the addresses of server and client to the session.
func (c *ConnectionSession) Connect(server string, client string) *ConnectionSession {
c.serverToBind = server
c.clientToBind = client
if entry {
c.ty = typeEntryShadow
}
return c
}
@@ -61,7 +51,7 @@ func (c *ConnectionSession) Disconnect() error {
}
func (c *ConnectionSession) Request() bool {
return c.ty == typeRequest
return c.request
}
func (c *ConnectionSession) UpdateShadow(newSid string) error {
@@ -81,21 +71,11 @@ func (c *ConnectionSession) CloneForRequest(sid string) (any, error) {
func (c *ConnectionSession) handleClonedRequest(original *ConnectionSession) {
c.newborn = false
c.shadow = original
c.ty = typeRequest
c.request = true
if len(original.serverToBind) > 0 {
c.serverToBind, c.clientToBind = original.serverToBind, original.clientToBind
data := map[string]interface{}{}
// for some protocol could connect with session id, could be had information stored
if original.ty == typeEntryShadow {
if raw, err := original.Raw(); err == nil {
data = raw
}
}
maps.Copy(data, map[string]any{
original.serverField: original.serverToBind,
original.clientField: original.clientToBind,
})
if err := c.Load(data); err == nil {
if err := c.SilentSet(c.serverField, c.serverToBind); err == nil {
c.SilentSet(c.clientField, c.clientToBind)
original.serverToBind, original.clientToBind = "", ""
}
}

View File

@@ -1,3 +0,0 @@
module github.com/idrunk/dce-go/session
go 1.23.3

View File

@@ -1,5 +0,0 @@
module github.com/idrunk/dce-go/session/redises
go 1.23.3
require github.com/redis/go-redis/v9 v9.7.0

View File

@@ -6,9 +6,9 @@ import (
"fmt"
"time"
"github.com/idrunk/dce-go/session"
"github.com/idrunk/dce-go/util"
"github.com/redis/go-redis/v9"
"go.drunkce.com/dce/session"
"go.drunkce.com/dce/util"
)
// Session is a generic struct that represents a session in a Redis-backed session management system.

View File

@@ -10,7 +10,7 @@ import (
"strconv"
"time"
"github.com/idrunk/dce-go/util"
"go.drunkce.com/dce/util"
)
const DefaultIdName = "dcesid"
@@ -99,7 +99,7 @@ func GenSid(ttlMinutes uint16) (sid string, createStamp int64) {
now := time.Now()
hash := sha256.New()
hash.Write([]byte(fmt.Sprintf("%d-%d", now.UnixNano(), rand.Uint())))
return fmt.Sprintf("%X%04X%X", hash.Sum(nil), ttlMinutes, now.Unix()), now.Unix()
return fmt.Sprintf("%x%04x%x", hash.Sum(nil), ttlMinutes, now.Unix()), now.Unix()
}
func (b *BasicSession) CloneBasic(cloned IfSession, id string) (*BasicSession, error) {
@@ -142,10 +142,10 @@ func (b *BasicSession) Set(field string, value any) error {
val, err := TryMarshal(value, b.IfSession.NeedSerial())
if err != nil {
return err
} else if err := b.TryTouch(); err != nil {
} else if err := b.SilentSet(field, val); err != nil {
return err
}
return b.SilentSet(field, val)
return b.TryTouch()
}
func (b *BasicSession) Get(field string, target any) error {

View File

@@ -2,12 +2,13 @@ package session
import (
"fmt"
"github.com/idrunk/dce-go/util"
"math/rand/v2"
"slices"
"strconv"
"sync"
"time"
"go.drunkce.com/dce/util"
)
// sessionMapping map[string]*shmMeta

View File

@@ -3,7 +3,7 @@ package session
import (
"strconv"
"github.com/idrunk/dce-go/util"
"go.drunkce.com/dce/util"
)
const DefaultUserPrefix = "dceusmap"

View File

@@ -1,3 +0,0 @@
module github.com/idrunk/dce-go/util
go 1.23.3

View File

@@ -3,6 +3,7 @@ package util
import (
"errors"
"fmt"
"reflect"
"strconv"
"time"
)
@@ -83,5 +84,14 @@ func Iif[T any](test bool, trueVal T, falseVal T) T {
func NewStruct[T any]() T {
var t T
return t
ty := reflect.TypeOf(t)
if ty.Kind() != reflect.Ptr {
return t
}
v := reflect.New(ty.Elem())
return v.Interface().(T)
}
func Ref[T any](v T) *T {
return &v
}