diff --git a/README-zh.md b/README-zh.md index b76f1af..ce4a4a1 100644 --- a/README-zh.md +++ b/README-zh.md @@ -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 +} +``` \ No newline at end of file diff --git a/README.md b/README.md index 3bce928..6dd1811 100644 --- a/README.md +++ b/README.md @@ -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 +} +``` \ No newline at end of file diff --git a/_examples/apis/cli.go b/_examples/apis/cli.go index cae1423..dbfdb4e 100644 --- a/_examples/apis/cli.go +++ b/_examples/apis/cli.go @@ -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() { diff --git a/_examples/apis/flex-quic.go b/_examples/apis/flex-quic.go index 8680b6d..c31602d 100644 --- a/_examples/apis/flex-quic.go +++ b/_examples/apis/flex-quic.go @@ -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" diff --git a/_examples/apis/flex-tcp.go b/_examples/apis/flex-tcp.go index 61a19e3..b0a1604 100644 --- a/_examples/apis/flex-tcp.go +++ b/_examples/apis/flex-tcp.go @@ -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) { diff --git a/_examples/apis/flex-udp.go b/_examples/apis/flex-udp.go index 8fd47d6..baca1c3 100644 --- a/_examples/apis/flex-udp.go +++ b/_examples/apis/flex-udp.go @@ -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) { diff --git a/_examples/apis/flex-websocket.go b/_examples/apis/flex-websocket.go index 0c4393b..11c51ae 100644 --- a/_examples/apis/flex-websocket.go +++ b/_examples/apis/flex-websocket.go @@ -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, diff --git a/_examples/apis/go.mod b/_examples/apis/go.mod deleted file mode 100644 index 042caf8..0000000 --- a/_examples/apis/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/idrunk/dce-go/_examples/apis - -go 1.23.3 - -require github.com/coder/websocket v1.8.12 diff --git a/_examples/apis/http-2_3.go b/_examples/apis/http-2_3.go index 90099a3..81e74ae 100644 --- a/_examples/apis/http-2_3.go +++ b/_examples/apis/http-2_3.go @@ -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() { diff --git a/_examples/apis/http.go b/_examples/apis/http.go index 41a54f0..62c893c 100644 --- a/_examples/apis/http.go +++ b/_examples/apis/http.go @@ -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, ` + te := converter.TextTemplate[*proto.HttpProtocol, *Greeting](c, ` {{.User}} {{.Age}} {{.Welcome}} `) - 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 } diff --git a/_examples/apis/json-tcp.go b/_examples/apis/json-tcp.go index 2e36605..473cc42 100644 --- a/_examples/apis/json-tcp.go +++ b/_examples/apis/json-tcp.go @@ -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) { diff --git a/_examples/apis/pb-tcp.go b/_examples/apis/pb-tcp.go index d601153..04d2cc5 100644 --- a/_examples/apis/pb-tcp.go +++ b/_examples/apis/pb-tcp.go @@ -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) { diff --git a/_examples/apis/session.go b/_examples/apis/session.go index 76c2d08..2068e5a 100644 --- a/_examples/apis/session.go +++ b/_examples/apis/session.go @@ -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 } diff --git a/_examples/go.mod b/_examples/go.mod deleted file mode 100644 index 0667baa..0000000 --- a/_examples/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/idrunk/dce-go/_examples - -go 1.23.3 diff --git a/_examples/main.go b/_examples/main.go index 1cd96db..f5adb49 100644 --- a/_examples/main.go +++ b/_examples/main.go @@ -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() { diff --git a/converter/go.mod b/converter/go.mod deleted file mode 100644 index 8eb54f5..0000000 --- a/converter/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/idrunk/dce-go/converter - -go 1.23.3 diff --git a/converter/json.go b/converter/json.go index fed2d07..4f69e26 100644 --- a/converter/json.go +++ b/converter/json.go @@ -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) } diff --git a/converter/protobuf.go b/converter/protobuf.go new file mode 100644 index 0000000..4c85f0f --- /dev/null +++ b/converter/protobuf.go @@ -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 +} diff --git a/converter/status.pb.go b/converter/status.pb.go new file mode 100644 index 0000000..395ee44 --- /dev/null +++ b/converter/status.pb.go @@ -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 +} diff --git a/converter/status.proto b/converter/status.proto new file mode 100644 index 0000000..79cb30f --- /dev/null +++ b/converter/status.proto @@ -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; +} diff --git a/converter/template.go b/converter/template.go index 06d720b..7eefd56 100644 --- a/converter/template.go +++ b/converter/template.go @@ -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 { diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..773fa07 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.work b/go.work index 219e7cc..19b70a5 100644 --- a/go.work +++ b/go.work @@ -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 -) \ No newline at end of file +use . \ No newline at end of file diff --git a/proto/cli.go b/proto/cli.go index 4dcd93b..e832263 100644 --- a/proto/cli.go +++ b/proto/cli.go @@ -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 ( diff --git a/proto/flex/doc.go b/proto/flex/doc.go index 21f1656..701cd44 100644 --- a/proto/flex/doc.go +++ b/proto/flex/doc.go @@ -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 ... | | | | | | +-+-------------+-+-------------+-+-------------+-------------------------------+ diff --git a/proto/flex/flex.go b/proto/flex/flex.go index 9679356..3fb0a43 100644 --- a/proto/flex/flex.go +++ b/proto/flex/flex.go @@ -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 { diff --git a/proto/flex/go.mod b/proto/flex/go.mod deleted file mode 100644 index fd0da00..0000000 --- a/proto/flex/go.mod +++ /dev/null @@ -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 diff --git a/proto/flex/quic.go b/proto/flex/quic.go index 38b7696..84279ba 100644 --- a/proto/flex/quic.go +++ b/proto/flex/quic.go @@ -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] diff --git a/proto/flex/tcp.go b/proto/flex/tcp.go index 42fc8ea..7c59887 100644 --- a/proto/flex/tcp.go +++ b/proto/flex/tcp.go @@ -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] diff --git a/proto/flex/udp.go b/proto/flex/udp.go index bfabf19..cac260f 100644 --- a/proto/flex/udp.go +++ b/proto/flex/udp.go @@ -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] diff --git a/proto/flex/websocket.go b/proto/flex/websocket.go index 3417bfb..801777e 100644 --- a/proto/flex/websocket.go +++ b/proto/flex/websocket.go @@ -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] diff --git a/proto/go.mod b/proto/go.mod deleted file mode 100644 index c99ed21..0000000 --- a/proto/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/idrunk/dce-go/proto - -go 1.23.3 diff --git a/proto/http.go b/proto/http.go index 957d57a..2085b73 100644 --- a/proto/http.go +++ b/proto/http.go @@ -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() { diff --git a/proto/json/go.mod b/proto/json/go.mod deleted file mode 100644 index cbb1459..0000000 --- a/proto/json/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/idrunk/dce-go/proto/json - -go 1.23.3 diff --git a/proto/json/json.go b/proto/json/json.go index 8faf8a6..a3bc137 100644 --- a/proto/json/json.go +++ b/proto/json/json.go @@ -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 { diff --git a/proto/json/tcp.go b/proto/json/tcp.go index b562728..931c11f 100644 --- a/proto/json/tcp.go +++ b/proto/json/tcp.go @@ -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] diff --git a/proto/json/udp.go b/proto/json/udp.go index 0a236f0..f9f9cba 100644 --- a/proto/json/udp.go +++ b/proto/json/udp.go @@ -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] diff --git a/proto/json/websocket.go b/proto/json/websocket.go index 460078c..5a9d97a 100644 --- a/proto/json/websocket.go +++ b/proto/json/websocket.go @@ -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] diff --git a/proto/pb/go.mod b/proto/pb/go.mod deleted file mode 100644 index 394bf04..0000000 --- a/proto/pb/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/idrunk/dce-go/proto/pb - -go 1.23.3 - -require google.golang.org/protobuf v1.36.1 diff --git a/proto/pb/pb.go b/proto/pb/pb.go index 6023178..61b2f02 100644 --- a/proto/pb/pb.go +++ b/proto/pb/pb.go @@ -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 { diff --git a/proto/pb/tcp.go b/proto/pb/tcp.go index 24c8132..7d4ed71 100644 --- a/proto/pb/tcp.go +++ b/proto/pb/tcp.go @@ -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] diff --git a/proto/pb/udp.go b/proto/pb/udp.go index 69ffce1..6b2ca71 100644 --- a/proto/pb/udp.go +++ b/proto/pb/udp.go @@ -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] diff --git a/proto/pb/websocket.go b/proto/pb/websocket.go index d460840..fb5e290 100644 --- a/proto/pb/websocket.go +++ b/proto/pb/websocket.go @@ -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] diff --git a/proto/util.go b/proto/util.go index 8092aa7..5f654c3 100644 --- a/proto/util.go +++ b/proto/util.go @@ -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 } diff --git a/router/api.go b/router/api.go index cc63380..10c4f1d 100644 --- a/router/api.go +++ b/router/api.go @@ -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#" diff --git a/router/context.go b/router/context.go index 4cf6c71..6f79056 100644 --- a/router/context.go +++ b/router/context.go @@ -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() diff --git a/router/converter.go b/router/converter.go index 4191236..550b09b 100644 --- a/router/converter.go +++ b/router/converter.go @@ -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]]()) } diff --git a/router/go.mod b/router/go.mod deleted file mode 100644 index d017fe5..0000000 --- a/router/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/idrunk/dce-go/router - -go 1.23.3 diff --git a/router/protocol.go b/router/protocol.go index 53e6f4a..8571c40 100644 --- a/router/protocol.go +++ b/router/protocol.go @@ -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 diff --git a/router/router.go b/router/router.go index 527f42e..37c63db 100644 --- a/router/router.go +++ b/router/router.go @@ -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) diff --git a/session/auto.go b/session/auto.go index 5d7cca7..e60fe36 100644 --- a/session/auto.go +++ b/session/auto.go @@ -4,7 +4,7 @@ import ( "math" "time" - "github.com/idrunk/dce-go/util" + "go.drunkce.com/dce/util" ) const DefaultNewSidField = "$newid" diff --git a/session/connection.go b/session/connection.go index b044acf..543911f 100644 --- a/session/connection.go +++ b/session/connection.go @@ -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 = "", "" } } diff --git a/session/go.mod b/session/go.mod deleted file mode 100644 index c9271e0..0000000 --- a/session/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/idrunk/dce-go/session - -go 1.23.3 diff --git a/session/redises/go.mod b/session/redises/go.mod deleted file mode 100644 index 47e959e..0000000 --- a/session/redises/go.mod +++ /dev/null @@ -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 diff --git a/session/redises/redis.go b/session/redises/redis.go index 43f2bf8..77208cb 100644 --- a/session/redises/redis.go +++ b/session/redises/redis.go @@ -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. diff --git a/session/session.go b/session/session.go index 69e9571..5f8b038 100644 --- a/session/session.go +++ b/session/session.go @@ -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 { diff --git a/session/shm.go b/session/shm.go index a0b17ec..a312b63 100644 --- a/session/shm.go +++ b/session/shm.go @@ -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 diff --git a/session/user.go b/session/user.go index b7a8e91..c282f57 100644 --- a/session/user.go +++ b/session/user.go @@ -3,7 +3,7 @@ package session import ( "strconv" - "github.com/idrunk/dce-go/util" + "go.drunkce.com/dce/util" ) const DefaultUserPrefix = "dceusmap" diff --git a/util/go.mod b/util/go.mod deleted file mode 100644 index 05938ab..0000000 --- a/util/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/idrunk/dce-go/util - -go 1.23.3 diff --git a/util/mixed.go b/util/mixed.go index 04d6a35..d48b5ef 100644 --- a/util/mixed.go +++ b/util/mixed.go @@ -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 }