mirror of
https://github.com/idrunk/dce-go.git
synced 2025-09-26 19:01:12 +08:00
initial commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.vscode/
|
||||
.idea/
|
||||
.ignore*/
|
||||
*.sum
|
7
LICENSE
Normal file
7
LICENSE
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2021-present, Wen Tan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
28
README-zh.md
Normal file
28
README-zh.md
Normal file
@@ -0,0 +1,28 @@
|
||||
中文 | [English](README.md)
|
||||
|
||||
DCE-GO是一个通用路由库,除了能路由http协议外,还能路由cli、websocket、tcp/udp等非标准可路由协议的协议。
|
||||
|
||||
DCE-GO按功能类型不同,划分了[路由器](router)、[可路由协议](proto)、[转换器](converter)、[会话管理器](session)及[工具](util)等模块。
|
||||
|
||||
路由器模块下,定义了api、上下文及路由器库,以及转换器、可路由协议等接口,它是DCE的核心模块。
|
||||
|
||||
可路由协议模块实现了一些常见协议的可路由协议封装,包括http、cli、websocket、tcp、udp、quic等。
|
||||
|
||||
转换器内置实现了json与template转换器,用于序列化及反序列化串行数据、双向转换传输对象与实体对象等。
|
||||
|
||||
会话管理器模块定义了基础会话、用户会话、连接会话及自重生会话接口,并内置了Redis、共享内存版这些接口的实现类库。
|
||||
|
||||
所有功能特性,都有相应用例,位于[_examples](_examples)目录下。它的路由性能非常高,与gin相当,可在[这里](_examples/attachs/report/ab-test-result.txt)查看ab测试报告,端口`2046`的为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致力于成为一个高效、开放、安全的通用路由库,欢迎你来贡献,使其更加高效、开放、安全。
|
||||
|
||||
TODO:
|
||||
- 优化JS版Websocket可路由协议客户端,完善各协议通信GOLANG版客户端
|
||||
- 升级控制器前后置事件接口,可与程序接口绑定
|
||||
- 完善数字路径支持
|
||||
- 尝试调整弹性数字函数,改为结构方法式
|
||||
- 研究可路由协议中支持自定义业务属性的可能性
|
||||
- 同步DCE-RUST
|
||||
- 逐步替换AI生成的文档为人工文档
|
28
README.md
Normal file
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
[中文](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).
|
||||
|
||||
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.
|
||||
|
||||
The Routable Protocols module implements routable protocol encapsulations for common protocols, including HTTP, CLI, WebSocket, TCP, UDP, QUIC, and more.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
DCE aims to be an efficient, open, and secure universal routing library. Contributions are welcome to make it even more efficient, open, and secure.
|
||||
|
||||
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.
|
45
_examples/apis/cli.go
Normal file
45
_examples/apis/cli.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/idrunk/dce-go/proto"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
)
|
||||
|
||||
func BindCli() {
|
||||
// Register a default command that responds with a welcome message when no specific command is provided.
|
||||
// Example usage: go run .
|
||||
proto.CliRouter.Push("", func(c *router.Context[*proto.CliProtocol]) {
|
||||
_, _ = c.WriteString("Welcome to DCE-GO!")
|
||||
})
|
||||
|
||||
// Register a command to handle the "hello" API with an optional target parameter.
|
||||
// This command demonstrates handling of various command-line arguments, including:
|
||||
// - Positional arguments (e.g., "DCE-GO")
|
||||
// - Named arguments (e.g., "locate=zh-cn", "-locate zh-cn")
|
||||
// - Boolean flags (e.g., "-bool-true", "-bool-false=0", "--bool-true true")
|
||||
// - Array arguments (e.g., "-i=1", "-i 2", "-i 3", "-i=4")
|
||||
// Example usages:
|
||||
// - go run . hello DCE-GO
|
||||
// - go run . hello locate=zh-cn -locate zh-cn DCE-GO
|
||||
// - go run . hello DCE-GO -bool-true -bool-false=0 --bool-true true --locate=zh-cn
|
||||
// - go run . hello DCE-GO -i=1 -i 2 -i 3 -i=4
|
||||
proto.CliRouter.PushApi(router.Api{Path: "hello/{target?}", Responsive: false}, func(c *router.Context[*proto.CliProtocol]) {
|
||||
fmt.Printf("Hello %s!\n", c.Param("target"))
|
||||
fmt.Printf("Arg locate: %s\nArg -locate: %s\n", c.Rp.Arg("locate"), c.Rp.Arg("-locate"))
|
||||
fmt.Printf("Arg -bool-true: %t\nArg -bool-false: %t\nArg --bool-true: %t\n", c.Rp.Bool("-bool-true"), c.Rp.Bool("-bool-false"), c.Rp.Bool("--bool-true"))
|
||||
fmt.Printf("Array arg -i: %v\n", c.Rp.Args("-i"))
|
||||
})
|
||||
|
||||
// Register a command to read input from a pipe or file.
|
||||
// This command reads the body of the input (e.g., from a pipe or file) and prints it.
|
||||
// Example usages:
|
||||
// - go run . read-pipe
|
||||
// - echo "Hello world!" | go run . read-pipe
|
||||
// - go run . read-pipe < go.mod
|
||||
proto.CliRouter.Push("read-pipe", func(c *router.Context[*proto.CliProtocol]) {
|
||||
body, _ := c.Rp.Body()
|
||||
fmt.Printf("parseBody from pipe:\n%sEOF\n", string(body))
|
||||
})
|
||||
}
|
158
_examples/apis/flex-quic.go
Normal file
158
_examples/apis/flex-quic.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"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"
|
||||
)
|
||||
|
||||
const alpn = "dce-quic-example"
|
||||
|
||||
func FlexQuicStart(c *proto.Cli) {
|
||||
flexQuicBind()
|
||||
port := c.Rp.ArgOr("-p", "2045")
|
||||
|
||||
tlsCert, err := tls.LoadX509KeyPair("./attachs/cert/localhost.crt", "./attachs/cert/localhost.key")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
listener, err := quic.ListenAddr(":"+port, &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
NextProtos: []string{alpn},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
fmt.Printf("FlexQuic server is started on port %s\n", port)
|
||||
for {
|
||||
conn, err := listener.Accept(context.Background())
|
||||
if err != nil {
|
||||
slog.Warn(err.Error())
|
||||
continue
|
||||
}
|
||||
go func(conn quic.Connection) {
|
||||
for {
|
||||
if !flex.QuicRouter.Route(conn, nil) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func flexQuicBind() {
|
||||
// service apis
|
||||
|
||||
// go run . quic localhost:2045 -- hello
|
||||
flex.QuicRouter.Push("hello", func(c *flex.Quic) {
|
||||
fmt.Printf("Api \"%s\": hello world\n", c.Api.Path)
|
||||
_, _ = c.WriteString("Hello world")
|
||||
})
|
||||
|
||||
// go run . quic localhost:2045 -- echo "echo me"
|
||||
flex.QuicRouter.Push("echo/{param?}", func(c *flex.Quic) {
|
||||
param := c.Param("param")
|
||||
body, _ := c.Rp.Body()
|
||||
msg := fmt.Sprintf("path param data: %s\nbody data: %s", param, string(body))
|
||||
fmt.Println(msg)
|
||||
_, _ = c.WriteString(msg)
|
||||
})
|
||||
}
|
||||
|
||||
func certPool() *x509.CertPool {
|
||||
certBytes, _ := os.ReadFile("./attachs/cert/localhost.crt")
|
||||
pool := x509.NewCertPool()
|
||||
pool.AppendCertsFromPEM(certBytes)
|
||||
return pool
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.CliRouter.Push("quic/start", FlexQuicStart)
|
||||
|
||||
// clients
|
||||
|
||||
// go run . quic interactive localhost:2045
|
||||
// and then type in some param
|
||||
proto.CliRouter.Push("quic/interactive/{address}", func(c *proto.Cli) {
|
||||
addr := c.Param("address")
|
||||
tlsConf := &tls.Config{
|
||||
RootCAs: certPool(),
|
||||
NextProtos: []string{alpn},
|
||||
}
|
||||
conn, err := quic.DialAddr(context.Background(), addr, tlsConf, &quic.Config{KeepAlivePeriod: 10 * time.Second})
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer conn.CloseWithError(0, "")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
fmt.Print("Param: ")
|
||||
param, _ := reader.ReadString('\n')
|
||||
param = strings.TrimSpace(param)
|
||||
|
||||
if strings.Compare("exit", param) == 0 {
|
||||
fmt.Println("exiting ...")
|
||||
break
|
||||
}
|
||||
path := "echo/" + param
|
||||
resp := quicRequest(conn, path)
|
||||
fmt.Printf("Got resp:\n%s(%d)\n", resp.Body, len(resp.Body))
|
||||
}
|
||||
})
|
||||
|
||||
proto.CliRouter.Push("quic/{address}", func(c *proto.Cli) {
|
||||
addr := c.Param("address")
|
||||
tlsConf := &tls.Config{
|
||||
RootCAs: certPool(),
|
||||
NextProtos: []string{alpn},
|
||||
}
|
||||
conn, err := quic.DialAddr(context.Background(), addr, tlsConf, nil)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer conn.CloseWithError(0, "")
|
||||
|
||||
passed := c.Rp.Passed
|
||||
if len(passed) == 0 {
|
||||
panic("passed args cannot be empty")
|
||||
}
|
||||
path := strings.Join(passed, router.MarkPathPartSeparator)
|
||||
resp := quicRequest(conn, path)
|
||||
fmt.Printf("Got resp:\n%s(%d)\n", resp.Body, len(resp.Body))
|
||||
})
|
||||
}
|
||||
|
||||
func quicRequest(conn quic.Connection, path string) *flex.Package {
|
||||
stream, err := conn.OpenStreamSync(context.Background())
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer stream.Close()
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(strconv.FormatUint(rand.Uint64(), 10)))
|
||||
content := []byte(fmt.Sprintf("Rand content「%X」", hash.Sum(nil)))
|
||||
if _, err := stream.Write(flex.NewPackage(path, content, "", -1).Serialize()); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
resp, err := flex.PackageDeserialize(bufio.NewReader(stream))
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
return resp
|
||||
}
|
127
_examples/apis/flex-tcp.go
Normal file
127
_examples/apis/flex-tcp.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package apis
|
||||
|
||||
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"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func FlexTcpStart(c *proto.Cli) {
|
||||
flexTcpBind()
|
||||
port := c.Rp.ArgOr("-p", "2048")
|
||||
|
||||
listener, err := net.Listen("tcp", ":"+port)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
fmt.Printf("FlexTcp server is started on port %s\n", port)
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
slog.Warn(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
for {
|
||||
if !flex.TcpRouter.Route(conn, nil) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func flexTcpBind() {
|
||||
// service apis
|
||||
|
||||
// go run . tcp 127.0.0.1:2048 -- hello
|
||||
flex.TcpRouter.Push("hello", func(c *flex.Tcp) {
|
||||
fmt.Printf("Api \"%s\": hello world\n", c.Api.Path)
|
||||
_, _ = c.WriteString("Hello world")
|
||||
})
|
||||
|
||||
// go run . tcp 127.0.0.1:2048 -- echo "echo me"
|
||||
flex.TcpRouter.Push("echo/{param?}", func(c *flex.Tcp) {
|
||||
param := c.Param("param")
|
||||
body, _ := c.Rp.Body()
|
||||
_, _ = c.WriteString(fmt.Sprintf("path param data: %s\nbody data: %s", param, string(body)))
|
||||
})
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.CliRouter.Push("tcp/start", FlexTcpStart)
|
||||
|
||||
// clients
|
||||
|
||||
// go run . tcp interactive 127.0.0.1:2048
|
||||
// and then type in some param
|
||||
proto.CliRouter.Push("tcp/interactive/{address}", func(c *proto.Cli) {
|
||||
addr := c.Param("address")
|
||||
dial, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer dial.Close()
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
fmt.Print("Param: ")
|
||||
param, _ := reader.ReadString('\n')
|
||||
param = strings.TrimSpace(param)
|
||||
|
||||
if strings.Compare("exit", param) == 0 {
|
||||
fmt.Println("exiting ...")
|
||||
break
|
||||
}
|
||||
path := "echo/" + param
|
||||
resp := tcpRequest(dial, path)
|
||||
fmt.Printf("Got resp:\n%s(%d)\n", resp.Body, len(resp.Body))
|
||||
}
|
||||
})
|
||||
|
||||
proto.CliRouter.Push("tcp/{address}", func(c *proto.Cli) {
|
||||
addr := c.Param("address")
|
||||
if len(addr) == 0 {
|
||||
panic("not a valid address")
|
||||
}
|
||||
dial, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer dial.Close()
|
||||
passed := c.Rp.Passed
|
||||
if len(passed) == 0 {
|
||||
panic("passed args cannot be empty")
|
||||
}
|
||||
path := strings.Join(passed, router.MarkPathPartSeparator)
|
||||
resp := tcpRequest(dial, path)
|
||||
fmt.Printf("Got resp:\n%s(%d)\n", resp.Body, len(resp.Body))
|
||||
})
|
||||
}
|
||||
|
||||
func tcpRequest(dial net.Conn, path string) *flex.Package {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(strconv.FormatUint(rand.Uint64(), 10)))
|
||||
content := []byte(fmt.Sprintf("Rand content「%X」", hash.Sum(nil)))
|
||||
if _, err := dial.Write(flex.NewPackage(path, content, "", -1).Serialize()); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
resp, err := flex.PackageDeserialize(bufio.NewReader(dial))
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
return resp
|
||||
}
|
87
_examples/apis/flex-udp.go
Normal file
87
_examples/apis/flex-udp.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package apis
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func FlexUdpStart(c *proto.Cli) {
|
||||
flexUdpBind()
|
||||
port := c.Rp.ArgOr("-p", "2049")
|
||||
localAddr, _ := net.ResolveUDPAddr("udp", "0.0.0.0:"+port)
|
||||
conn, err := net.ListenUDP("udp", localAddr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("FlexUdp server is started on port %s\n", port)
|
||||
for {
|
||||
var pkg = make([]byte, 8192)
|
||||
n, addr, err := conn.ReadFromUDP(pkg)
|
||||
if err != nil {
|
||||
slog.Warn(err.Error())
|
||||
continue
|
||||
}
|
||||
go func(n int, addr *net.UDPAddr, pkg []byte) {
|
||||
flex.UdpRoute(conn, pkg[:n], addr, nil)
|
||||
}(n, addr, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
func flexUdpBind() {
|
||||
// go run . udp 127.0.0.1:2049 -- hello
|
||||
flex.UdpRouter.Push("hello", func(c *flex.Udp) {
|
||||
fmt.Printf("Api \"%s\": hello world\n", c.Api.Path)
|
||||
_, _ = c.WriteString("Hello world")
|
||||
})
|
||||
|
||||
// go run . udp 127.0.0.1:2049 -- echo "echo me"
|
||||
flex.UdpRouter.Push("echo/{param?}", func(c *flex.Udp) {
|
||||
param := c.Param("param")
|
||||
body, _ := c.Rp.Body()
|
||||
_, _ = c.WriteString(fmt.Sprintf("path param data: %s\nbody data: %s", param, string(body)))
|
||||
})
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.CliRouter.Push("udp/start", FlexUdpStart)
|
||||
|
||||
proto.CliRouter.Push("udp/{address}", func(c *proto.Cli) {
|
||||
addr := c.Param("address")
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
panic("not a valid address")
|
||||
}
|
||||
dial, err := net.DialUDP("udp", nil, udpAddr)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer dial.Close()
|
||||
passed := c.Rp.Passed
|
||||
if len(passed) == 0 {
|
||||
panic("passed args cannot be empty")
|
||||
}
|
||||
path := strings.Join(passed, router.MarkPathPartSeparator)
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(strconv.FormatUint(rand.Uint64(), 10)))
|
||||
content := []byte(fmt.Sprintf("Rand content「%X」", hash.Sum(nil)))
|
||||
if _, err := dial.Write(flex.NewPackage(path, content, "", -1).Serialize()); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
resp, err := flex.PackageDeserialize(bufio.NewReader(dial))
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
fmt.Printf("Got resp:\n%s(%d)\n", resp.Body, len(resp.Body))
|
||||
})
|
||||
}
|
181
_examples/apis/flex-websocket.go
Normal file
181
_examples/apis/flex-websocket.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
proto.CliRouter.Push("websocket/start", FlexWebsocketStart)
|
||||
}
|
||||
|
||||
func FlexWebsocketStart(c *proto.Cli) {
|
||||
port := c.Rp.ArgOr("-p", "2047")
|
||||
flexWebsocketBind(port)
|
||||
fmt.Printf("FlexWebsocket server is starting on port %s\n", port)
|
||||
log.Fatal(http.ListenAndServe(":"+port, http.HandlerFunc(proto.HttpRouter.Route)))
|
||||
}
|
||||
|
||||
func flexWebsocketBind(port string) {
|
||||
wd, _ := os.Getwd()
|
||||
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 }{
|
||||
ServerAddr: "ws://127.0.0.1:" + port + "/ws",
|
||||
})
|
||||
})
|
||||
|
||||
proto.HttpRouter.Get("ws/{sid?}", func(h *proto.Http) {
|
||||
c, err := websocket.Accept(h.Rp.Writer, h.Rp.Req, nil)
|
||||
if err != nil {
|
||||
slog.Warn(err.Error())
|
||||
return
|
||||
}
|
||||
shadowSess, err := 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)
|
||||
// notify others to sync-user-list
|
||||
go syncUserList(shadowSess, h, nil)
|
||||
}()
|
||||
if shadowSess != nil {
|
||||
shadowSess.Connect(":2047", h.Rp.Req.RemoteAddr, true)
|
||||
defer shadowSess.Disconnect()
|
||||
if _, ok := shadowSess.User(); !ok {
|
||||
// auto register and login
|
||||
_ = shadowSess.Login(genUser(), session.DefaultTtlMinutes)
|
||||
// must be generated a new sid when login called
|
||||
h.Rp.SetRespSid(shadowSess.Id())
|
||||
}
|
||||
user, ok := shadowSess.User()
|
||||
if ok {
|
||||
flex.WebsocketRouter.UidSetMapping(h.Rp.Req.RemoteAddr, user.Uid())
|
||||
}
|
||||
// notify all to sync-user-list
|
||||
go syncUserList(shadowSess, h, user)
|
||||
}
|
||||
for {
|
||||
ctxData := map[string]any{"$shadowSession": shadowSess}
|
||||
if !flex.WebsocketRouter.Route(c, h.Rp.Req, ctxData) {
|
||||
break
|
||||
}
|
||||
}
|
||||
_ = c.Close(websocket.StatusNormalClosure, "")
|
||||
})
|
||||
|
||||
flex.WebsocketRouter.Push("send", func(w *flex.Websocket) {
|
||||
msg, err := w.Body()
|
||||
if err != nil && w.SetError(err) {
|
||||
return
|
||||
}
|
||||
go func(w *flex.Websocket) {
|
||||
sess := w.Rp.Session()
|
||||
user, _ := sess.(*session.ShmSession[*session.SimpleUser]).User()
|
||||
body, _ := json.Marshal(struct {
|
||||
Uid uint64 `json:"uid,omitempty"`
|
||||
Nick string `json:"nick,omitempty"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Time string `json:"time,omitempty"`
|
||||
}{
|
||||
user.Uid(),
|
||||
user.Nick,
|
||||
string(msg),
|
||||
time.Now().Format("06-01-02 15:04:05"),
|
||||
})
|
||||
pkg := flex.NewPackage("sync-new-message", body, "", 0).Serialize()
|
||||
for _, conn := range flex.WebsocketRouter.ConnMapping() {
|
||||
go conn.Write(w, websocket.MessageBinary, pkg)
|
||||
}
|
||||
}(w)
|
||||
_, _ = w.Write([]byte{'1'})
|
||||
})
|
||||
|
||||
flex.WebsocketRouter.SetEventHandler(func(ctx *flex.Websocket) error {
|
||||
shadowSession, _ := ctx.Rp.CtxData("$shadowSession")
|
||||
rs := shadowSession.(*session.ShmSession[*session.SimpleUser])
|
||||
cloned, _ := rs.CloneForRequest(ctx.Rp.Sid())
|
||||
sess := cloned.(*session.ShmSession[*session.SimpleUser])
|
||||
if _, ok := sess.User(); !ok {
|
||||
return util.Openly(401, "Unauthorized")
|
||||
}
|
||||
ctx.Rp.SetSession(sess)
|
||||
auto := session.NewAutoRenew(sess)
|
||||
if gotNew, err := auto.TryRenew(); err == nil && gotNew {
|
||||
ctx.Rp.SetRespSid(sess.Id())
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func syncUserList(sess *session.ShmSession[*session.SimpleUser], h *proto.Http, user *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 {
|
||||
us := uses[0].(*session.ShmSession[*session.SimpleUser])
|
||||
if u, o := us.User(); o {
|
||||
conn, _ := flex.WebsocketRouter.ConnBy(addr)
|
||||
connList[addr] = conn
|
||||
if !slices.ContainsFunc(userList, func(ru session.SimpleUser) bool {
|
||||
return ru.Id == u.Id
|
||||
}) {
|
||||
userList = append(userList, *u)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resp := struct {
|
||||
UserList []session.SimpleUser `json:"userList"`
|
||||
SessionUser *session.SimpleUser `json:"sessionUser,omitempty"`
|
||||
}{
|
||||
UserList: userList,
|
||||
}
|
||||
respJson, _ := json.Marshal(resp)
|
||||
respPkg := flex.NewPackage("sync-user-list", respJson, "", 0).Serialize()
|
||||
var respSessionPkg []byte
|
||||
if user != nil {
|
||||
respSession := resp
|
||||
respSession.SessionUser = user
|
||||
respSessionJson, _ := json.Marshal(respSession)
|
||||
respSessionPkg = flex.NewPackage("sync-user-list", respSessionJson, h.Rp.RespSid(), 0).Serialize()
|
||||
}
|
||||
ctx := context.Background()
|
||||
for addr, conn := range connList {
|
||||
if addr == h.Rp.Req.RemoteAddr {
|
||||
_ = conn.Write(ctx, websocket.MessageBinary, respSessionPkg)
|
||||
} else {
|
||||
_ = conn.Write(ctx, websocket.MessageBinary, respPkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var incrementUid uint64 = 0
|
||||
|
||||
func genUser() *session.SimpleUser {
|
||||
namePool := []string{"Drunk", "Golang", "午言"}
|
||||
now := time.Now()
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(fmt.Sprintf("%d-%d", now.UnixNano(), rand.Uint())))
|
||||
incrementUid++
|
||||
return &session.SimpleUser{
|
||||
Id: incrementUid,
|
||||
Nick: fmt.Sprintf("%s-%x", namePool[rand.IntN(len(namePool))], hash.Sum(nil)[:3]),
|
||||
}
|
||||
}
|
487
_examples/apis/flex-websocket.html
Normal file
487
_examples/apis/flex-websocket.html
Normal file
@@ -0,0 +1,487 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Websocket test</title>
|
||||
<style>
|
||||
body{width: 800px; margin: 50px auto}
|
||||
.chat-box {display: flex; border: 1px solid #aaa; border-radius: 6px;}
|
||||
.cb-left {flex: auto; border-right: 1px solid #aaa}
|
||||
.chat-board {height: 500px; overflow-y: auto; border-bottom: 1px solid #aaa;}
|
||||
.msg-bubble {padding: 10px;}
|
||||
.msg-bubble.self {text-align: right}
|
||||
.msg-head {font-size: 14px; color: #777;}
|
||||
.msg-head .time-wrap {font-size: 12px; margin-left: 10px;}
|
||||
.msg-bubble.self .time-wrap {margin: 0 10px 0 0;}
|
||||
.msg-bubble p {margin: 0; padding: 10px 5px;}
|
||||
.editor-wrap {display: flex;}
|
||||
.editor-wrap #editor {flex: auto; border: 0; border-right: 1px solid #aaa; border-bottom-left-radius: 8px;}
|
||||
.editor-wrap button {width: 80px; height: 48px; border: 0;}
|
||||
.group-members {width: 160px; list-style: none; margin: 0; padding: 0;}
|
||||
.group-members li {margin: 10px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>[<span class="online-state"></span>] <span class="session-user"></span>'s group chat</h1>
|
||||
<div class="chat-box">
|
||||
<div class="cb-left">
|
||||
<div class="chat-board"></div>
|
||||
<form class="editor-wrap">
|
||||
<textarea id="editor"></textarea>
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
<ul class="group-members"></ul>
|
||||
</div>
|
||||
<script>
|
||||
class FlexWebsocketClient {
|
||||
ws
|
||||
sidHandler
|
||||
requestMapping = {}
|
||||
listenMapping = {}
|
||||
|
||||
constructor(address, sidHandler, onopen, onclose, onmessage) {
|
||||
const self = this
|
||||
this.sidHandler = sidHandler
|
||||
this.ws = new WebSocket(address)
|
||||
this.ws.binaryType = "arraybuffer"
|
||||
this.ws.onopen = onopen
|
||||
this.ws.onclose = onclose
|
||||
this.ws.onmessage = function (ev) {
|
||||
let fp
|
||||
try {
|
||||
fp = FlexPackage.deserialize(Array.from(new Uint8Array(ev.data)))
|
||||
} catch (e) {
|
||||
onmessage && onmessage(ev.data) || console.error(e)
|
||||
return
|
||||
}
|
||||
self.sidHandler(fp)
|
||||
if (fp.code || fp.message) {
|
||||
console.warn(`Code: ${fp.code}, Message: ${fp.message}`)
|
||||
}
|
||||
if (fp.id in self.requestMapping) {
|
||||
self.requestMapping[fp.id](fp)
|
||||
} else if (fp.path in self.listenMapping) {
|
||||
self.listenMapping[fp.path](fp)
|
||||
} else if (onmessage) {
|
||||
onmessage(fp, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bind(path, callback) {
|
||||
this.listenMapping[path] = callback
|
||||
}
|
||||
|
||||
sendText(content) {
|
||||
this.ws.send(content)
|
||||
}
|
||||
|
||||
send(path, content, id) {
|
||||
const pkg = FlexPackage.new(path, content, this.sidHandler(), id)
|
||||
this.sendText(pkg.serialize())
|
||||
return pkg.id
|
||||
}
|
||||
|
||||
async request(path, content) {
|
||||
const reqId = this.send(path, content, -1)
|
||||
const self = this
|
||||
return new Promise(resolve => {
|
||||
self.requestMapping[reqId] = function(fp) {
|
||||
resolve(fp.body)
|
||||
delete self.requestMapping[fp.id]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class FlexPackage {
|
||||
static #flagId = 128
|
||||
static #flagPath = 64
|
||||
static #flagSid = 32
|
||||
static #flagCode = 16
|
||||
static #flagMsg = 8
|
||||
static #flagBody = 4
|
||||
static #flagNumPath = 2
|
||||
|
||||
static #reqId = 0
|
||||
|
||||
id
|
||||
path
|
||||
numPath
|
||||
sid
|
||||
code
|
||||
message
|
||||
body
|
||||
|
||||
static new(path, body, sid, id, numPath) {
|
||||
if (id === -1) {
|
||||
id = ++FlexPackage.#reqId
|
||||
}
|
||||
const fp = new FlexPackage
|
||||
fp.id = id
|
||||
fp.path = path
|
||||
fp.sid = sid
|
||||
fp.numPath = numPath
|
||||
fp.body = body
|
||||
return fp
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const buffer = [0]
|
||||
const lenSeqInfo = []
|
||||
const textBuffer = []
|
||||
let seq
|
||||
if ((this.path || "").length > 0) {
|
||||
buffer[0] |= FlexPackage.#flagPath
|
||||
textBuffer.push(seq = new TextEncoder().encode(this.path))
|
||||
lenSeqInfo.push(FlexNum.non0LenPackHead(seq.length))
|
||||
}
|
||||
if ((this.sid || "").length > 0) {
|
||||
buffer[0] |= FlexPackage.#flagSid
|
||||
textBuffer.push(seq = new TextEncoder().encode(this.sid))
|
||||
lenSeqInfo.push(FlexNum.non0LenPackHead(seq.length))
|
||||
}
|
||||
if ((this.message || "").length > 0) {
|
||||
buffer[0] |= FlexPackage.#flagMsg
|
||||
textBuffer.push(seq = new TextEncoder().encode(this.message))
|
||||
lenSeqInfo.push(FlexNum.non0LenPackHead(seq.length))
|
||||
}
|
||||
if ((this.body || "").length > 0) {
|
||||
buffer[0] |= FlexPackage.#flagBody
|
||||
textBuffer.push(seq = new TextEncoder().encode(this.body))
|
||||
lenSeqInfo.push(FlexNum.non0LenPackHead(seq.length))
|
||||
}
|
||||
if (this.id > 0) {
|
||||
buffer[0] |= FlexPackage.#flagId
|
||||
lenSeqInfo.push(FlexNum.non0LenPackHead(this.id))
|
||||
}
|
||||
if (this.code) {
|
||||
buffer[0] |= FlexPackage.#flagCode
|
||||
lenSeqInfo.push(FlexNum.intPackHead(this.code))
|
||||
}
|
||||
if (this.numPath > 0) {
|
||||
buffer[0] |= FlexPackage.#flagNumPath
|
||||
lenSeqInfo.push(FlexNum.non0LenPackHead(this.numPath))
|
||||
}
|
||||
buffer.push(...Array(lenSeqInfo.length).fill(0))
|
||||
for (let i=0; i<lenSeqInfo.length; i++) {
|
||||
buffer[1+i] = lenSeqInfo[i][0]
|
||||
buffer.push(...FlexNum.packBody(lenSeqInfo[i][3], lenSeqInfo[i][2], lenSeqInfo[i][1]))
|
||||
}
|
||||
for (let i=0; i<textBuffer.length; i++) {
|
||||
buffer.push(...textBuffer[i])
|
||||
}
|
||||
return Uint8Array.from(buffer)
|
||||
}
|
||||
|
||||
static deserialize(seq) {
|
||||
const fp = new FlexPackage
|
||||
const flag = seq.splice(0, 1)?.[0]
|
||||
if (! flag) {
|
||||
return fp
|
||||
}
|
||||
const onesCount = FlexNum.onesCount(flag)
|
||||
const numHeadSeq = seq.splice(0, onesCount)
|
||||
if (numHeadSeq.length < onesCount) {
|
||||
return fp
|
||||
}
|
||||
const numInfoSeq = new Array(onesCount).fill(0)
|
||||
for (let i=0; i<onesCount; i++) {
|
||||
const tuple = FlexNum.parseHead(numHeadSeq[i], true)
|
||||
const numBodySeq = tuple[1] > 0 ? seq.splice(0, tuple[1]) : []
|
||||
if (numBodySeq.length < tuple[1]) {
|
||||
return fp
|
||||
}
|
||||
numInfoSeq[i] = [tuple, numBodySeq]
|
||||
}
|
||||
if ((flag & this.#flagPath) > 0) {
|
||||
const [numInfo, numBodySeq] = numInfoSeq.shift()
|
||||
const len = FlexNum.non0LenParse([numInfo[3], ...numBodySeq])
|
||||
const sq = seq.splice(0, len)
|
||||
if (sq.length < len) {
|
||||
return fp
|
||||
}
|
||||
fp.path = new TextDecoder().decode(Uint8Array.from(sq))
|
||||
}
|
||||
if ((flag & this.#flagSid) > 0) {
|
||||
const [numInfo, numBodySeq] = numInfoSeq.shift()
|
||||
const len = FlexNum.non0LenParse([numInfo[3], ...numBodySeq])
|
||||
const sq = seq.splice(0, len)
|
||||
if (sq.length < len) {
|
||||
return fp
|
||||
}
|
||||
fp.sid = new TextDecoder().decode(Uint8Array.from(sq))
|
||||
}
|
||||
if ((flag & this.#flagMsg) > 0) {
|
||||
const [numInfo, numBodySeq] = numInfoSeq.shift()
|
||||
const len = FlexNum.non0LenParse([numInfo[3], ...numBodySeq])
|
||||
const sq = seq.splice(0, len)
|
||||
if (sq.length < len) {
|
||||
return fp
|
||||
}
|
||||
fp.message = new TextDecoder().decode(Uint8Array.from(sq))
|
||||
}
|
||||
if ((flag & this.#flagBody) > 0) {
|
||||
const [numInfo, numBodySeq] = numInfoSeq.shift()
|
||||
const len = FlexNum.non0LenParse([numInfo[3], ...numBodySeq])
|
||||
const sq = seq.splice(0, len)
|
||||
if (sq.length < len) {
|
||||
return fp
|
||||
}
|
||||
fp.body = new TextDecoder().decode(Uint8Array.from(sq))
|
||||
}
|
||||
if ((flag & this.#flagId) > 0) {
|
||||
const [numInfo, numBodySeq] = numInfoSeq.shift()
|
||||
fp.id = FlexNum.non0LenParse([numInfo[3], ...numBodySeq])
|
||||
}
|
||||
if ((flag & this.#flagCode) > 0) {
|
||||
const [numInfo, numBodySeq] = numInfoSeq.shift()
|
||||
fp.code = FlexNum.intParse([numInfo[0], ...numBodySeq], numInfo[2])
|
||||
}
|
||||
if ((flag & this.#flagNumPath) > 0) {
|
||||
const [numInfo, numBodySeq] = numInfoSeq.shift()
|
||||
fp.numPath = FlexNum.non0LenParse([numInfo[3], ...numBodySeq])
|
||||
}
|
||||
return fp
|
||||
}
|
||||
}
|
||||
|
||||
class FlexNum {
|
||||
static uintSerialize(unsigned) {
|
||||
return this.#serialize(...this.#uintPackHead(unsigned))
|
||||
}
|
||||
|
||||
static intSerialize(integer) {
|
||||
return this.#serialize(...this.intPackHead(integer))
|
||||
}
|
||||
|
||||
static non0LenPackHead(unsigned) {
|
||||
return this.#uintPackHead(unsigned - 1)
|
||||
}
|
||||
|
||||
static #uintPackHead(unsigned) {
|
||||
const usize = Math.abs(unsigned)
|
||||
const bitsLen = this.bitsLen(usize)
|
||||
const [head, bytesLen] = this.#packHead(usize, bitsLen)
|
||||
return [head, bytesLen, bitsLen, usize]
|
||||
}
|
||||
|
||||
static intPackHead(integer) {
|
||||
let unsigned = 0
|
||||
if (integer < 0) {
|
||||
unsigned = Math.abs(integer)
|
||||
}
|
||||
let bitsLen = FlexNum.bitsLen(unsigned)
|
||||
let [head, bytesLen] = FlexNum.#packHead(unsigned, bitsLen)
|
||||
if (integer < 0) {
|
||||
let negative = 1
|
||||
if (bytesLen < 7) {
|
||||
negative = 1 << (6 - bytesLen)
|
||||
}
|
||||
head |= negative
|
||||
}
|
||||
return [head, bytesLen, bitsLen, unsigned]
|
||||
}
|
||||
|
||||
static #packHead(unsigned, bitsLen) {
|
||||
let bytesLen = Math.floor(bitsLen / 8)
|
||||
let headMaskShift = 8 - bytesLen
|
||||
let headBits = 0
|
||||
if (bytesLen > 5) {
|
||||
bytesLen = 8
|
||||
headMaskShift = 2
|
||||
} else if (bitsLen%8 > 7-bytesLen) {
|
||||
bytesLen ++
|
||||
headMaskShift --
|
||||
} else {
|
||||
headBits |= unsigned >> (bytesLen * 8)
|
||||
}
|
||||
return [255 << headMaskShift & 255 | headBits, bytesLen]
|
||||
}
|
||||
|
||||
static #serialize(head, bytesLen, bitsLen, u64) {
|
||||
let units = new Uint8Array(bytesLen + 1)
|
||||
units[0] = head
|
||||
units.set(this.packBody(u64, bitsLen, bytesLen), 1)
|
||||
return units
|
||||
}
|
||||
|
||||
static packBody(usize, bitsLen, bytesLen) {
|
||||
let units = new Uint8Array(bytesLen)
|
||||
for (let i=0; i<bytesLen && i*8 < bitsLen; i++) {
|
||||
units[bytesLen-i-1] = usize >> (i * 8) & 255
|
||||
}
|
||||
return units
|
||||
}
|
||||
|
||||
static uintDeserialize(seq) {
|
||||
[seq[0]] = this.parseHead(seq[0], false)
|
||||
return this.#parse(seq)
|
||||
}
|
||||
|
||||
static intDeserialize(seq) {
|
||||
const [headBits, _, negative] = this.parseHead(seq[0], true)
|
||||
seq[0] = headBits
|
||||
return this.intParse(seq, negative)
|
||||
}
|
||||
|
||||
static parseHead(head, sign) {
|
||||
let unsignedBits = 0
|
||||
let bytesLen = 0
|
||||
let negative = false
|
||||
let originalBits = 0
|
||||
for (let i = 0; i<8; i ++) {
|
||||
if ((128 >> i & head) === 0) {
|
||||
if ((bytesLen = i) > 5) {
|
||||
bytesLen = 8
|
||||
originalBits = 1 & head
|
||||
} else {
|
||||
originalBits = 127 >> bytesLen & head
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
unsignedBits = originalBits
|
||||
if (sign) {
|
||||
if (bytesLen === 8) {
|
||||
negative = (1 & head) === 1
|
||||
} else {
|
||||
let signShift = 0
|
||||
if ((negative = (64 >> bytesLen & head) > 0)) {
|
||||
signShift = 1
|
||||
}
|
||||
unsignedBits = 127 >> bytesLen >> signShift & head
|
||||
}
|
||||
}
|
||||
return [unsignedBits, bytesLen, negative, originalBits]
|
||||
}
|
||||
|
||||
static intParse(seq, negative) {
|
||||
const u64 = this.#parse(seq)
|
||||
if (negative) {
|
||||
return -u64
|
||||
}
|
||||
return u64
|
||||
}
|
||||
|
||||
static non0LenParse(seq) {
|
||||
return this.#parse(seq) + 1
|
||||
}
|
||||
|
||||
static #parse(seq) {
|
||||
let u64 = 0
|
||||
for (let i = 0; i < seq.length; i++) {
|
||||
if (seq[i] > 0) {
|
||||
u64 |= seq[i] << ((seq.length -i - 1) * 8)
|
||||
}
|
||||
}
|
||||
return u64
|
||||
}
|
||||
|
||||
static bitsLen(num) {
|
||||
let len = 0
|
||||
do {
|
||||
len++
|
||||
num >>= 1
|
||||
} while (num > 0)
|
||||
return len
|
||||
}
|
||||
|
||||
static onesCount(num) {
|
||||
let count = 0
|
||||
for (let i = this.bitsLen(num) - 1; i >=0; i --) {
|
||||
if ((1 << i & num) > 0) {
|
||||
count ++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
const sessionUser = {}
|
||||
const sessionUserElem = document.querySelector(".session-user")
|
||||
const chatBoard = document.querySelector(".chat-board")
|
||||
const editorWrap = document.querySelector(".editor-wrap")
|
||||
const editor = document.getElementById("editor")
|
||||
const onlineState = document.querySelector(".online-state")
|
||||
const groupMembers = document.querySelector(".group-members")
|
||||
|
||||
function sidHandler(fp) {
|
||||
if (fp) {
|
||||
if (fp?.sid && fp.sid.length > 0) {
|
||||
sessionStorage.setItem("session-id", fp.sid)
|
||||
}
|
||||
} else {
|
||||
// return location.pathname.replace(/^\/+/, "").replace(/^([^/]+).*/, "$1")
|
||||
return sessionStorage.getItem("session-id")
|
||||
}
|
||||
}
|
||||
|
||||
const SWC = new FlexWebsocketClient(
|
||||
"{{.ServerAddr}}" + (sidHandler() ? "/"+sidHandler() : ""),
|
||||
sidHandler,
|
||||
() => onlineState.textContent = "Online",
|
||||
() => onlineState.textContent = "Offline",
|
||||
)
|
||||
|
||||
SWC.bind("sync-user-list", fp => {
|
||||
groupMembers.innerHTML = ""
|
||||
const resp = JSON.parse(fp.body)
|
||||
if (resp.sessionUser) {
|
||||
Object.assign(sessionUser, resp.sessionUser)
|
||||
sessionUserElem.textContent = sessionUser.nick
|
||||
}
|
||||
for (const user of resp.userList) {
|
||||
const userElem = document.createElement("li")
|
||||
userElem.textContent = user.nick
|
||||
if (user.id === sessionUser.id) {
|
||||
userElem.className = "self"
|
||||
}
|
||||
groupMembers.appendChild(userElem)
|
||||
}
|
||||
})
|
||||
|
||||
SWC.bind("sync-new-message", fp => {
|
||||
const msgPkg = JSON.parse(fp.body)
|
||||
showMsg(msgPkg)
|
||||
})
|
||||
|
||||
function showMsg(msgPkg) {
|
||||
const isSelf = msgPkg.uid === sessionUser.id
|
||||
const msgBubble = chatBoard.appendChild(document.createElement("div"))
|
||||
msgBubble.className = "msg-bubble " + (isSelf ? "self" : "")
|
||||
const msgHead = msgBubble.appendChild(document.createElement("div"))
|
||||
msgHead.className = "msg-head"
|
||||
const userWrap = msgHead.appendChild(document.createElement("span"))
|
||||
userWrap.className = "user-wrap"
|
||||
userWrap.textContent = isSelf ? "ME" : msgPkg.nick
|
||||
const timeWrap = document.createElement("span")
|
||||
timeWrap.className = "time-wrap"
|
||||
timeWrap.textContent = msgPkg.time
|
||||
if (isSelf) {
|
||||
msgHead.insertBefore(timeWrap, userWrap)
|
||||
} else {
|
||||
msgHead.appendChild(timeWrap)
|
||||
}
|
||||
const msgContent = msgBubble.appendChild(document.createElement("p"))
|
||||
msgContent.textContent = msgPkg.msg
|
||||
chatBoard.scrollTop = chatBoard.scrollHeight
|
||||
}
|
||||
|
||||
editorWrap.onsubmit = function () {
|
||||
sendMsg()
|
||||
return false
|
||||
}
|
||||
|
||||
async function sendMsg() {
|
||||
if (editor.value === "") {
|
||||
alert("empty message cannot send")
|
||||
return
|
||||
}
|
||||
await SWC.request("send", editor.value)
|
||||
editor.value = ""
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
5
_examples/apis/go.mod
Normal file
5
_examples/apis/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/idrunk/dce-go/_examples/apis
|
||||
|
||||
go 1.23.3
|
||||
|
||||
require github.com/coder/websocket v1.8.12
|
55
_examples/apis/http-2_3.go
Normal file
55
_examples/apis/http-2_3.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/idrunk/dce-go/proto"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
proto.CliRouter.
|
||||
// go run . http2 start
|
||||
Push("http2/start", Http2Start).
|
||||
// go run . http3 start
|
||||
Push("http3/start", Http3Start)
|
||||
}
|
||||
|
||||
func Http2Start(c *proto.Cli) {
|
||||
// curl --insecure https://localhost:2043/hello
|
||||
proto.HttpRouter.Get("hello", func(h *proto.Http) {
|
||||
_, _ = h.WriteString("hello world")
|
||||
})
|
||||
|
||||
// curl -v --insecure https://localhost:2043/
|
||||
// curl --insecure https://localhost:2043/DCE
|
||||
proto.HttpRouter.Get("{target?}", func(h *proto.Http) {
|
||||
_, _ = h.WriteString("hello " + h.Param("target"))
|
||||
})
|
||||
|
||||
port := c.Rp.ArgOr("-p", "2043")
|
||||
fmt.Printf("Http2 server is starting on port %s\n", port)
|
||||
if err := http.ListenAndServeTLS(":"+port, "./attachs/cert/localhost.crt", "./attachs/cert/localhost.key", http.HandlerFunc(proto.HttpRouter.Route)); err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func Http3Start(c *proto.Cli) {
|
||||
// curl --http3 --insecure https://localhost:2044/hello
|
||||
proto.HttpRouter.Get("hello", func(h *proto.Http) {
|
||||
_, _ = h.WriteString("hello world")
|
||||
})
|
||||
|
||||
// curl -v --http3 --insecure https://localhost:2044/
|
||||
// curl --http3 --insecure https://localhost:2044/DCE
|
||||
proto.HttpRouter.Get("{target?}", func(h *proto.Http) {
|
||||
_, _ = h.WriteString("hello " + h.Param("target"))
|
||||
})
|
||||
|
||||
port := c.Rp.ArgOr("-p", "2044")
|
||||
fmt.Printf("Http3 server is starting on port %s\n", port)
|
||||
if err := http3.ListenAndServeQUIC(":"+port, "./attachs/cert/localhost.crt", "./attachs/cert/localhost.key", http.HandlerFunc(proto.HttpRouter.Route)); err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
}
|
168
_examples/apis/http.go
Normal file
168
_examples/apis/http.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package apis
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// go run . http start
|
||||
proto.CliRouter.Push("http/start", HttpStart)
|
||||
}
|
||||
|
||||
func HttpStart(c *proto.Cli) {
|
||||
proto.HttpRouter.
|
||||
Get("{var1}", var1).
|
||||
Get("{var1}/var3/{var3?}", var3).
|
||||
Get("var4/{var4*}", var4).
|
||||
Get("var5/var5/{var5+}", var5).
|
||||
Get("var6/var6/{var6}/var6", var6).
|
||||
Get("session/{username?}", sessionApi).
|
||||
Get("hello", hello).
|
||||
Post("hello", helloPost).
|
||||
PushApi(router.Api{
|
||||
Path: "home",
|
||||
Method: proto.HttpGet,
|
||||
Omission: true,
|
||||
}, home).
|
||||
Raw().SetEventHandler(func(ctx *router.Context[*proto.HttpProtocol]) error {
|
||||
if ctx.Api.Path == "session/{username?}" {
|
||||
if username := ctx.Param("username"); len(username) > 0 {
|
||||
ctx.Rp.SetCtxData("hello", "Inject from [BeforeController]")
|
||||
} else {
|
||||
return util.Openly(401, "Need to login")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
port := c.Rp.ArgOr("-p", "2046")
|
||||
fmt.Printf("Http server is starting on port %s\n", port)
|
||||
if err := http.ListenAndServe(":"+port, http.HandlerFunc(proto.HttpRouter.Route)); err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// curl http://127.0.0.1:2046/Drunk
|
||||
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"?>
|
||||
<greeting>
|
||||
<user>{{.User}}</user>
|
||||
<age>{{.Age}}</age>
|
||||
<welcome>{{.Welcome}}</welcome>
|
||||
</greeting>`)
|
||||
te.Response(Greeting{
|
||||
User: user,
|
||||
Age: 0,
|
||||
Welcome: "Welcome to DCE-GO",
|
||||
})
|
||||
}
|
||||
|
||||
// curl http://127.0.0.1:2046/var1/var3
|
||||
// curl http://127.0.0.1:2046/var1/var3/var3
|
||||
func var3(c *proto.Http) {
|
||||
var1 := c.Param("var1")
|
||||
var3 := c.Param("var3")
|
||||
fmt.Printf("var1: %s\nvar3: %s\n", var1, var3)
|
||||
_, _ = c.WriteString(fmt.Sprintf(`var1: %s(%d)<br/>var3: %s(%d)`, var1, len(var1), var3, len(var3)))
|
||||
}
|
||||
|
||||
// curl http://127.0.0.1:2046/var4
|
||||
// curl http://127.0.0.1:2046/var4/var4
|
||||
func var4(c *proto.Http) {
|
||||
var4 := c.Params("var4")
|
||||
_, _ = c.WriteString(fmt.Sprintf(`var4: %s(%d)`, var4, len(var4)))
|
||||
}
|
||||
|
||||
// curl http://127.0.0.1:2046/var5/var5/var5
|
||||
// curl http://127.0.0.1:2046/var5/var5/var5/var5
|
||||
func var5(c *proto.Http) {
|
||||
var5 := c.Params("var5")
|
||||
_, _ = c.WriteString(fmt.Sprintf(`var5: %s(%d)`, var5, len(var5)))
|
||||
}
|
||||
|
||||
// curl http://127.0.0.1:2046/var6/var6/var6/var6
|
||||
func var6(c *proto.Http) {
|
||||
var6 := c.Param("var6")
|
||||
_, _ = c.WriteString(fmt.Sprintf(`var6: %s(%d)`, var6, len(var6)))
|
||||
}
|
||||
|
||||
// curl http://127.0.0.1:2046/session/dce
|
||||
// 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)
|
||||
if username := c.Param("username"); username == "dce" {
|
||||
msg, _ := c.Rp.CtxData("hello")
|
||||
fmt.Println(msg)
|
||||
_, _ = c.WriteString(msg.(string))
|
||||
} else {
|
||||
fmt.Println(c.Rp.Body())
|
||||
t.Fail("invalid manager", 403)
|
||||
}
|
||||
}
|
||||
|
||||
// curl http://127.0.0.1:2046/hello
|
||||
func hello(c *proto.Http) {
|
||||
_, _ = c.WriteString(`request via get`)
|
||||
}
|
||||
|
||||
// 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()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// curl http://127.0.0.1:2046/
|
||||
func home(c *proto.Http) {
|
||||
jc := converter.JsonConverterNoParse[*proto.HttpProtocol, Greeting, GreetingResp](c)
|
||||
jc.Response(Greeting{
|
||||
User: "Dce",
|
||||
Age: 18,
|
||||
Welcome: "Welcome to Golang",
|
||||
})
|
||||
}
|
||||
|
||||
type Greeting struct {
|
||||
User string
|
||||
Age uint8
|
||||
Welcome string
|
||||
}
|
||||
|
||||
type GreetingReq struct {
|
||||
User string `json:"user"`
|
||||
Age uint8 `json:"age"`
|
||||
}
|
||||
|
||||
type GreetingResp struct {
|
||||
Welcome string `json:"welcome"`
|
||||
}
|
||||
|
||||
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{
|
||||
Welcome: g.Welcome,
|
||||
}, nil
|
||||
}
|
133
_examples/apis/json-tcp.go
Normal file
133
_examples/apis/json-tcp.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package apis
|
||||
|
||||
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"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func JsonTcpStart(c *proto.Cli) {
|
||||
jsonTcpBind()
|
||||
port := c.Rp.ArgOr("-p", "3048")
|
||||
|
||||
listener, err := net.Listen("tcp", ":"+port)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
fmt.Printf("JsonTcp server is started on port %s\n", port)
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
slog.Warn(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
for {
|
||||
if !json.TcpRouter.Route(conn, nil) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func jsonTcpBind() {
|
||||
// service apis
|
||||
|
||||
// go run . json-tcp 127.0.0.1:3048 -- hello
|
||||
json.TcpRouter.Push("hello", func(c *json.Tcp) {
|
||||
fmt.Printf("Api \"%s\": hello world\n", c.Api.Path)
|
||||
_, _ = c.WriteString("Hello world")
|
||||
})
|
||||
|
||||
// go run . json-tcp 127.0.0.1:3048 -- echo "echo me"
|
||||
json.TcpRouter.Push("echo/{param?}", func(c *json.Tcp) {
|
||||
param := c.Param("param")
|
||||
body, _ := c.Rp.Body()
|
||||
_, _ = c.WriteString(fmt.Sprintf("path param data: %s\nbody data: %s", param, string(body)))
|
||||
})
|
||||
}
|
||||
|
||||
func init() {
|
||||
// go run . json-tcp start
|
||||
proto.CliRouter.Push("json-tcp/start", JsonTcpStart)
|
||||
|
||||
// clients
|
||||
|
||||
// go run . json-tcp interactive 127.0.0.1:3048
|
||||
// and then type in some param
|
||||
proto.CliRouter.Push("json-tcp/interactive/{address}", func(c *proto.Cli) {
|
||||
addr := c.Param("address")
|
||||
dial, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer dial.Close()
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
fmt.Print("Param: ")
|
||||
param, _ := reader.ReadString('\n')
|
||||
param = strings.TrimSpace(param)
|
||||
|
||||
if strings.Compare("exit", param) == 0 {
|
||||
fmt.Println("exiting ...")
|
||||
break
|
||||
}
|
||||
path := "echo/" + param
|
||||
resp := jsonTcpRequest(dial, path)
|
||||
fmt.Printf("Got resp:\n%s(%d)\n", resp.Body, len(resp.Body))
|
||||
}
|
||||
})
|
||||
|
||||
proto.CliRouter.Push("json-tcp/{address}", func(c *proto.Cli) {
|
||||
addr := c.Param("address")
|
||||
if len(addr) == 0 {
|
||||
panic("not a valid address")
|
||||
}
|
||||
dial, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer dial.Close()
|
||||
passed := c.Rp.Passed
|
||||
if len(passed) == 0 {
|
||||
panic("passed args cannot be empty")
|
||||
}
|
||||
path := strings.Join(passed, router.MarkPathPartSeparator)
|
||||
resp := jsonTcpRequest(dial, path)
|
||||
fmt.Printf("Got resp:\n%s(%d)\n", resp.Body, len(resp.Body))
|
||||
})
|
||||
}
|
||||
|
||||
func jsonTcpRequest(dial net.Conn, path string) *json.Package {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(strconv.FormatUint(rand.Uint64(), 10)))
|
||||
content := []byte(fmt.Sprintf("Rand content「%X」", hash.Sum(nil)))
|
||||
if _, err := dial.Write(flex.StreamPack(json.NewPackage(path, content, "", -1).Serialize())); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
result, err := flex.StreamRead(dial)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
resp, err := json.PackageDeserialize(result)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
return resp
|
||||
}
|
133
_examples/apis/pb-tcp.go
Normal file
133
_examples/apis/pb-tcp.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package apis
|
||||
|
||||
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"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func PbTcpStart(c *proto.Cli) {
|
||||
pbTcpBind()
|
||||
port := c.Rp.ArgOr("-p", "4048")
|
||||
|
||||
listener, err := net.Listen("tcp", ":"+port)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
fmt.Printf("PbTcp server is started on port %s\n", port)
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
slog.Warn(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
for {
|
||||
if !pb.TcpRouter.Route(conn, nil) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func pbTcpBind() {
|
||||
// service apis
|
||||
|
||||
// go run . pb-tcp 127.0.0.1:4048 -- hello
|
||||
pb.TcpRouter.Push("hello", func(c *pb.Tcp) {
|
||||
fmt.Printf("Api \"%s\": hello world\n", c.Api.Path)
|
||||
_, _ = c.WriteString("Hello world")
|
||||
})
|
||||
|
||||
// go run . pb-tcp 127.0.0.1:4048 -- echo "echo me"
|
||||
pb.TcpRouter.Push("echo/{param?}", func(c *pb.Tcp) {
|
||||
param := c.Param("param")
|
||||
body, _ := c.Rp.Body()
|
||||
_, _ = c.WriteString(fmt.Sprintf("path param data: %s\nbody data: %s", param, string(body)))
|
||||
})
|
||||
}
|
||||
|
||||
func init() {
|
||||
// go run . pb-tcp start
|
||||
proto.CliRouter.Push("pb-tcp/start", PbTcpStart)
|
||||
|
||||
// clients
|
||||
|
||||
// go run . pb-tcp interactive 127.0.0.1:4048
|
||||
// and then type in some param
|
||||
proto.CliRouter.Push("pb-tcp/interactive/{address}", func(c *proto.Cli) {
|
||||
addr := c.Param("address")
|
||||
dial, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer dial.Close()
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
fmt.Print("Param: ")
|
||||
param, _ := reader.ReadString('\n')
|
||||
param = strings.TrimSpace(param)
|
||||
|
||||
if strings.Compare("exit", param) == 0 {
|
||||
fmt.Println("exiting ...")
|
||||
break
|
||||
}
|
||||
path := "echo/" + param
|
||||
resp := pbTcpRequest(dial, path)
|
||||
fmt.Printf("Got resp:\n%s(%d)\n", resp.Body, len(resp.Body))
|
||||
}
|
||||
})
|
||||
|
||||
proto.CliRouter.Push("pb-tcp/{address}", func(c *proto.Cli) {
|
||||
addr := c.Param("address")
|
||||
if len(addr) == 0 {
|
||||
panic("not a valid address")
|
||||
}
|
||||
dial, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer dial.Close()
|
||||
passed := c.Rp.Passed
|
||||
if len(passed) == 0 {
|
||||
panic("passed args cannot be empty")
|
||||
}
|
||||
path := strings.Join(passed, router.MarkPathPartSeparator)
|
||||
resp := pbTcpRequest(dial, path)
|
||||
fmt.Printf("Got resp:\n%s(%d)\n", resp.Body, len(resp.Body))
|
||||
})
|
||||
}
|
||||
|
||||
func pbTcpRequest(dial net.Conn, path string) *pb.Package {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(strconv.FormatUint(rand.Uint64(), 10)))
|
||||
content := []byte(fmt.Sprintf("Rand content「%X」", hash.Sum(nil)))
|
||||
if _, err := dial.Write(flex.StreamPack(pb.PackageSerialize(path, content, "", -1))); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
result, err := flex.StreamRead(dial)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
resp, err := pb.PackageDeserialize(result)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
return resp
|
||||
}
|
236
_examples/apis/session.go
Normal file
236
_examples/apis/session.go
Normal file
@@ -0,0 +1,236 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
proto.CliRouter.Push("http/start/session", HttpStartSession)
|
||||
}
|
||||
|
||||
func HttpStartSession(c *proto.Cli) {
|
||||
bind()
|
||||
_ = PrepareRedis(c.Rp.ArgOr("redis", ":6379"))
|
||||
port := c.Rp.ArgOr("port", "2050")
|
||||
fmt.Printf("http server is starting at :%s\n", port)
|
||||
if err := http.ListenAndServe(":"+port, http.HandlerFunc(proto.HttpRouter.Route)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func bind() {
|
||||
// curl http://127.0.0.1:2050/
|
||||
proto.HttpRouter.Get("", func(h *proto.Http) {
|
||||
_, _ = h.WriteString("This is a public page, you can access without a token")
|
||||
})
|
||||
|
||||
// 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()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
name, ok := u["name"].(string)
|
||||
if !ok && jc.Fail("Name required", 1000) {
|
||||
return
|
||||
}
|
||||
member, ok := util.SeqFrom(members()).Find(func(m Member) bool {
|
||||
return strings.ToLower(m.Name) == strings.ToLower(name)
|
||||
})
|
||||
if !ok && jc.Fail("Wrong name", 1001) {
|
||||
return
|
||||
}
|
||||
se := h.Rp.Session()
|
||||
if se == nil && jc.Fail("Invalid session", 999) {
|
||||
return
|
||||
}
|
||||
sess := se.(*redises.Session[*Member])
|
||||
if err := sess.Login(&member, session.DefaultTtlMinutes); err != nil && h.SetError(err) && jc.Fail("Failed to login", 1002) {
|
||||
return
|
||||
}
|
||||
h.Rp.SetRespSid(sess.Id())
|
||||
_, _ = h.WriteString(fmt.Sprintf("Succeed login with:\n%v", member))
|
||||
})
|
||||
|
||||
// curl http://127.0.0.1:2050/manage/profile // without sid, cannot access got 401
|
||||
// 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) {
|
||||
se := h.Rp.Session()
|
||||
if se == nil && h.SetError(util.Openly0("Invalid session")) {
|
||||
return
|
||||
}
|
||||
sess := se.(*redises.Session[*Member])
|
||||
member, _ := sess.User()
|
||||
_, _ = h.WriteString(fmt.Sprintf("Your profile:\n%v", member))
|
||||
})
|
||||
|
||||
// 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()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var newName string
|
||||
var newRoleId uint16
|
||||
if tNewName, ok := u["name"]; ok {
|
||||
newName = tNewName.(string)
|
||||
}
|
||||
if tNewRoleId, ok := u["role_id"]; ok {
|
||||
newRoleId = uint16(tNewRoleId.(float64))
|
||||
}
|
||||
if newName == "" && newRoleId == 0 && jc.Fail("Must specified something to modify", 1010) {
|
||||
return
|
||||
}
|
||||
se := h.Rp.Session()
|
||||
if se == nil && h.SetError(util.Openly0("Invalid session")) {
|
||||
return
|
||||
}
|
||||
sess := se.(*redises.Session[*Member])
|
||||
member, _ := sess.User()
|
||||
member.Name = newName
|
||||
member.RoleId = newRoleId
|
||||
if err := sess.Sync(&member); err != nil && h.SetError(err) {
|
||||
return
|
||||
}
|
||||
_, _ = h.WriteString(fmt.Sprintf("You have succeed to modified profile to:\n%v", member))
|
||||
})
|
||||
|
||||
// curl -I http://127.0.0.1:2050/manage/user -H "X-Session-Id: $session_id" // got 403 if the session user role is 1, you can use role 2 user login to access
|
||||
// curl http://127.0.0.1:2050/manage/user -H "X-Session-Id: $session_id"
|
||||
proto.HttpRouter.PushApi(
|
||||
router.Path("manage/user").ByMethod(proto.HttpGet|proto.HttpHead).Append("roles", 2),
|
||||
func(h *proto.Http) {
|
||||
se := h.Rp.Session()
|
||||
if se == nil && h.SetError(util.Openly0("Invalid session")) {
|
||||
return
|
||||
}
|
||||
sess := se.(*redises.Session[*Member])
|
||||
member, _ := sess.User()
|
||||
_, _ = h.WriteString(fmt.Sprintf("You are role %d, so you can access, your profile:\n%v", (*member).RoleId, member))
|
||||
},
|
||||
)
|
||||
|
||||
proto.HttpRouter.Raw().SetEventHandler(func(ctx *router.Context[*proto.HttpProtocol]) error {
|
||||
sess, err := redises.NewSession[*Member](Rdb, []string{ctx.Rp.Sid()}, session.DefaultTtlMinutes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
auto := session.NewAutoRenew(sess).Config(240, 0, 0)
|
||||
auth := NewAppAuth(ctx, auto)
|
||||
if err = auth.valid(); err != nil {
|
||||
return err
|
||||
} else if strings.Contains(ctx.Rp.Req.URL.RequestURI(), "autologin") {
|
||||
if err = auth.autoLogin(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !auth.isLogin() {
|
||||
if err = auth.tryRenew(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx.Rp.SetSession(sess)
|
||||
return nil
|
||||
}, func(ctx *router.Context[*proto.HttpProtocol]) error {
|
||||
if newSid := ctx.Rp.RespSid(); newSid != "" {
|
||||
_, _ = ctx.Rp.WriteString(fmt.Sprintf("\n\nGot new sid, you can use it to access private page:\n%s", newSid))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type AppAuth[Rp router.RoutableProtocol] struct {
|
||||
ctx *router.Context[Rp]
|
||||
session *session.AutoRenew[*redises.Session[*Member]]
|
||||
rolesNeeds []uint16
|
||||
}
|
||||
|
||||
func NewAppAuth[Rp router.RoutableProtocol](ctx *router.Context[Rp], session *session.AutoRenew[*redises.Session[*Member]]) *AppAuth[Rp] {
|
||||
var roles []uint16
|
||||
if rls := ctx.Api.ExtrasBy("roles"); rls != nil {
|
||||
roles = util.MapSeqFrom[any, uint16](rls).Map(func(a any) uint16 {
|
||||
return uint16(a.(int))
|
||||
}).Collect()
|
||||
}
|
||||
return &AppAuth[Rp]{ctx, session, roles}
|
||||
}
|
||||
|
||||
func (a *AppAuth[Rp]) isPrivate() bool {
|
||||
return len(a.rolesNeeds) > 0
|
||||
}
|
||||
|
||||
func (a *AppAuth[Rp]) isLogin() bool {
|
||||
return strings.HasSuffix(a.ctx.Api.Path, "login")
|
||||
}
|
||||
|
||||
func (a *AppAuth[Rp]) autoLogin() error {
|
||||
if err := a.session.S.AutoLogin(); err != nil {
|
||||
return err
|
||||
}
|
||||
a.ctx.Rp.SetRespSid(a.session.S.Id())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AppAuth[Rp]) tryRenew() error {
|
||||
if ok, err := a.session.TryRenew(); err != nil {
|
||||
return err
|
||||
} else if ok {
|
||||
a.ctx.Rp.SetRespSid(a.session.S.Id())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AppAuth[Rp]) valid() error {
|
||||
if a.isPrivate() {
|
||||
if user, ok := a.session.S.User(); !ok {
|
||||
return util.Openly(401, "Unauthorized")
|
||||
} else if !slices.Contains(a.rolesNeeds, user.RoleId) {
|
||||
return util.Openly(403, "Forbidden")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func members() []Member {
|
||||
return []Member{
|
||||
{Id: 1000, Name: "Drunk", RoleId: 1},
|
||||
{Id: 1001, Name: "Dce", RoleId: 2},
|
||||
{Id: 1002, Name: "Golang", RoleId: 2},
|
||||
}
|
||||
}
|
||||
|
||||
type Member struct {
|
||||
Id uint64
|
||||
Name string
|
||||
RoleId uint16
|
||||
}
|
||||
|
||||
func (m *Member) Uid() uint64 {
|
||||
return m.Id
|
||||
}
|
||||
|
||||
var Rdb *redis.Client
|
||||
|
||||
func PrepareRedis(addr string) *redis.Client {
|
||||
if Rdb != nil {
|
||||
return Rdb
|
||||
}
|
||||
Rdb = redis.NewClient(&redis.Options{Addr: addr})
|
||||
return Rdb
|
||||
}
|
18
_examples/attachs/cert/localhost.crt
Normal file
18
_examples/attachs/cert/localhost.crt
Normal file
@@ -0,0 +1,18 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC8zCCAdugAwIBAgIUGYuQm4W1ZGPHS75yd6nUElDMDfIwDQYJKoZIhvcNAQEL
|
||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDEwNzIwMTQwOVoXDTQ3MDYx
|
||||
MzIwMTQwOVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAQ8AMIIBCgKCAQEA2QHD7i3zB3J0ToQ03h2j1qwqkUp9aBMk+q1UDHc8A9hd
|
||||
qSCIsa/WbQ30QmFY0x3Xg/hQmCsBwYVE9RR+0ynKshR+5Wbx7+Yg4JANSzS/JzOI
|
||||
Ux0Ixx0V6LKxGpYZTpJbxcGh8uAOD2/9P4FmGD8MCu/ZhYEsqMWuT1AEu3JTV3P4
|
||||
bSN1MkW2TvqWXuPEliojOtJ9/46PR9Hq2ytf0ugqRRyDXTsnZQkxfjG6ll4eVcac
|
||||
0arrSRr66JJW80Yg7APiXkT7DqD39XHoo5AXqYXEdFRP4Ne9GTua65NYNs54SMlI
|
||||
TvrdVl38A+XMNSN8afHhNld+MKQncMcCMV9Ip9i9HwIDAQABoz0wOzAaBgNVHREE
|
||||
EzARgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0OBBYEFP8VP4Is5yq6ldDvAmEV+nAD
|
||||
Xl0JMA0GCSqGSIb3DQEBCwUAA4IBAQBCeDHTYMG0JGb9oc5L4OF5blUpcR+bu9Nm
|
||||
lD7nCLOUpKCazMo/6T+T33UOpX6IUADMWeMAXpIBp6bKhVRrn843Tz8qJahI3aBJ
|
||||
WZ5i4ntC1+kWRBxC2LyCRcBPRm9404Ft+f7i3JnGxCkSboD6uCq1i59/A6k91vzF
|
||||
DbJn04sU4R3AUH3QteSv9DULwSMxSmpl+rB40g72dJc5/NwIUUt0oM7GRAQk3GYC
|
||||
luZsspecMGlJzTiiA1LqmaCo3WoVOvf3Lcltbcni4rzmTdlexPm+8fwbtuVzuhIT
|
||||
kcXNhIF7JZP6woe0CY91//i15oHF0H6MHhqSDT882tZ0a7R1iVSk
|
||||
-----END CERTIFICATE-----
|
16
_examples/attachs/cert/localhost.csr
Normal file
16
_examples/attachs/cert/localhost.csr
Normal file
@@ -0,0 +1,16 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIChjCCAW4CAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEA2QHD7i3zB3J0ToQ03h2j1qwqkUp9aBMk+q1UDHc8
|
||||
A9hdqSCIsa/WbQ30QmFY0x3Xg/hQmCsBwYVE9RR+0ynKshR+5Wbx7+Yg4JANSzS/
|
||||
JzOIUx0Ixx0V6LKxGpYZTpJbxcGh8uAOD2/9P4FmGD8MCu/ZhYEsqMWuT1AEu3JT
|
||||
V3P4bSN1MkW2TvqWXuPEliojOtJ9/46PR9Hq2ytf0ugqRRyDXTsnZQkxfjG6ll4e
|
||||
Vcac0arrSRr66JJW80Yg7APiXkT7DqD39XHoo5AXqYXEdFRP4Ne9GTua65NYNs54
|
||||
SMlITvrdVl38A+XMNSN8afHhNld+MKQncMcCMV9Ip9i9HwIDAQABoC0wKwYJKoZI
|
||||
hvcNAQkOMR4wHDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcN
|
||||
AQELBQADggEBAFwKAnHLFQ2O0c718xmHtg78rr8LUssSrzL+BpFQcUdTfU5lxzEs
|
||||
bIlHnNgOeoTifKNy4IJn72qj74DzFDVgP7wGVA9vjL7SS5g3y8FAsP85rXBnQpk/
|
||||
wWv1uODNzCi9yUAdGMz6ssasOjO5xpQh4PWcd4VISxxl+0S/Hlwd6BvgxwigohTm
|
||||
PerPQCbb6EBI/yrY4f3FuHjn4CVyEn0HdVLs6hRfmaAFvHkb5ymZjXsd55+gL1QY
|
||||
L2eNRfWuwXv7/IjPaoOmWD2lz5jvE51Uq4NcdIRFquWuEIAklaSjosl4/7DCQFDx
|
||||
JTg2Yql+sGpOyh7OxB/cUk5h5cvY1YNBPaQ=
|
||||
-----END CERTIFICATE REQUEST-----
|
28
_examples/attachs/cert/localhost.key
Normal file
28
_examples/attachs/cert/localhost.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDZAcPuLfMHcnRO
|
||||
hDTeHaPWrCqRSn1oEyT6rVQMdzwD2F2pIIixr9ZtDfRCYVjTHdeD+FCYKwHBhUT1
|
||||
FH7TKcqyFH7lZvHv5iDgkA1LNL8nM4hTHQjHHRXosrEalhlOklvFwaHy4A4Pb/0/
|
||||
gWYYPwwK79mFgSyoxa5PUAS7clNXc/htI3UyRbZO+pZe48SWKiM60n3/jo9H0erb
|
||||
K1/S6CpFHINdOydlCTF+MbqWXh5VxpzRqutJGvroklbzRiDsA+JeRPsOoPf1ceij
|
||||
kBephcR0VE/g170ZO5rrk1g2znhIyUhO+t1WXfwD5cw1I3xp8eE2V34wpCdwxwIx
|
||||
X0in2L0fAgMBAAECggEAJjYdXg9RP/pzaG/3LzVg6CggxryrIGxekpV6u0czlim/
|
||||
NEy6RS+FPma6oAWsMmCK1n4fEuxfvsfMwKr/Oged3YWmYpah+3A2UdSLwrZjAnAc
|
||||
jSYLqaQBe5wbe2b6xc8Xwda6wZ2aXDWIDmqmo5ZWdIaPcuawCnfknaqlaqxS6I78
|
||||
AJ74wInZLUB4nAwXTZsl86vZ/DhR7rLutxnpyZ5l4EkK8wS1oZUJ9obmUqFxCbo2
|
||||
FyntOMxqkhLNrNspNhEuCR3yMWP4Jy+9AiKBtWM2+d9n734y0vsZdj8/joYtZ3KV
|
||||
B4rs131Ny7q2+qd/vfKbKZTaWo+wYuhrANXJ1LyhQQKBgQD627tw1D+9wsvvFU4L
|
||||
/yzk0iERfnAhhmtJA5YAu6RdpCnN/I4Puw2tOSPex4IlPlbkneIX1jSb3vw/fZ11
|
||||
ayrLjDgjqdhaFj27RJqdwqyQimpHeN8WhNb6IcgyY05i1RF/6esVA/gCM9S+874Q
|
||||
S+3ATVHJ1BZI9gfjrBbqeBAhmQKBgQDddGmsLPvn4qmCiisTgV9OOt2qcJAjmEwo
|
||||
aGKBtTVvfICGFKNRM7p78DRZxw5LAyH96tgUblaLyxYkPrak6OGERLIrcLcDUIVG
|
||||
y9joM5VmvBL8+9hMwPvpQTcCYbQsOIg12K8kSUy/IthKRk9zoKuiouviVtRvxP/x
|
||||
Jjro6XN3dwKBgQCZi7l1XFUPn5YX5yB4c15VSND41j1oJ9CvRkSgejonHv5/mKCT
|
||||
vFiouJreF5vvk/K3yHPFR1W4OoqHiinA3zG4mUEbgzBsI8TxRRKmkavyZOacjL1w
|
||||
GStEuzzAqswl7mjhtJoxqNY68uK7ZpHlg7QoyqrPMMPbMMyvbHwyU/77sQKBgQCM
|
||||
k5NpCn02K/oytYa2sQ9Q16lSwnWdQtZFaE3vzJoJFV14v60UpOOiPU7eFrAKCgkP
|
||||
6H4WKhyiTN7XT0Ad2v8dOYZocPqcDgcsc8ZTUDtspcLf+PbLck33OcCzsFXxJEnC
|
||||
9LPpMuaXBoWKUKuq2LlbWlSmrzvXX5Sg/gWzSE5V7QKBgHiUrys/EKCg4lCx7f3s
|
||||
xj4tQSUTZMDTOOIODZ/mD4zBsqH2IGn88BUxc8ClOpsv1J8EUW0S5qIq0izthPa1
|
||||
na/HjKIZwao3cGETxxvsL4g6r4fWtqdrTSRzjYtlQexpZhdzPq6eHirAXoP8SwZz
|
||||
sj7jdcqV6cWTZKxuft2YNST1
|
||||
-----END PRIVATE KEY-----
|
2
_examples/attachs/cert/openssl.cnf
Normal file
2
_examples/attachs/cert/openssl.cnf
Normal file
@@ -0,0 +1,2 @@
|
||||
[v3_req]
|
||||
subjectAltName=DNS:localhost,IP:127.0.0.1
|
278
_examples/attachs/report/ab-test-result.txt
Normal file
278
_examples/attachs/report/ab-test-result.txt
Normal file
@@ -0,0 +1,278 @@
|
||||
D:\Program\httpd-2.4.43-win64-VS16\Apache24\bin>ab -n 10000 -c 16 http://127.0.0.1:2046/hello
|
||||
This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
|
||||
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
||||
Licensed to The Apache Software Foundation, http://www.apache.org/
|
||||
|
||||
Benchmarking 127.0.0.1 (be patient)
|
||||
Completed 1000 requests
|
||||
Completed 2000 requests
|
||||
Completed 3000 requests
|
||||
Completed 4000 requests
|
||||
Completed 5000 requests
|
||||
Completed 6000 requests
|
||||
Completed 7000 requests
|
||||
Completed 8000 requests
|
||||
Completed 9000 requests
|
||||
Completed 10000 requests
|
||||
Finished 10000 requests
|
||||
|
||||
|
||||
Server Software:
|
||||
Server Hostname: 127.0.0.1
|
||||
Server Port: 2046
|
||||
|
||||
Document Path: /hello
|
||||
Document Length: 15 bytes
|
||||
|
||||
Concurrency Level: 16
|
||||
Time taken for tests: 3.486 seconds
|
||||
Complete requests: 10000
|
||||
Failed requests: 0
|
||||
Total transferred: 1320000 bytes
|
||||
HTML transferred: 150000 bytes
|
||||
Requests per second: 2868.78 [#/sec] (mean)
|
||||
Time per request: 5.577 [ms] (mean)
|
||||
Time per request: 0.349 [ms] (mean, across all concurrent requests)
|
||||
Transfer rate: 369.80 [Kbytes/sec] received
|
||||
|
||||
Connection Times (ms)
|
||||
min mean[+/-sd] median max
|
||||
Connect: 0 0 0.3 0 1
|
||||
Processing: 1 5 0.3 5 14
|
||||
Waiting: 0 3 1.5 3 10
|
||||
Total: 1 5 0.3 6 14
|
||||
ERROR: The median and mean for the total time are more than twice the standard
|
||||
deviation apart. These results are NOT reliable.
|
||||
|
||||
Percentage of the requests served within a certain time (ms)
|
||||
50% 6
|
||||
66% 6
|
||||
75% 6
|
||||
80% 6
|
||||
90% 6
|
||||
95% 6
|
||||
98% 6
|
||||
99% 6
|
||||
100% 14 (longest request)
|
||||
|
||||
D:\Program\httpd-2.4.43-win64-VS16\Apache24\bin>ab -n 10000 -c 16 http://127.0.0.1:8080/albums
|
||||
This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
|
||||
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
||||
Licensed to The Apache Software Foundation, http://www.apache.org/
|
||||
|
||||
Benchmarking 127.0.0.1 (be patient)
|
||||
Completed 1000 requests
|
||||
Completed 2000 requests
|
||||
Completed 3000 requests
|
||||
Completed 4000 requests
|
||||
Completed 5000 requests
|
||||
Completed 6000 requests
|
||||
Completed 7000 requests
|
||||
Completed 8000 requests
|
||||
Completed 9000 requests
|
||||
Completed 10000 requests
|
||||
Finished 10000 requests
|
||||
|
||||
|
||||
Server Software:
|
||||
Server Hostname: 127.0.0.1
|
||||
Server Port: 8080
|
||||
|
||||
Document Path: /albums
|
||||
Document Length: 382 bytes
|
||||
|
||||
Concurrency Level: 16
|
||||
Time taken for tests: 3.572 seconds
|
||||
Complete requests: 10000
|
||||
Failed requests: 0
|
||||
Total transferred: 5060000 bytes
|
||||
HTML transferred: 3820000 bytes
|
||||
Requests per second: 2799.67 [#/sec] (mean)
|
||||
Time per request: 5.715 [ms] (mean)
|
||||
Time per request: 0.357 [ms] (mean, across all concurrent requests)
|
||||
Transfer rate: 1383.43 [Kbytes/sec] received
|
||||
|
||||
Connection Times (ms)
|
||||
min mean[+/-sd] median max
|
||||
Connect: 0 0 0.3 0 1
|
||||
Processing: 1 5 0.4 5 21
|
||||
Waiting: 0 3 1.5 3 16
|
||||
Total: 1 6 0.4 6 22
|
||||
|
||||
Percentage of the requests served within a certain time (ms)
|
||||
50% 6
|
||||
66% 6
|
||||
75% 6
|
||||
80% 6
|
||||
90% 6
|
||||
95% 6
|
||||
98% 6
|
||||
99% 7
|
||||
100% 22 (longest request)
|
||||
|
||||
D:\Program\httpd-2.4.43-win64-VS16\Apache24\bin>ab -n 10000 -c 16 http://127.0.0.1:2046/hello
|
||||
This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
|
||||
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
||||
Licensed to The Apache Software Foundation, http://www.apache.org/
|
||||
|
||||
Benchmarking 127.0.0.1 (be patient)
|
||||
Completed 1000 requests
|
||||
Completed 2000 requests
|
||||
Completed 3000 requests
|
||||
Completed 4000 requests
|
||||
Completed 5000 requests
|
||||
Completed 6000 requests
|
||||
Completed 7000 requests
|
||||
Completed 8000 requests
|
||||
Completed 9000 requests
|
||||
Completed 10000 requests
|
||||
Finished 10000 requests
|
||||
|
||||
|
||||
Server Software:
|
||||
Server Hostname: 127.0.0.1
|
||||
Server Port: 2046
|
||||
|
||||
Document Path: /hello
|
||||
Document Length: 15 bytes
|
||||
|
||||
Concurrency Level: 16
|
||||
Time taken for tests: 3.442 seconds
|
||||
Complete requests: 10000
|
||||
Failed requests: 0
|
||||
Total transferred: 1320000 bytes
|
||||
HTML transferred: 150000 bytes
|
||||
Requests per second: 2905.50 [#/sec] (mean)
|
||||
Time per request: 5.507 [ms] (mean)
|
||||
Time per request: 0.344 [ms] (mean, across all concurrent requests)
|
||||
Transfer rate: 374.54 [Kbytes/sec] received
|
||||
|
||||
Connection Times (ms)
|
||||
min mean[+/-sd] median max
|
||||
Connect: 0 0 0.3 0 1
|
||||
Processing: 1 5 0.3 5 16
|
||||
Waiting: 1 3 1.4 3 11
|
||||
Total: 1 5 0.3 5 16
|
||||
|
||||
Percentage of the requests served within a certain time (ms)
|
||||
50% 5
|
||||
66% 6
|
||||
75% 6
|
||||
80% 6
|
||||
90% 6
|
||||
95% 6
|
||||
98% 6
|
||||
99% 6
|
||||
100% 16 (longest request)
|
||||
|
||||
D:\Program\httpd-2.4.43-win64-VS16\Apache24\bin>ab -n 10000 -c 16 http://127.0.0.1:2046/session/dce
|
||||
This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
|
||||
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
||||
Licensed to The Apache Software Foundation, http://www.apache.org/
|
||||
|
||||
Benchmarking 127.0.0.1 (be patient)
|
||||
Completed 1000 requests
|
||||
Completed 2000 requests
|
||||
Completed 3000 requests
|
||||
Completed 4000 requests
|
||||
Completed 5000 requests
|
||||
Completed 6000 requests
|
||||
Completed 7000 requests
|
||||
Completed 8000 requests
|
||||
Completed 9000 requests
|
||||
Completed 10000 requests
|
||||
Finished 10000 requests
|
||||
|
||||
|
||||
Server Software:
|
||||
Server Hostname: 127.0.0.1
|
||||
Server Port: 2046
|
||||
|
||||
Document Path: /session/dce
|
||||
Document Length: 30 bytes
|
||||
|
||||
Concurrency Level: 16
|
||||
Time taken for tests: 3.404 seconds
|
||||
Complete requests: 10000
|
||||
Failed requests: 0
|
||||
Total transferred: 1470000 bytes
|
||||
HTML transferred: 300000 bytes
|
||||
Requests per second: 2937.35 [#/sec] (mean)
|
||||
Time per request: 5.447 [ms] (mean)
|
||||
Time per request: 0.340 [ms] (mean, across all concurrent requests)
|
||||
Transfer rate: 421.67 [Kbytes/sec] received
|
||||
|
||||
Connection Times (ms)
|
||||
min mean[+/-sd] median max
|
||||
Connect: 0 0 0.3 0 1
|
||||
Processing: 1 5 0.4 5 11
|
||||
Waiting: 1 3 1.3 3 9
|
||||
Total: 1 5 0.4 5 11
|
||||
|
||||
Percentage of the requests served within a certain time (ms)
|
||||
50% 5
|
||||
66% 5
|
||||
75% 6
|
||||
80% 6
|
||||
90% 6
|
||||
95% 6
|
||||
98% 6
|
||||
99% 7
|
||||
100% 11 (longest request)
|
||||
|
||||
D:\Program\httpd-2.4.43-win64-VS16\Apache24\bin>ab -n 10000 -c 16 http://127.0.0.1:8080/album/2
|
||||
This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
|
||||
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
||||
Licensed to The Apache Software Foundation, http://www.apache.org/
|
||||
|
||||
Benchmarking 127.0.0.1 (be patient)
|
||||
Completed 1000 requests
|
||||
Completed 2000 requests
|
||||
Completed 3000 requests
|
||||
Completed 4000 requests
|
||||
Completed 5000 requests
|
||||
Completed 6000 requests
|
||||
Completed 7000 requests
|
||||
Completed 8000 requests
|
||||
Completed 9000 requests
|
||||
Completed 10000 requests
|
||||
Finished 10000 requests
|
||||
|
||||
|
||||
Server Software:
|
||||
Server Hostname: 127.0.0.1
|
||||
Server Port: 8080
|
||||
|
||||
Document Path: /album/2
|
||||
Document Length: 90 bytes
|
||||
|
||||
Concurrency Level: 16
|
||||
Time taken for tests: 3.506 seconds
|
||||
Complete requests: 10000
|
||||
Failed requests: 0
|
||||
Total transferred: 2130000 bytes
|
||||
HTML transferred: 900000 bytes
|
||||
Requests per second: 2851.91 [#/sec] (mean)
|
||||
Time per request: 5.610 [ms] (mean)
|
||||
Time per request: 0.351 [ms] (mean, across all concurrent requests)
|
||||
Transfer rate: 593.22 [Kbytes/sec] received
|
||||
|
||||
Connection Times (ms)
|
||||
min mean[+/-sd] median max
|
||||
Connect: 0 0 0.3 0 1
|
||||
Processing: 1 5 0.3 5 8
|
||||
Waiting: 1 4 1.3 4 8
|
||||
Total: 1 6 0.3 5 8
|
||||
ERROR: The median and mean for the total time are more than twice the standard
|
||||
deviation apart. These results are NOT reliable.
|
||||
|
||||
Percentage of the requests served within a certain time (ms)
|
||||
50% 5
|
||||
66% 6
|
||||
75% 6
|
||||
80% 6
|
||||
90% 6
|
||||
95% 6
|
||||
98% 6
|
||||
99% 6
|
||||
100% 8 (longest request)
|
3
_examples/go.mod
Normal file
3
_examples/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/idrunk/dce-go/_examples
|
||||
|
||||
go 1.23.3
|
11
_examples/main.go
Normal file
11
_examples/main.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/idrunk/dce-go/_examples/apis"
|
||||
"github.com/idrunk/dce-go/proto"
|
||||
)
|
||||
|
||||
func main() {
|
||||
apis.BindCli()
|
||||
proto.CliRoute(1)
|
||||
}
|
3
converter/go.mod
Normal file
3
converter/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/idrunk/dce-go/converter
|
||||
|
||||
go 1.23.3
|
104
converter/json.go
Normal file
104
converter/json.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"github.com/idrunk/dce-go/util"
|
||||
)
|
||||
|
||||
type JsonRequestProcessor[Rp router.RoutableProtocol, ReqDto, Req, Resp, RespDto any] struct {
|
||||
*router.Context[Rp]
|
||||
}
|
||||
|
||||
func (j *JsonRequestProcessor[Rp, ReqDto, Req, Resp, RespDto]) Serialize(dto RespDto) ([]byte, error) {
|
||||
return json.Marshal(dto)
|
||||
}
|
||||
|
||||
func (j *JsonRequestProcessor[Rp, ReqDto, Req, Resp, RespDto]) Deserialize(seq []byte) (ReqDto, error) {
|
||||
var obj ReqDto
|
||||
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
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
215
converter/template.go
Normal file
215
converter/template.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"github.com/idrunk/dce-go/util"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type TemplateEngine[Rp router.RoutableProtocol, Resp any] struct {
|
||||
*router.Context[Rp]
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func FileTemplate[Rp router.RoutableProtocol, Resp any](c *router.Context[Rp], tplPath string) TemplateEngine[Rp, Resp] {
|
||||
return TemplateEngine[Rp, Resp]{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 EmptyTemplate[Rp router.RoutableProtocol](c *router.Context[Rp]) TemplateEngine[Rp, router.DoNotConvert] {
|
||||
return TemplateEngine[Rp, router.DoNotConvert]{Context: c}
|
||||
}
|
||||
|
||||
func fileTemplate(tplPath string, key string) *template.Template {
|
||||
if len(key) == 0 {
|
||||
key = tplPath
|
||||
}
|
||||
return TplConfig.templateOrGen(key, func() *template.Template {
|
||||
tplPath = TplConfig.root() + tplPath
|
||||
if !fs.ValidPath(tplPath) {
|
||||
panic("invalid template path: " + tplPath)
|
||||
}
|
||||
return template.Must(template.ParseFiles(tplPath))
|
||||
})
|
||||
}
|
||||
|
||||
func textMd5(text string) string {
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(text))
|
||||
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
||||
|
||||
func textTemplate(text string, key string) *template.Template {
|
||||
if len(key) == 0 {
|
||||
key = textMd5(text)
|
||||
}
|
||||
return TplConfig.templateOrGen(key, func() *template.Template {
|
||||
tpl, err := template.New(key).Parse(text)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return tpl
|
||||
})
|
||||
}
|
||||
|
||||
func (t *TemplateEngine[Rp, Resp]) Serialize(resp Resp) ([]byte, error) {
|
||||
buff := new(bytes.Buffer)
|
||||
if err := t.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 {
|
||||
return fileTemplate(path, StatusTplId)
|
||||
} else {
|
||||
return textTemplate(text, StatusTplId)
|
||||
}
|
||||
} else if path, text := TplConfig.notfoundTpl(); len(path) > 0 {
|
||||
return fileTemplate(path, NotfoundTplId)
|
||||
} else {
|
||||
return textTemplate(text, NotfoundTplId)
|
||||
}
|
||||
}
|
||||
|
||||
type TemplateConfig struct{ *util.Config }
|
||||
|
||||
func (t *TemplateConfig) templateOrGen(key string, supplier func() *template.Template) *template.Template {
|
||||
if tpl, ok := t.Scalar(key); ok {
|
||||
return tpl.(*template.Template)
|
||||
}
|
||||
tpl := supplier()
|
||||
t.SetScalar(key, tpl)
|
||||
return tpl
|
||||
}
|
||||
|
||||
func (t *TemplateConfig) root() string {
|
||||
root, _ := t.Scalar("root_dir")
|
||||
return root.(string)
|
||||
}
|
||||
|
||||
func (t *TemplateConfig) SetRoot(root string) *TemplateConfig {
|
||||
t.SetScalar("root_dir", root)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *TemplateConfig) statusTpl() (path string, text string) {
|
||||
if path, _ := t.Scalar("status_path"); len(path.(string)) > 0 {
|
||||
return path.(string), ""
|
||||
} else {
|
||||
text, _ := t.Scalar("status_text")
|
||||
return "", text.(string)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TemplateConfig) SetStatusTpl(path string, text string) *TemplateConfig {
|
||||
t.SetScalar("status_path", path)
|
||||
t.SetScalar("status_text", text)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *TemplateConfig) notfoundTpl() (path string, text string) {
|
||||
if path, _ := t.Scalar("notfound_path"); len(path.(string)) > 0 {
|
||||
return path.(string), ""
|
||||
} else {
|
||||
text, _ := t.Scalar("notfound_text")
|
||||
return "", text.(string)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TemplateConfig) SetNotfoundTpl(path string, text string) *TemplateConfig {
|
||||
t.SetScalar("notfound_path", path)
|
||||
t.SetScalar("notfound_text", text)
|
||||
return t
|
||||
}
|
||||
|
||||
var TplConfig = TemplateConfig{util.NewConfig()}
|
||||
|
||||
const (
|
||||
StatusTplId = "status_tmpl_id_status"
|
||||
NotfoundTplId = "status_tmpl_id_notfound"
|
||||
)
|
||||
|
||||
func init() {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
root := filepath.Dir(exe)
|
||||
TplConfig.
|
||||
SetRoot(root+"/assets/templates/").
|
||||
SetNotfoundTpl("", `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Not found</title>
|
||||
</head>
|
||||
<body>
|
||||
Not found
|
||||
</body>
|
||||
</html>`).
|
||||
SetStatusTpl("", `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Exception ({{.Code}})</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Exception</h1>
|
||||
<p>{{.Code}}: {{.Msg}}</p>
|
||||
</body>
|
||||
</html>`)
|
||||
}
|
15
go.work
Normal file
15
go.work
Normal file
@@ -0,0 +1,15 @@
|
||||
go 1.23.3
|
||||
|
||||
use (
|
||||
./converter
|
||||
./_examples
|
||||
./_examples/apis
|
||||
./proto
|
||||
./proto/flex
|
||||
./proto/json
|
||||
./proto/pb
|
||||
./router
|
||||
./session
|
||||
./session/redises
|
||||
./util
|
||||
)
|
179
proto/cli.go
Normal file
179
proto/cli.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
MarkPassedSeparator = "--"
|
||||
MarkAssignment = "="
|
||||
MarkArgPrefix = "-"
|
||||
)
|
||||
|
||||
const (
|
||||
ArgTypeAssignExpr = iota + 1
|
||||
ArgTypePrefixName
|
||||
ArgTypePath
|
||||
ArgTypePassedSeparator
|
||||
)
|
||||
|
||||
type Cli = router.Context[*CliProtocol]
|
||||
|
||||
type CliProtocol struct {
|
||||
router.Meta[[]string]
|
||||
Passed []string
|
||||
path string
|
||||
args cliArgs
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func CliRoute(base int) {
|
||||
c := &CliProtocol{Meta: router.NewMeta(os.Args[base:], nil, true)}
|
||||
c.parse()
|
||||
ctx := router.NewContext(c)
|
||||
CliRouter.Route(ctx)
|
||||
c.TryPrintErr()
|
||||
if ctx.Api != nil && ctx.Api.Responsive {
|
||||
if sid := c.RespSid(); sid != "" {
|
||||
if _, err := c.WriteString("\n\nNew sid: " + sid); err != nil {
|
||||
println(err)
|
||||
}
|
||||
}
|
||||
resp := string(c.ClearBuffer()[:])
|
||||
fmt.Println(resp)
|
||||
}
|
||||
}
|
||||
|
||||
func parseType(arg string) (int, string, string) {
|
||||
if parts := strings.SplitN(arg, MarkAssignment, 2); len(parts) == 2 {
|
||||
return ArgTypeAssignExpr, parts[0], parts[1]
|
||||
} else if strings.HasPrefix(arg, MarkArgPrefix) {
|
||||
if arg == MarkPassedSeparator {
|
||||
return ArgTypePassedSeparator, "", ""
|
||||
}
|
||||
return ArgTypePrefixName, "", ""
|
||||
} else {
|
||||
return ArgTypePath, "", ""
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CliProtocol) parse() {
|
||||
var paths []string
|
||||
c.args = cliArgs{map[string]string{}, map[string][]string{}}
|
||||
Loop:
|
||||
for i := 0; i < len(c.Req); i++ {
|
||||
arg := c.Req[i]
|
||||
switch ty, left, right := parseType(arg); ty {
|
||||
case ArgTypeAssignExpr:
|
||||
c.args.setValue(left, right)
|
||||
case ArgTypePrefixName:
|
||||
if len(c.Req) > i {
|
||||
if ty, _, _ = parseType(c.Req[i+1]); ty == ArgTypePath {
|
||||
i++
|
||||
c.args.setValue(arg, c.Req[i])
|
||||
continue
|
||||
}
|
||||
}
|
||||
c.args.setValue(arg, "true")
|
||||
case ArgTypePassedSeparator:
|
||||
c.Passed = c.Req[i+1:]
|
||||
break Loop
|
||||
default:
|
||||
paths = append(paths, arg)
|
||||
}
|
||||
}
|
||||
c.path = strings.Join(paths, router.MarkPathPartSeparator)
|
||||
c.parseBody()
|
||||
}
|
||||
|
||||
func (c *CliProtocol) parseBody() {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
stat, _ := os.Stdin.Stat()
|
||||
if stat.Mode()&os.ModeCharDevice == 0 {
|
||||
read := make(chan byte, 1)
|
||||
go func() {
|
||||
var buf [1]byte
|
||||
if _, err := os.Stdin.Read(buf[:]); err == nil {
|
||||
read <- buf[0]
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case first := <-read:
|
||||
buffer.WriteByte(first)
|
||||
if bts, err := io.ReadAll(os.Stdin); err == nil {
|
||||
buffer.Write(bts)
|
||||
} else {
|
||||
slog.Debug(err.Error())
|
||||
}
|
||||
case <-time.After(32 * time.Millisecond):
|
||||
// sometimes (like run with goland), will enter this outer "if" logic incorrectly, and the program will be
|
||||
// blocked at the "Stdin.Read" forever, so we use this timeout logic to cancel the reading action
|
||||
_ = os.Stdin.Close()
|
||||
}
|
||||
}
|
||||
c.body = buffer
|
||||
}
|
||||
|
||||
func (c *CliProtocol) Path() string {
|
||||
return c.path
|
||||
}
|
||||
|
||||
func (c *CliProtocol) Body() ([]byte, error) {
|
||||
return io.ReadAll(c.body)
|
||||
}
|
||||
|
||||
func (c *CliProtocol) Bool(key string) bool {
|
||||
val := c.args.scalars[key]
|
||||
return val == "true" || val == "1"
|
||||
}
|
||||
|
||||
func (c *CliProtocol) Arg(key string) string {
|
||||
return c.args.scalars[key]
|
||||
}
|
||||
|
||||
func (c *CliProtocol) ArgOr(key string, def string) string {
|
||||
if v, ok := c.args.scalars[key]; ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func (c *CliProtocol) Args(key string) []string {
|
||||
return c.args.vectors[key]
|
||||
}
|
||||
|
||||
func (c *CliProtocol) Scalars() map[string]string {
|
||||
return c.args.scalars
|
||||
}
|
||||
|
||||
func (c *CliProtocol) Vectors() map[string][]string {
|
||||
return c.args.vectors
|
||||
}
|
||||
|
||||
type cliArgs struct {
|
||||
scalars map[string]string
|
||||
vectors map[string][]string
|
||||
}
|
||||
|
||||
func (c *cliArgs) setValue(key string, value string) {
|
||||
if vector, ok := c.vectors[key]; ok {
|
||||
delete(c.scalars, key)
|
||||
c.vectors[key] = append(vector, value)
|
||||
} else {
|
||||
c.scalars[key] = value
|
||||
c.vectors[key] = []string{value}
|
||||
}
|
||||
}
|
||||
|
||||
var CliRouter *router.Router[*CliProtocol]
|
||||
|
||||
func init() {
|
||||
CliRouter = router.ProtoRouter[*CliProtocol]("cli")
|
||||
}
|
64
proto/flex/doc.go
Normal file
64
proto/flex/doc.go
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Package flex
|
||||
|
||||
Package bit format routable protocol struct
|
||||
Protocol format:
|
||||
|
||||
0 1 . . .
|
||||
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 |
|
||||
|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 ... |
|
||||
| | | | |
|
||||
+-+-------------+-+-------------+-+-------------+-------------------------------+
|
||||
|
||||
First byte is bit flags, or be an empty package if not set.
|
||||
IDEN: with an request Id
|
||||
PATH: with a request Path
|
||||
SID: with a session Id
|
||||
CODE: with an error Code
|
||||
MSG: with error messages
|
||||
LOAD: with payload data
|
||||
NPAT: with a number Path
|
||||
|
||||
LEN of xxx FlexNum HEAD: The FlexNum HEAD of xxx's length
|
||||
xxx FlexNum HEAD: The FlexNum HEAD of xxx number
|
||||
same order FlexNum BODY: The FlexNum BODYs with same order to the heads
|
||||
PATH part: the api Path. (No this part if the NPAT is set)
|
||||
SID part: the session Id
|
||||
MESG part: the error Msg
|
||||
Payload part: the payload data
|
||||
|
||||
Definition of flexible length sequence numbers:
|
||||
|
||||
0 1 2 3 4 5 6
|
||||
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
|
||||
|0|S|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-
|
||||
|1|0|S|B|B|B|B|B|B|B|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-
|
||||
|1|1|0|S|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-
|
||||
|1|1|1|0|S|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-
|
||||
|1|1|1|1|0|S|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-
|
||||
|1|1|1|1|1|0|S|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|-|-|-|-|-|-|-|-
|
||||
|1|1|1|1|1|1|0|S|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B
|
||||
|1|1|1|1|1|1|1|0|S| Reserve for int128, max 1+8+8=17 bytes. S: negative sign
|
||||
|1|1|1|1|1|1|1|1| Reserve for other situations. B: binary number
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
|
||||
7 8 9 10 11 12 13
|
||||
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|
||||
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|
||||
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|
||||
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|
||||
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|
||||
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|
||||
|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
*/
|
||||
package flex
|
391
proto/flex/flex.go
Normal file
391
proto/flex/flex.go
Normal file
@@ -0,0 +1,391 @@
|
||||
package flex
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"github.com/idrunk/dce-go/util"
|
||||
"io"
|
||||
"math"
|
||||
"math/bits"
|
||||
"net"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type PackageProtocol[Req any] struct {
|
||||
router.Meta[Req]
|
||||
pkg *Package
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) Id() uint32 {
|
||||
return p.pkg.Id
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) Path() string {
|
||||
return p.pkg.Path
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) Sid() string {
|
||||
return p.pkg.Sid
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) Body() ([]byte, error) {
|
||||
return p.pkg.parseBody()
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) ClearBuffer() []byte {
|
||||
p.pkg.Sid = p.RespSid()
|
||||
p.pkg.Body = p.Meta.ClearBuffer()
|
||||
code, message := p.ErrorUnits()
|
||||
p.pkg.Code, p.pkg.Message = int32(code), message
|
||||
return p.pkg.Serialize()
|
||||
}
|
||||
|
||||
func NewPackageProtocol[Req any](reader *bufio.Reader, req Req, ctxData map[string]any) (*PackageProtocol[Req], error) {
|
||||
return NewPackageProtocolWithMeta(reader, router.NewMeta(req, ctxData, true))
|
||||
}
|
||||
|
||||
func NewPackageProtocolWithMeta[Req any](reader *bufio.Reader, meta router.Meta[Req]) (*PackageProtocol[Req], error) {
|
||||
pkg, err := PackageDeserializeHead(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PackageProtocol[Req]{meta, pkg}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
flagId uint8 = 128 >> iota
|
||||
flagPath
|
||||
flagSid
|
||||
flagCode
|
||||
flagMsg
|
||||
flagBody
|
||||
flagNumPath
|
||||
)
|
||||
|
||||
type Package struct {
|
||||
Id uint32
|
||||
Path string
|
||||
NumPath uint32
|
||||
Sid string
|
||||
Code int32
|
||||
Message string
|
||||
Body []byte
|
||||
bodyLen uint64
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
func (p *Package) Serialize() []byte {
|
||||
// Init a seq buffer and set flag byte to 0
|
||||
buffer := make([]byte, 1, 512)
|
||||
textBuffer := make([][]byte, 0, 4)
|
||||
lenSeqInfoVec := make([]util.Tuple4[byte, int, int, uint], 0, 8)
|
||||
// Fill protocol headFlags and pack FlexNum head, cache text contents
|
||||
if length := len(p.Path); length > 0 {
|
||||
buffer[0] |= flagPath
|
||||
lenSeqInfoVec = append(lenSeqInfoVec, util.NewTuple4(Non0LenPackHead(uint16(length))))
|
||||
textBuffer = append(textBuffer, []byte(p.Path))
|
||||
}
|
||||
if length := len(p.Sid); length > 0 {
|
||||
buffer[0] |= flagSid
|
||||
lenSeqInfoVec = append(lenSeqInfoVec, util.NewTuple4(Non0LenPackHead(uint16(length))))
|
||||
textBuffer = append(textBuffer, []byte(p.Sid))
|
||||
}
|
||||
if length := len(p.Message); length > 0 {
|
||||
buffer[0] |= flagMsg
|
||||
lenSeqInfoVec = append(lenSeqInfoVec, util.NewTuple4(Non0LenPackHead(uint16(length))))
|
||||
textBuffer = append(textBuffer, []byte(p.Message))
|
||||
}
|
||||
if length := len(p.Body); length > 0 {
|
||||
buffer[0] |= flagBody
|
||||
lenSeqInfoVec = append(lenSeqInfoVec, util.NewTuple4(Non0LenPackHead(uint(length))))
|
||||
textBuffer = append(textBuffer, p.Body)
|
||||
}
|
||||
if p.Id > 0 {
|
||||
buffer[0] |= flagId
|
||||
lenSeqInfoVec = append(lenSeqInfoVec, util.NewTuple4(Non0LenPackHead(p.Id)))
|
||||
}
|
||||
if p.Code != 0 {
|
||||
buffer[0] |= flagCode
|
||||
lenSeqInfoVec = append(lenSeqInfoVec, util.NewTuple4(IntPackHead(p.Code)))
|
||||
}
|
||||
if p.NumPath > 0 {
|
||||
buffer[0] |= flagNumPath
|
||||
lenSeqInfoVec = append(lenSeqInfoVec, util.NewTuple4(Non0LenPackHead(p.NumPath)))
|
||||
}
|
||||
// Fill FlexNum heads and pack and fill FlexNum body
|
||||
buffer = buffer[:1+len(lenSeqInfoVec)]
|
||||
for i, lenSeqInfo := range lenSeqInfoVec {
|
||||
buffer[1+i] = lenSeqInfo.A
|
||||
buffer = append(buffer, NumPackBody(lenSeqInfo.D, lenSeqInfo.C, lenSeqInfo.B)...)
|
||||
}
|
||||
// Append the text contents
|
||||
for _, part := range textBuffer {
|
||||
buffer = append(buffer, part...)
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
func (p *Package) parseBody() ([]byte, error) {
|
||||
body := make([]byte, p.bodyLen)
|
||||
if _, err := io.ReadFull(p.reader, body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func PackageDeserializeHead(reader *bufio.Reader) (*Package, error) {
|
||||
// Try read flag
|
||||
flag, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Count the FlexNum and init a numHead bytes container
|
||||
var numHeadSeq = make([]byte, bits.OnesCount8(flag))
|
||||
if _, err = reader.Read(numHeadSeq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var numInfoSeq = make([]util.Tuple5[byte, uint8, bool, byte, []byte], len(numHeadSeq))
|
||||
// Parse the FlexNum head and total the FlexNum body len
|
||||
for i, numHead := range numHeadSeq {
|
||||
unsignedBits, bytesLen, negative, originalBits := NumParseHead(numHead, true)
|
||||
// Read FlexNum bodies
|
||||
var numBodySeq = make([]byte, bytesLen)
|
||||
if _, err = reader.Read(numBodySeq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
numInfoSeq[i] = util.NewTuple5(unsignedBits, bytesLen, negative, originalBits, numBodySeq)
|
||||
}
|
||||
pkg := Package{reader: reader}
|
||||
// Finally number parse and read text seq
|
||||
if flag&flagPath > 0 {
|
||||
numInfo, _ := util.SliceDeleteGet(&numInfoSeq, 0, 1)
|
||||
seq := make([]byte, Non0LenParse(numInfo[0].D, numInfo[0].E))
|
||||
if _, err = io.ReadFull(reader, seq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkg.Path = string(seq)
|
||||
}
|
||||
if flag&flagSid > 0 {
|
||||
numInfo, _ := util.SliceDeleteGet(&numInfoSeq, 0, 1)
|
||||
seq := make([]byte, Non0LenParse(numInfo[0].D, numInfo[0].E))
|
||||
if _, err = io.ReadFull(reader, seq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkg.Sid = string(seq)
|
||||
}
|
||||
if flag&flagMsg > 0 {
|
||||
numInfo, _ := util.SliceDeleteGet(&numInfoSeq, 0, 1)
|
||||
seq := make([]byte, Non0LenParse(numInfo[0].D, numInfo[0].E))
|
||||
if _, err = io.ReadFull(reader, seq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkg.Message = string(seq)
|
||||
}
|
||||
if flag&flagBody > 0 {
|
||||
numInfo, _ := util.SliceDeleteGet(&numInfoSeq, 0, 1)
|
||||
pkg.bodyLen = Non0LenParse(numInfo[0].D, numInfo[0].E)
|
||||
}
|
||||
if flag&flagId > 0 {
|
||||
numInfo, _ := util.SliceDeleteGet(&numInfoSeq, 0, 1)
|
||||
pkg.Id = uint32(Non0LenParse(numInfo[0].D, numInfo[0].E))
|
||||
}
|
||||
if flag&flagCode > 0 {
|
||||
numInfo, _ := util.SliceDeleteGet(&numInfoSeq, 0, 1)
|
||||
pkg.Code = IntParse[int32](numInfo[0].C, numInfo[0].A, numInfo[0].E)
|
||||
}
|
||||
if flag&flagNumPath > 0 {
|
||||
numInfo, _ := util.SliceDeleteGet(&numInfoSeq, 0, 1)
|
||||
pkg.NumPath = uint32(Non0LenParse(numInfo[0].D, numInfo[0].E))
|
||||
}
|
||||
return &pkg, nil
|
||||
}
|
||||
|
||||
func PackageDeserialize(reader *bufio.Reader) (*Package, error) {
|
||||
sp, err := PackageDeserializeHead(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if b, e := sp.parseBody(); e != nil {
|
||||
return nil, e
|
||||
} else {
|
||||
sp.Body = b
|
||||
return sp, nil
|
||||
}
|
||||
}
|
||||
|
||||
var reqId atomic.Uint32
|
||||
|
||||
func NewPackage(path string, body []byte, sid string, id int) *Package {
|
||||
return NewNumPackage(0, body, sid, id, path)
|
||||
}
|
||||
|
||||
func NewNumPackage(numPath uint32, body []byte, sid string, id int, path string) *Package {
|
||||
if id == -1 {
|
||||
reqId.Add(1)
|
||||
if id = int(reqId.Load()); id == math.MaxUint32 {
|
||||
reqId.Store(0)
|
||||
}
|
||||
}
|
||||
return &Package{Id: uint32(id), Path: path, NumPath: numPath, Sid: sid, Body: body}
|
||||
}
|
||||
|
||||
func UintSerialize[U uint | uint64 | uint32 | uint16 | uint8](unsigned U) []byte {
|
||||
return numSerialize(UintPackHead(unsigned))
|
||||
}
|
||||
|
||||
func IntSerialize[I int | int64 | int32 | int16 | int8](integer I) []byte {
|
||||
return numSerialize(IntPackHead(integer))
|
||||
}
|
||||
|
||||
// Non0LenPackHead can package 128 into uint7 to represent the length of sha512 in hexadecimal
|
||||
func Non0LenPackHead[U uint | uint64 | uint32 | uint16 | uint8](unsigned U) (head byte, bytesLen int, bitsLen int, usize uint) {
|
||||
return UintPackHead(unsigned - 1)
|
||||
}
|
||||
|
||||
func UintPackHead[U uint | uint64 | uint32 | uint16 | uint8](unsigned U) (head byte, bytesLen int, bitsLen int, usize uint) {
|
||||
usize = uint(unsigned)
|
||||
bitsLen = bits.Len(usize)
|
||||
head, bytesLen = numPackHead(usize, bitsLen)
|
||||
return head, bytesLen, bitsLen, usize
|
||||
}
|
||||
|
||||
func IntPackHead[I int | int64 | int32 | int16 | int8](integer I) (byte, int, int, uint) {
|
||||
var unsigned uint
|
||||
if integer < 0 {
|
||||
// use -int to resolve the edge case, eg. int8(-128) to int8(128) is illegal, but to int(128) is legal.
|
||||
// but the int(math.MinInt) to -int(math.MinInt) may still illegal, but it is unlikely to be used.
|
||||
unsigned = uint(-int(integer))
|
||||
} else {
|
||||
unsigned = uint(integer)
|
||||
}
|
||||
// add the width of the sign
|
||||
bitsLen := bits.Len(unsigned) + 1
|
||||
head, bytesLen := numPackHead(unsigned, bitsLen)
|
||||
if integer < 0 {
|
||||
var negative uint8 = 1
|
||||
if bytesLen < 7 {
|
||||
negative = 1 << (6 - bytesLen)
|
||||
}
|
||||
// handle the negative situation to mark the sign bit
|
||||
head |= negative
|
||||
}
|
||||
return head, bytesLen, bitsLen, unsigned
|
||||
}
|
||||
|
||||
func numPackHead[U uint | uint64 | uint32 | uint16 | uint8](u64 U, bitsLen int) (byte, int) {
|
||||
bytesLen := int(math.Floor(float64(bitsLen) / 8))
|
||||
headMaskShift := 8 - bytesLen
|
||||
var headBits uint8
|
||||
if bytesLen > 5 {
|
||||
// Directly alloc 8bytes if the requirement greater than 5
|
||||
bytesLen = 8
|
||||
headMaskShift = 2
|
||||
} else if bitsLen%8 > 7-bytesLen {
|
||||
// If the remains bits width than the headBits, then need to increase the bytes length,
|
||||
// and decrease the maskShift to match the body byte length.
|
||||
// When bytesLen updated, the head no longer needs to be stored any bits, just need to keep the default
|
||||
bytesLen++
|
||||
headMaskShift--
|
||||
} else {
|
||||
// Otherwise, right shift the bodyBytesLen to calc out the headBits
|
||||
headBits |= uint8(u64 >> (bytesLen * 8))
|
||||
}
|
||||
return 255<<headMaskShift&255 | headBits, bytesLen
|
||||
}
|
||||
|
||||
func numSerialize[U uint | uint64 | uint32 | uint16 | uint8](head uint8, bytesLen int, bitsLen int, u64 U) []byte {
|
||||
units := make([]byte, bytesLen+1)
|
||||
units[0] = head
|
||||
copy(units[1:], NumPackBody(u64, bitsLen, bytesLen))
|
||||
return units
|
||||
}
|
||||
|
||||
func NumPackBody[U uint | uint64 | uint32 | uint16 | uint8](u64 U, bitsLen int, bytesLen int) []byte {
|
||||
units := make([]byte, bytesLen)
|
||||
for i := 0; i < bytesLen && i*8 < bitsLen; i++ {
|
||||
units[bytesLen-i-1] = uint8(u64 >> (i * 8) & 255)
|
||||
}
|
||||
return units
|
||||
}
|
||||
|
||||
func UintDeserialize[U uint | uint64 | uint32 | uint16 | uint8](seq []byte) U {
|
||||
seq[0], _, _, _ = NumParseHead(seq[0], false)
|
||||
return U(NumParse(seq))
|
||||
}
|
||||
|
||||
func IntDeserialize[I int | int64 | int32 | int16 | int8](seq []byte) I {
|
||||
headBits, _, negative, _ := NumParseHead(seq[0], true)
|
||||
return IntParse[I](negative, headBits, seq[1:])
|
||||
}
|
||||
|
||||
func NumParseHead(head byte, sign bool) (unsignedBits byte, bytesLen uint8, negative bool, originalBits byte) {
|
||||
for i := 0; i < 8; i++ {
|
||||
if 128>>i&head == 0 {
|
||||
if bytesLen = uint8(i); bytesLen > 5 {
|
||||
bytesLen = 8
|
||||
originalBits = 1 & head
|
||||
} else {
|
||||
originalBits = 127 >> bytesLen & head
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
unsignedBits = originalBits
|
||||
if sign {
|
||||
if bytesLen == 8 {
|
||||
negative = 1&head == 1
|
||||
} else {
|
||||
signShift := 0
|
||||
if negative = 64>>bytesLen&head > 0; negative {
|
||||
signShift = 1
|
||||
}
|
||||
unsignedBits = 127 >> bytesLen >> signShift & head
|
||||
}
|
||||
}
|
||||
return unsignedBits, bytesLen, negative, originalBits
|
||||
}
|
||||
|
||||
func IntParse[I int | int64 | int32 | int16 | int8](negative bool, head uint8, seq []byte) I {
|
||||
u64 := NumParse(slices.Insert(seq, 0, head))
|
||||
if negative {
|
||||
return I(-int64(u64))
|
||||
}
|
||||
return I(u64)
|
||||
}
|
||||
|
||||
func Non0LenParse(head uint8, seq []byte) uint64 {
|
||||
return NumParse(slices.Insert(seq, 0, head)) + 1
|
||||
}
|
||||
|
||||
func NumParse(seq []byte) uint64 {
|
||||
var u64 uint64 = 0
|
||||
for i, b := range seq {
|
||||
if b > 0 {
|
||||
u64 |= uint64(b) << ((len(seq) - i - 1) * 8)
|
||||
}
|
||||
}
|
||||
return u64
|
||||
}
|
||||
|
||||
func StreamRead(conn net.Conn) ([]byte, error) {
|
||||
reader := bufio.NewReader(conn)
|
||||
numHead, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, bytesLen, _, headBits := NumParseHead(numHead, false)
|
||||
numBody := make([]byte, bytesLen)
|
||||
if _, err = reader.Read(numBody); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var data = make([]byte, Non0LenParse(headBits, numBody))
|
||||
if _, err = io.ReadFull(reader, data); err != nil {
|
||||
return data, err
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
func StreamPack(bytes []byte) []byte {
|
||||
head, bytesLen, bitsLen, usize := Non0LenPackHead(uint(len(bytes)))
|
||||
return slices.Insert(bytes, 0, slices.Insert(NumPackBody(usize, bitsLen, bytesLen), 0, head)...)
|
||||
}
|
234
proto/flex/flex_test.go
Normal file
234
proto/flex/flex_test.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package flex
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"math"
|
||||
"math/bits"
|
||||
"math/rand/v2"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPackage_Serialize(t *testing.T) {
|
||||
Body := []byte(strings.Repeat("Hello world!你好,世界!", rand.IntN(1000)))
|
||||
hash := md5.New()
|
||||
hash.Write(Body)
|
||||
srcHash := hex.EncodeToString(hash.Sum(nil))
|
||||
t.Logf("Source content hash: %s", srcHash)
|
||||
pkg := NewPackage("home", Body, srcHash, -1)
|
||||
seq := pkg.Serialize()
|
||||
dePkg, _ := PackageDeserialize(bufio.NewReader(bytes.NewReader(seq)))
|
||||
hash2 := md5.New()
|
||||
hash2.Write(dePkg.Body)
|
||||
handledHash := hex.EncodeToString(hash2.Sum(nil))
|
||||
t.Logf("Handled content hash: %s", handledHash)
|
||||
if srcHash != handledHash {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlexNumSerialize(t *testing.T) {
|
||||
for _, n := range []any{
|
||||
uint8(127),
|
||||
uint8(128),
|
||||
uint8(129),
|
||||
uint8(255),
|
||||
uint16(256),
|
||||
uint16(257),
|
||||
uint16(16383),
|
||||
uint16(16384),
|
||||
uint16(16385),
|
||||
uint64(999999999999999999),
|
||||
|
||||
int8(63),
|
||||
int8(64),
|
||||
int8(65),
|
||||
int8(127),
|
||||
int16(128),
|
||||
int16(129),
|
||||
int16(16383),
|
||||
int16(16384),
|
||||
int16(16385),
|
||||
int64(999999999999999999),
|
||||
|
||||
int8(-63),
|
||||
int8(-64),
|
||||
int8(-65),
|
||||
int8(-127),
|
||||
int8(-128),
|
||||
int16(-129),
|
||||
int16(-16383),
|
||||
int16(-16384),
|
||||
int16(-16385),
|
||||
int64(-999999999999999999),
|
||||
} {
|
||||
testFlexNumSerialize(t, n)
|
||||
}
|
||||
}
|
||||
|
||||
func testFlexNumSerialize(t *testing.T, num any) {
|
||||
var val any
|
||||
switch n := num.(type) {
|
||||
case uint:
|
||||
val = UintDeserialize[uint](UintSerialize(n))
|
||||
case uint64:
|
||||
val = UintDeserialize[uint64](UintSerialize(n))
|
||||
case uint32:
|
||||
val = UintDeserialize[uint32](UintSerialize(n))
|
||||
case uint16:
|
||||
val = UintDeserialize[uint16](UintSerialize(n))
|
||||
case uint8:
|
||||
val = UintDeserialize[uint8](UintSerialize(n))
|
||||
case int:
|
||||
val = IntDeserialize[int](IntSerialize(n))
|
||||
case int64:
|
||||
val = IntDeserialize[int64](IntSerialize(n))
|
||||
case int32:
|
||||
val = IntDeserialize[int32](IntSerialize(n))
|
||||
case int16:
|
||||
val = IntDeserialize[int16](IntSerialize(n))
|
||||
case int8:
|
||||
val = IntDeserialize[int8](IntSerialize(n))
|
||||
}
|
||||
if val != num {
|
||||
t.Errorf("Deserialized num \"%d\" was not eq to \"%d\".", val, num)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBits7NumSerialize(t *testing.T) {
|
||||
for _, n := range []any{
|
||||
uint8(127),
|
||||
uint8(128),
|
||||
uint8(129),
|
||||
uint8(255),
|
||||
uint16(256),
|
||||
uint16(257),
|
||||
uint16(16383),
|
||||
uint16(16384),
|
||||
uint16(16385),
|
||||
uint64(999999999999999999),
|
||||
|
||||
int8(63),
|
||||
int8(64),
|
||||
int8(65),
|
||||
int8(127),
|
||||
int16(128),
|
||||
int16(129),
|
||||
int16(16383),
|
||||
int16(16384),
|
||||
int16(16385),
|
||||
int64(99999999999999),
|
||||
|
||||
int8(-63),
|
||||
int8(-64),
|
||||
int8(-65),
|
||||
int8(-127),
|
||||
int8(-128),
|
||||
int16(-129),
|
||||
int16(-16383),
|
||||
int16(-16384),
|
||||
int16(-16385),
|
||||
int64(-99999999999999),
|
||||
} {
|
||||
testBits7NumSerialize(t, n)
|
||||
}
|
||||
}
|
||||
|
||||
func testBits7NumSerialize(t *testing.T, num any) {
|
||||
var val any
|
||||
switch n := num.(type) {
|
||||
case uint:
|
||||
val = Bits7UintDeserialize[uint](Bits7UintSerialize(n))
|
||||
case uint64:
|
||||
val = Bits7UintDeserialize[uint64](Bits7UintSerialize(n))
|
||||
case uint32:
|
||||
val = Bits7UintDeserialize[uint32](Bits7UintSerialize(n))
|
||||
case uint16:
|
||||
val = Bits7UintDeserialize[uint16](Bits7UintSerialize(n))
|
||||
case uint8:
|
||||
val = Bits7UintDeserialize[uint8](Bits7UintSerialize(n))
|
||||
case int:
|
||||
val = Bits7IntDeserialize[int](Bits7IntSerialize(n))
|
||||
case int64:
|
||||
val = Bits7IntDeserialize[int64](Bits7IntSerialize(n))
|
||||
case int32:
|
||||
val = Bits7IntDeserialize[int32](Bits7IntSerialize(n))
|
||||
case int16:
|
||||
val = Bits7IntDeserialize[int16](Bits7IntSerialize(n))
|
||||
case int8:
|
||||
val = Bits7IntDeserialize[int8](Bits7IntSerialize(n))
|
||||
}
|
||||
if val != num {
|
||||
t.Errorf("Deserialized num \"%d\" was not eq to \"%d\".", num, val)
|
||||
}
|
||||
}
|
||||
|
||||
func Bits7UintDeserialize[U uint | uint64 | uint32 | uint16 | uint8](seq []byte) U {
|
||||
return U(bits7NumDeserialize(seq, 0))
|
||||
}
|
||||
|
||||
func Bits7IntDeserialize[I int | int64 | int32 | int16 | int8](seq []byte) I {
|
||||
u64 := bits7NumDeserialize(seq, 1)
|
||||
var negativeSign uint8 = 64
|
||||
if len(seq) == 9 {
|
||||
negativeSign = 128
|
||||
}
|
||||
if seq[len(seq)-1]&negativeSign != 0 {
|
||||
return I(-u64)
|
||||
}
|
||||
return I(u64)
|
||||
}
|
||||
|
||||
func bits7NumDeserialize(seq []byte, highMarkShift int) uint64 {
|
||||
var u64 uint64 = 0
|
||||
for i, lastIndex := 0, len(seq)-1; i <= lastIndex; i++ {
|
||||
if i != lastIndex {
|
||||
u64 |= uint64(seq[i]&127) << (i * 7)
|
||||
} else if i == 8 {
|
||||
u64 |= uint64(seq[i]&(255>>highMarkShift)) << (i * 7)
|
||||
} else {
|
||||
u64 |= uint64(seq[i]&(127>>highMarkShift)) << (i * 7)
|
||||
}
|
||||
}
|
||||
return u64
|
||||
}
|
||||
|
||||
func Bits7UintSerialize[U uint | uint64 | uint32 | uint16 | uint8](unsigned U) []byte {
|
||||
return bits7NumSerialize(unsigned, bits.Len(uint(unsigned)))
|
||||
}
|
||||
|
||||
func Bits7IntSerialize[I int | int64 | int32 | int16 | int8](integer I) []byte {
|
||||
var mask uint8 = 0
|
||||
u64 := uint64(integer)
|
||||
if integer < 0 {
|
||||
mask = 64
|
||||
u64 = uint64(-integer)
|
||||
}
|
||||
bit7Units := bits7NumSerialize(u64, bits.Len(uint(u64))+1)
|
||||
if mask > 0 {
|
||||
lastIndex := len(bit7Units) - 1
|
||||
if lastIndex == 8 {
|
||||
mask = 128
|
||||
}
|
||||
bit7Units[lastIndex] |= mask
|
||||
}
|
||||
return bit7Units
|
||||
}
|
||||
|
||||
func bits7NumSerialize[U uint | uint64 | uint32 | uint16 | uint8](u64 U, bitsLen int) []byte {
|
||||
bit7Units := make([]byte, int(math.Ceil(float64(bitsLen)/7)))
|
||||
lastIndex := len(bit7Units) - 1
|
||||
for i := 0; i <= lastIndex; i++ {
|
||||
if i != lastIndex {
|
||||
bit7Units[i] = uint8(u64>>(7*i)&127) | 128
|
||||
} else if i == 8 {
|
||||
bit7Units[i] = uint8(u64 >> (7 * i) & 255)
|
||||
} else {
|
||||
bit7Units[i] = uint8(u64 >> (7 * i) & 127)
|
||||
}
|
||||
}
|
||||
return bit7Units
|
||||
}
|
5
proto/flex/go.mod
Normal file
5
proto/flex/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/idrunk/dce-go/proto/flex
|
||||
|
||||
go 1.23.3
|
||||
|
||||
require github.com/quic-go/quic-go v0.48.2
|
67
proto/flex/quic.go
Normal file
67
proto/flex/quic.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package flex
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"github.com/idrunk/dce-go/proto"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
type Quic = router.Context[*QuicProtocol]
|
||||
|
||||
type QuicProtocol struct {
|
||||
*PackageProtocol[quic.Connection]
|
||||
}
|
||||
|
||||
type WrappedQuicRouter struct {
|
||||
proto.ConnectorMappingManager[*QuicProtocol, quic.Connection]
|
||||
}
|
||||
|
||||
func (q *WrappedQuicRouter) Route(conn quic.Connection, ctxData map[string]any) bool {
|
||||
meta := router.NewMeta(conn, ctxData, true)
|
||||
stream, err := conn.AcceptStream(&meta)
|
||||
if err != nil {
|
||||
return q.Except(conn.RemoteAddr().String(), err)
|
||||
}
|
||||
defer stream.Close()
|
||||
context, qp, ok := q.uniRoute(stream, meta)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if context.Api != nil && context.Api.Responsive {
|
||||
bts := qp.ClearBuffer()
|
||||
if _, err = stream.Write(bts); err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (q *WrappedQuicRouter) UniRoute(conn quic.Connection, ctxData map[string]any) bool {
|
||||
meta := router.NewMeta(conn, ctxData, true)
|
||||
stream, err := conn.AcceptUniStream(&meta)
|
||||
if err != nil {
|
||||
return q.Except(conn.RemoteAddr().String(), err)
|
||||
}
|
||||
_, _, ok := q.uniRoute(stream, meta)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (q *WrappedQuicRouter) uniRoute(stream quic.ReceiveStream, meta router.Meta[quic.Connection]) (*Quic, *QuicProtocol, bool) {
|
||||
pkgProto, err := NewPackageProtocolWithMeta(bufio.NewReader(stream), meta)
|
||||
if err != nil {
|
||||
q.Except(meta.Req.RemoteAddr().String(), err)
|
||||
return nil, nil, false
|
||||
}
|
||||
qp := &QuicProtocol{pkgProto}
|
||||
context := router.NewContext(qp)
|
||||
q.Router.Route(context)
|
||||
qp.TryPrintErr()
|
||||
return context, qp, true
|
||||
}
|
||||
|
||||
var QuicRouter *WrappedQuicRouter
|
||||
|
||||
func init() {
|
||||
QuicRouter = &WrappedQuicRouter{proto.NewConnectorMappingManager[*QuicProtocol, quic.Connection]("flex-quic")}
|
||||
}
|
43
proto/flex/tcp.go
Normal file
43
proto/flex/tcp.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package flex
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"github.com/idrunk/dce-go/proto"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"log/slog"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Tcp = router.Context[*TcpProtocol]
|
||||
|
||||
type TcpProtocol struct {
|
||||
*PackageProtocol[net.Conn]
|
||||
}
|
||||
|
||||
type WrappedTcpRouter struct {
|
||||
proto.ConnectorMappingManager[*TcpProtocol, net.Conn]
|
||||
}
|
||||
|
||||
func (t *WrappedTcpRouter) Route(conn net.Conn, ctxData map[string]any) bool {
|
||||
pkg, err := NewPackageProtocol(bufio.NewReader(conn), conn, ctxData)
|
||||
if err != nil {
|
||||
return t.Except(conn.RemoteAddr().String(), err)
|
||||
}
|
||||
sw := &TcpProtocol{pkg}
|
||||
context := router.NewContext(sw)
|
||||
t.Router.Route(context)
|
||||
sw.TryPrintErr()
|
||||
if context.Api != nil && context.Api.Responsive {
|
||||
bytes := sw.ClearBuffer()
|
||||
if _, err = conn.Write(bytes); err != nil {
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var TcpRouter *WrappedTcpRouter
|
||||
|
||||
func init() {
|
||||
TcpRouter = &WrappedTcpRouter{proto.NewConnectorMappingManager[*TcpProtocol, net.Conn]("flex-tcp")}
|
||||
}
|
39
proto/flex/udp.go
Normal file
39
proto/flex/udp.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package flex
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Udp = router.Context[*UdpProtocol]
|
||||
|
||||
type UdpProtocol struct {
|
||||
*PackageProtocol[*net.UDPAddr]
|
||||
}
|
||||
|
||||
func UdpRoute(conn *net.UDPConn, pkg []byte, addr *net.UDPAddr, ctxData map[string]any) {
|
||||
pkgProto, err := NewPackageProtocol(bufio.NewReader(bytes.NewReader(pkg)), addr, ctxData)
|
||||
if err != nil {
|
||||
println(fmt.Sprintf("FlexPackage parse failed with: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
sw := &UdpProtocol{pkgProto}
|
||||
context := router.NewContext(sw)
|
||||
UdpRouter.Route(context)
|
||||
sw.TryPrintErr()
|
||||
if context.Api != nil && context.Api.Responsive {
|
||||
bts := sw.ClearBuffer()
|
||||
if _, err = conn.WriteToUDP(bts, addr); err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var UdpRouter *router.Router[*UdpProtocol]
|
||||
|
||||
func init() {
|
||||
UdpRouter = router.ProtoRouter[*UdpProtocol]("flex-udp")
|
||||
}
|
49
proto/flex/websocket.go
Normal file
49
proto/flex/websocket.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package flex
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"github.com/coder/websocket"
|
||||
"github.com/idrunk/dce-go/proto"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Websocket = router.Context[*WebsocketProtocol]
|
||||
|
||||
type WebsocketProtocol struct {
|
||||
*PackageProtocol[*http.Request]
|
||||
}
|
||||
|
||||
type WrappedWebsocketRouter struct {
|
||||
proto.ConnectorMappingManager[*WebsocketProtocol, *websocket.Conn]
|
||||
}
|
||||
|
||||
func (w *WrappedWebsocketRouter) Route(conn *websocket.Conn, req *http.Request, ctxData map[string]any) bool {
|
||||
meta := router.NewMeta(req, ctxData, true)
|
||||
ty, reader, err := conn.Reader(&meta)
|
||||
if err != nil {
|
||||
return w.Except(req.RemoteAddr, err)
|
||||
}
|
||||
pkg, err := NewPackageProtocolWithMeta(bufio.NewReader(reader), meta)
|
||||
if err != nil {
|
||||
return w.Except(req.RemoteAddr, err)
|
||||
}
|
||||
sw := &WebsocketProtocol{pkg}
|
||||
context := router.NewContext(sw)
|
||||
w.Router.Route(context)
|
||||
sw.TryPrintErr()
|
||||
if context.Api != nil && context.Api.Responsive {
|
||||
bytes := sw.ClearBuffer()
|
||||
if err = conn.Write(context, ty, bytes); err != nil {
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var WebsocketRouter *WrappedWebsocketRouter
|
||||
|
||||
func init() {
|
||||
WebsocketRouter = &WrappedWebsocketRouter{proto.NewConnectorMappingManager[*WebsocketProtocol, *websocket.Conn]("flex-websocket")}
|
||||
}
|
3
proto/go.mod
Normal file
3
proto/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/idrunk/dce-go/proto
|
||||
|
||||
go 1.23.3
|
187
proto/http.go
Normal file
187
proto/http.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"github.com/idrunk/dce-go/util"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
HttpGet = router.Method(1)
|
||||
HttpPost = router.Method(2)
|
||||
HttpPut = router.Method(3)
|
||||
HttpDelete = router.Method(4)
|
||||
HttpHead = router.Method(5)
|
||||
HttpOptions = router.Method(6)
|
||||
HttpConnect = router.Method(7)
|
||||
HttpPatch = router.Method(8)
|
||||
HttpTrace = router.Method(9)
|
||||
)
|
||||
|
||||
type Http = router.Context[*HttpProtocol]
|
||||
|
||||
const headerSidKey = "X-Session-Id"
|
||||
|
||||
type HttpProtocol struct {
|
||||
router.Meta[*http.Request]
|
||||
Writer http.ResponseWriter
|
||||
}
|
||||
|
||||
func (h *HttpProtocol) Path() string {
|
||||
return h.Req.URL.Path[1:]
|
||||
}
|
||||
|
||||
func (h *HttpProtocol) MatchApi(apis []*router.Api) (index int) {
|
||||
return slices.IndexFunc(apis, func(api *router.Api) bool {
|
||||
if method := uint(ToUintMethod(h.Req.Method)); method < 1 || method&uint(api.Method) != method {
|
||||
return false
|
||||
} else if hosts := api.Hosts(); len(hosts) > 0 {
|
||||
for _, host := range hosts {
|
||||
if strings.Contains(host, ":") {
|
||||
if host == h.Req.Host {
|
||||
return true
|
||||
}
|
||||
} else if _, err := strconv.Atoi(host); err == nil {
|
||||
if strings.HasSuffix(h.Req.Host, ":"+host) {
|
||||
return true
|
||||
}
|
||||
} else if strings.HasPrefix(h.Req.Host, host+":") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (h *HttpProtocol) Body() ([]byte, error) {
|
||||
return io.ReadAll(h.Req.Body)
|
||||
}
|
||||
|
||||
func (h *HttpProtocol) Sid() string {
|
||||
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"
|
||||
}); ok {
|
||||
return (*cookie).Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *HttpProtocol) Deadline() (deadline time.Time, ok bool) {
|
||||
return h.Req.Context().Deadline()
|
||||
}
|
||||
|
||||
func (h *HttpProtocol) Done() <-chan struct{} {
|
||||
return h.Req.Context().Done()
|
||||
}
|
||||
|
||||
func (h *HttpProtocol) Err() error {
|
||||
return h.Req.Context().Err()
|
||||
}
|
||||
|
||||
func (h *HttpProtocol) Value(key any) any {
|
||||
return h.Req.Context().Value(key)
|
||||
}
|
||||
|
||||
var methodNameUintMapping = map[string]router.Method{
|
||||
"GET": HttpGet,
|
||||
"POST": HttpPost,
|
||||
"PUT": HttpPut,
|
||||
"DELETE": HttpDelete,
|
||||
"HEAD": HttpHead,
|
||||
"OPTIONS": HttpOptions,
|
||||
"CONNECT": HttpConnect,
|
||||
"PATCH": HttpPatch,
|
||||
"TRACE": HttpTrace,
|
||||
}
|
||||
|
||||
func ToUintMethod(name string) router.Method {
|
||||
if methodUint, ok := methodNameUintMapping[name]; ok {
|
||||
return methodUint
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type WrappedHttpRouter router.Router[*HttpProtocol]
|
||||
|
||||
func (h *WrappedHttpRouter) Raw() *router.Router[*HttpProtocol] {
|
||||
return (*router.Router[*HttpProtocol])(h)
|
||||
}
|
||||
|
||||
func (h *WrappedHttpRouter) Get(path string, controller func(h *Http)) *WrappedHttpRouter {
|
||||
return h.pushMethod(HttpGet|HttpHead, path, controller)
|
||||
}
|
||||
|
||||
func (h *WrappedHttpRouter) Post(path string, controller func(h *Http)) *WrappedHttpRouter {
|
||||
return h.pushMethod(HttpPost|HttpOptions, path, controller)
|
||||
}
|
||||
|
||||
func (h *WrappedHttpRouter) Put(path string, controller func(h *Http)) *WrappedHttpRouter {
|
||||
return h.pushMethod(HttpPut|HttpOptions, path, controller)
|
||||
}
|
||||
|
||||
func (h *WrappedHttpRouter) Patch(path string, controller func(h *Http)) *WrappedHttpRouter {
|
||||
return h.pushMethod(HttpPatch|HttpOptions, path, controller)
|
||||
}
|
||||
|
||||
func (h *WrappedHttpRouter) Delete(path string, controller func(h *Http)) *WrappedHttpRouter {
|
||||
return h.pushMethod(HttpDelete|HttpOptions, path, controller)
|
||||
}
|
||||
|
||||
func (h *WrappedHttpRouter) pushMethod(method router.Method, path string, controller func(h *Http)) *WrappedHttpRouter {
|
||||
return h.PushApi(router.Api{Method: method, Path: path}, controller)
|
||||
}
|
||||
|
||||
func (h *WrappedHttpRouter) PushApi(api router.Api, controller func(c *Http)) *WrappedHttpRouter {
|
||||
if api.Method == 0 {
|
||||
panic(`Please specify the http "method" property`)
|
||||
}
|
||||
h.Raw().PushApi(api, controller)
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *WrappedHttpRouter) Route(writer http.ResponseWriter, request *http.Request) {
|
||||
hp := &HttpProtocol{
|
||||
Meta: router.NewMeta(request, nil, false),
|
||||
Writer: writer,
|
||||
}
|
||||
context := router.NewContext(hp)
|
||||
h.Raw().Route(context)
|
||||
hp.TryPrintErr()
|
||||
if ct, ok := hp.CtxData(router.HttpContentTypeKey); ok {
|
||||
hp.Writer.Header().Set(router.HttpContentTypeKey, ct.(string))
|
||||
}
|
||||
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 {
|
||||
hp.Writer.WriteHeader(util.ServiceUnavailable)
|
||||
}
|
||||
}
|
||||
if sid := hp.RespSid(); sid != "" {
|
||||
hp.Writer.Header().Set(headerSidKey, sid)
|
||||
}
|
||||
bytes := hp.ClearBuffer()
|
||||
if _, err := hp.Writer.Write(bytes); err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var HttpRouter *WrappedHttpRouter
|
||||
|
||||
func init() {
|
||||
HttpRouter = (*WrappedHttpRouter)(router.ProtoRouter[*HttpProtocol]("http"))
|
||||
}
|
3
proto/json/go.mod
Normal file
3
proto/json/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/idrunk/dce-go/proto/json
|
||||
|
||||
go 1.23.3
|
81
proto/json/json.go
Normal file
81
proto/json/json.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"math"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type PackageProtocol[Req any] struct {
|
||||
router.Meta[Req]
|
||||
pkg *Package
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) Id() uint32 {
|
||||
return p.pkg.Id
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) Path() string {
|
||||
return p.pkg.Path
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) Sid() string {
|
||||
return p.pkg.Sid
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) Body() ([]byte, error) {
|
||||
return p.pkg.Body, nil
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) ClearBuffer() []byte {
|
||||
p.pkg.Sid = p.RespSid()
|
||||
p.pkg.Body = p.Meta.ClearBuffer()
|
||||
code, message := p.ErrorUnits()
|
||||
p.pkg.Code, p.pkg.Msg = int32(code), message
|
||||
return p.pkg.Serialize()
|
||||
}
|
||||
|
||||
func NewPackageProtocol[Req any](data []byte, meta router.Meta[Req]) (*PackageProtocol[Req], error) {
|
||||
pkg, err := PackageDeserialize(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PackageProtocol[Req]{meta, pkg}, nil
|
||||
}
|
||||
|
||||
type Package struct {
|
||||
Id uint32 `json:"id,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Sid string `json:"sid,omitempty"`
|
||||
Code int32 `json:"code,omitempty"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Body []byte `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
func (p *Package) Serialize() []byte {
|
||||
if seq, err := json.Marshal(p); err == nil {
|
||||
return seq
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func PackageDeserialize(data []byte) (*Package, error) {
|
||||
var pkg Package
|
||||
if err := json.Unmarshal(data, &pkg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pkg, nil
|
||||
}
|
||||
|
||||
var reqId atomic.Uint32
|
||||
|
||||
func NewPackage(path string, body []byte, sid string, id int) *Package {
|
||||
if id == -1 {
|
||||
reqId.Add(1)
|
||||
if id = int(reqId.Load()); id == math.MaxUint32 {
|
||||
reqId.Store(0)
|
||||
}
|
||||
}
|
||||
return &Package{Id: uint32(id), Path: path, Sid: sid, Body: body}
|
||||
}
|
48
proto/json/tcp.go
Normal file
48
proto/json/tcp.go
Normal file
@@ -0,0 +1,48 @@
|
||||
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"
|
||||
)
|
||||
|
||||
type Tcp = router.Context[*TcpProtocol]
|
||||
|
||||
type TcpProtocol struct {
|
||||
*PackageProtocol[net.Conn]
|
||||
}
|
||||
|
||||
type WrappedTcpRouter struct {
|
||||
proto.ConnectorMappingManager[*TcpProtocol, net.Conn]
|
||||
}
|
||||
|
||||
func (t *WrappedTcpRouter) Route(conn net.Conn, ctxData map[string]any) bool {
|
||||
data, err := flex.StreamRead(conn)
|
||||
if err != nil {
|
||||
// Reading failure is considered as connection loss, just return false to let loop break
|
||||
return t.Except(conn.RemoteAddr().String(), err)
|
||||
}
|
||||
pkg, err := NewPackageProtocol(data, router.NewMeta(conn, ctxData, true))
|
||||
if err != nil {
|
||||
return t.Warn(err)
|
||||
}
|
||||
sw := &TcpProtocol{pkg}
|
||||
context := router.NewContext(sw)
|
||||
t.Router.Route(context)
|
||||
sw.TryPrintErr()
|
||||
if context.Api != nil && context.Api.Responsive {
|
||||
bytes := sw.ClearBuffer()
|
||||
if _, err = conn.Write(flex.StreamPack(bytes)); err != nil {
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var TcpRouter *WrappedTcpRouter
|
||||
|
||||
func init() {
|
||||
TcpRouter = &WrappedTcpRouter{proto.NewConnectorMappingManager[*TcpProtocol, net.Conn]("json-tcp")}
|
||||
}
|
37
proto/json/udp.go
Normal file
37
proto/json/udp.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Udp = router.Context[*UdpProtocol]
|
||||
|
||||
type UdpProtocol struct {
|
||||
*PackageProtocol[*net.UDPAddr]
|
||||
}
|
||||
|
||||
func UdpRoute(conn *net.UDPConn, pkg []byte, addr *net.UDPAddr, ctxData map[string]any) {
|
||||
pkgProto, err := NewPackageProtocol(pkg, router.NewMeta(addr, ctxData, true))
|
||||
if err != nil {
|
||||
println(fmt.Sprintf("Package parse failed with: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
sw := &UdpProtocol{pkgProto}
|
||||
context := router.NewContext(sw)
|
||||
UdpRouter.Route(context)
|
||||
sw.TryPrintErr()
|
||||
if context.Api != nil && context.Api.Responsive {
|
||||
bts := sw.ClearBuffer()
|
||||
if _, err = conn.WriteToUDP(bts, addr); err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var UdpRouter *router.Router[*UdpProtocol]
|
||||
|
||||
func init() {
|
||||
UdpRouter = router.ProtoRouter[*UdpProtocol]("json-udp")
|
||||
}
|
48
proto/json/websocket.go
Normal file
48
proto/json/websocket.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"github.com/coder/websocket"
|
||||
"github.com/idrunk/dce-go/proto"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Websocket = router.Context[*WebsocketProtocol]
|
||||
|
||||
type WebsocketProtocol struct {
|
||||
*PackageProtocol[*http.Request]
|
||||
}
|
||||
|
||||
type WrappedWebsocketRouter struct {
|
||||
proto.ConnectorMappingManager[*WebsocketProtocol, *websocket.Conn]
|
||||
}
|
||||
|
||||
func (w *WrappedWebsocketRouter) Route(conn *websocket.Conn, req *http.Request, ctxData map[string]any) bool {
|
||||
meta := router.NewMeta(req, ctxData, true)
|
||||
ty, data, err := conn.Read(&meta)
|
||||
if err != nil {
|
||||
return w.Except(req.RemoteAddr, err)
|
||||
}
|
||||
pkg, err := NewPackageProtocol(data, meta)
|
||||
if err != nil {
|
||||
return w.Warn(err)
|
||||
}
|
||||
sw := &WebsocketProtocol{pkg}
|
||||
context := router.NewContext(sw)
|
||||
w.Router.Route(context)
|
||||
sw.TryPrintErr()
|
||||
if context.Api != nil && context.Api.Responsive {
|
||||
bytes := sw.ClearBuffer()
|
||||
if err = conn.Write(context, ty, bytes); err != nil {
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var WebsocketRouter *WrappedWebsocketRouter
|
||||
|
||||
func init() {
|
||||
WebsocketRouter = &WrappedWebsocketRouter{proto.NewConnectorMappingManager[*WebsocketProtocol, *websocket.Conn]("json-websocket")}
|
||||
}
|
5
proto/pb/go.mod
Normal file
5
proto/pb/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/idrunk/dce-go/proto/pb
|
||||
|
||||
go 1.23.3
|
||||
|
||||
require google.golang.org/protobuf v1.36.1
|
175
proto/pb/package.pb.go
Normal file
175
proto/pb/package.pb.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.1
|
||||
// protoc v5.29.2
|
||||
// source: package.proto
|
||||
|
||||
package pb
|
||||
|
||||
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 Package struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id *uint32 `protobuf:"varint,1,opt,name=id,proto3,oneof" json:"id,omitempty"`
|
||||
Path *string `protobuf:"bytes,2,opt,name=path,proto3,oneof" json:"path,omitempty"`
|
||||
Sid *string `protobuf:"bytes,3,opt,name=sid,proto3,oneof" json:"sid,omitempty"`
|
||||
Code *int32 `protobuf:"varint,4,opt,name=code,proto3,oneof" json:"code,omitempty"`
|
||||
Msg *string `protobuf:"bytes,5,opt,name=msg,proto3,oneof" json:"msg,omitempty"`
|
||||
Body []byte `protobuf:"bytes,6,opt,name=body,proto3,oneof" json:"body,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Package) Reset() {
|
||||
*x = Package{}
|
||||
mi := &file_package_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Package) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Package) ProtoMessage() {}
|
||||
|
||||
func (x *Package) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_package_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 Package.ProtoReflect.Descriptor instead.
|
||||
func (*Package) Descriptor() ([]byte, []int) {
|
||||
return file_package_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Package) GetId() uint32 {
|
||||
if x != nil && x.Id != nil {
|
||||
return *x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Package) GetPath() string {
|
||||
if x != nil && x.Path != nil {
|
||||
return *x.Path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Package) GetSid() string {
|
||||
if x != nil && x.Sid != nil {
|
||||
return *x.Sid
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Package) GetCode() int32 {
|
||||
if x != nil && x.Code != nil {
|
||||
return *x.Code
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Package) GetMsg() string {
|
||||
if x != nil && x.Msg != nil {
|
||||
return *x.Msg
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Package) GetBody() []byte {
|
||||
if x != nil {
|
||||
return x.Body
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_package_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_package_proto_rawDesc = []byte{
|
||||
0x0a, 0x0d, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
|
||||
0xc9, 0x01, 0x0a, 0x07, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x12, 0x13, 0x0a, 0x02, 0x69,
|
||||
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x88, 0x01, 0x01,
|
||||
0x12, 0x17, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01,
|
||||
0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x15, 0x0a, 0x03, 0x73, 0x69, 0x64,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x03, 0x73, 0x69, 0x64, 0x88, 0x01, 0x01,
|
||||
0x12, 0x17, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x48, 0x03,
|
||||
0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x12, 0x15, 0x0a, 0x03, 0x6d, 0x73, 0x67,
|
||||
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x04, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x88, 0x01, 0x01,
|
||||
0x12, 0x17, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x05,
|
||||
0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x88, 0x01, 0x01, 0x42, 0x05, 0x0a, 0x03, 0x5f, 0x69, 0x64,
|
||||
0x42, 0x07, 0x0a, 0x05, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x73, 0x69,
|
||||
0x64, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x6d,
|
||||
0x73, 0x67, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x42, 0x06, 0x5a, 0x04, 0x2e,
|
||||
0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_package_proto_rawDescOnce sync.Once
|
||||
file_package_proto_rawDescData = file_package_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_package_proto_rawDescGZIP() []byte {
|
||||
file_package_proto_rawDescOnce.Do(func() {
|
||||
file_package_proto_rawDescData = protoimpl.X.CompressGZIP(file_package_proto_rawDescData)
|
||||
})
|
||||
return file_package_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_package_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_package_proto_goTypes = []any{
|
||||
(*Package)(nil), // 0: Package
|
||||
}
|
||||
var file_package_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_package_proto_init() }
|
||||
func file_package_proto_init() {
|
||||
if File_package_proto != nil {
|
||||
return
|
||||
}
|
||||
file_package_proto_msgTypes[0].OneofWrappers = []any{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_package_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_package_proto_goTypes,
|
||||
DependencyIndexes: file_package_proto_depIdxs,
|
||||
MessageInfos: file_package_proto_msgTypes,
|
||||
}.Build()
|
||||
File_package_proto = out.File
|
||||
file_package_proto_rawDesc = nil
|
||||
file_package_proto_goTypes = nil
|
||||
file_package_proto_depIdxs = nil
|
||||
}
|
12
proto/pb/package.proto
Normal file
12
proto/pb/package.proto
Normal file
@@ -0,0 +1,12 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "./pb";
|
||||
|
||||
message Package {
|
||||
optional uint32 id = 1;
|
||||
optional string path = 2;
|
||||
optional string sid = 3;
|
||||
optional int32 code = 4;
|
||||
optional string msg = 5;
|
||||
optional bytes body = 6;
|
||||
}
|
84
proto/pb/pb.go
Normal file
84
proto/pb/pb.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package pb
|
||||
|
||||
import (
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"log/slog"
|
||||
"math"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type PackageProtocol[Req any] struct {
|
||||
router.Meta[Req]
|
||||
pkg *Package
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) Id() uint32 {
|
||||
return p.pkg.GetId()
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) Path() string {
|
||||
return p.pkg.GetPath()
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) Sid() string {
|
||||
return p.pkg.GetSid()
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) Body() ([]byte, error) {
|
||||
return p.pkg.GetBody(), nil
|
||||
}
|
||||
|
||||
func (p *PackageProtocol[Req]) ClearBuffer() []byte {
|
||||
respSid := p.RespSid()
|
||||
p.pkg.Sid = &respSid
|
||||
p.pkg.Body = p.Meta.ClearBuffer()
|
||||
code, message := p.ErrorUnits()
|
||||
i32Code := int32(code)
|
||||
p.pkg.Code, p.pkg.Msg = &i32Code, &message
|
||||
return pkgSerialize(p.pkg)
|
||||
}
|
||||
|
||||
func NewPackageProtocol[Req any](bts []byte, meta router.Meta[Req]) (*PackageProtocol[Req], error) {
|
||||
pkg, err := PackageDeserialize(bts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PackageProtocol[Req]{meta, pkg}, nil
|
||||
}
|
||||
|
||||
func pkgSerialize(pkg *Package) []byte {
|
||||
seq, err := proto.Marshal(pkg)
|
||||
if err != nil {
|
||||
slog.Warn("Protobuf serialize failed.")
|
||||
}
|
||||
return seq
|
||||
}
|
||||
|
||||
var reqId atomic.Uint32
|
||||
|
||||
func PackageSerialize(path string, body []byte, sid string, id int) []byte {
|
||||
if id == -1 {
|
||||
reqId.Add(1)
|
||||
if id = int(reqId.Load()); id == math.MaxUint32 {
|
||||
reqId.Store(0)
|
||||
}
|
||||
}
|
||||
rid := uint32(id)
|
||||
return pkgSerialize(&Package{
|
||||
Id: &rid,
|
||||
Path: &path,
|
||||
Sid: &sid,
|
||||
Code: nil,
|
||||
Msg: nil,
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
|
||||
func PackageDeserialize(bts []byte) (*Package, error) {
|
||||
var pkg Package
|
||||
if err := proto.Unmarshal(bts, &pkg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pkg, nil
|
||||
}
|
48
proto/pb/tcp.go
Normal file
48
proto/pb/tcp.go
Normal file
@@ -0,0 +1,48 @@
|
||||
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"
|
||||
)
|
||||
|
||||
type Tcp = router.Context[*TcpProtocol]
|
||||
|
||||
type TcpProtocol struct {
|
||||
*PackageProtocol[net.Conn]
|
||||
}
|
||||
|
||||
type WrappedTcpRouter struct {
|
||||
proto.ConnectorMappingManager[*TcpProtocol, net.Conn]
|
||||
}
|
||||
|
||||
func (t *WrappedTcpRouter) Route(conn net.Conn, ctxData map[string]any) bool {
|
||||
data, err := flex.StreamRead(conn)
|
||||
if err != nil {
|
||||
// Reading failure is considered as connection loss, just return false to let loop break
|
||||
return t.Except(conn.RemoteAddr().String(), err)
|
||||
}
|
||||
pkg, err := NewPackageProtocol(data, router.NewMeta(conn, ctxData, true))
|
||||
if err != nil {
|
||||
return t.Warn(err)
|
||||
}
|
||||
sw := &TcpProtocol{pkg}
|
||||
context := router.NewContext(sw)
|
||||
t.Router.Route(context)
|
||||
sw.TryPrintErr()
|
||||
if context.Api != nil && context.Api.Responsive {
|
||||
bytes := sw.ClearBuffer()
|
||||
if _, err = conn.Write(flex.StreamPack(bytes)); err != nil {
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var TcpRouter *WrappedTcpRouter
|
||||
|
||||
func init() {
|
||||
TcpRouter = &WrappedTcpRouter{proto.NewConnectorMappingManager[*TcpProtocol, net.Conn]("pb-tcp")}
|
||||
}
|
37
proto/pb/udp.go
Normal file
37
proto/pb/udp.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package pb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Udp = router.Context[*UdpProtocol]
|
||||
|
||||
type UdpProtocol struct {
|
||||
*PackageProtocol[*net.UDPAddr]
|
||||
}
|
||||
|
||||
func UdpRoute(conn *net.UDPConn, pkg []byte, addr *net.UDPAddr, ctxData map[string]any) {
|
||||
pkgProto, err := NewPackageProtocol(pkg, router.NewMeta(addr, ctxData, true))
|
||||
if err != nil {
|
||||
println(fmt.Sprintf("Package parse failed with: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
sw := &UdpProtocol{pkgProto}
|
||||
context := router.NewContext(sw)
|
||||
UdpRouter.Route(context)
|
||||
sw.TryPrintErr()
|
||||
if context.Api != nil && context.Api.Responsive {
|
||||
bts := sw.ClearBuffer()
|
||||
if _, err = conn.WriteToUDP(bts, addr); err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var UdpRouter *router.Router[*UdpProtocol]
|
||||
|
||||
func init() {
|
||||
UdpRouter = router.ProtoRouter[*UdpProtocol]("pb-udp")
|
||||
}
|
48
proto/pb/websocket.go
Normal file
48
proto/pb/websocket.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package pb
|
||||
|
||||
import (
|
||||
"github.com/coder/websocket"
|
||||
"github.com/idrunk/dce-go/proto"
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Websocket = router.Context[*WebsocketProtocol]
|
||||
|
||||
type WebsocketProtocol struct {
|
||||
*PackageProtocol[*http.Request]
|
||||
}
|
||||
|
||||
type WrappedWebsocketRouter struct {
|
||||
proto.ConnectorMappingManager[*WebsocketProtocol, *websocket.Conn]
|
||||
}
|
||||
|
||||
func (w *WrappedWebsocketRouter) Route(conn *websocket.Conn, req *http.Request, ctxData map[string]any) bool {
|
||||
meta := router.NewMeta(req, ctxData, true)
|
||||
ty, data, err := conn.Read(&meta)
|
||||
if err != nil {
|
||||
return w.Except(req.RemoteAddr, err)
|
||||
}
|
||||
pkg, err := NewPackageProtocol(data, meta)
|
||||
if err != nil {
|
||||
return w.Warn(err)
|
||||
}
|
||||
sw := &WebsocketProtocol{pkg}
|
||||
context := router.NewContext(sw)
|
||||
w.Router.Route(context)
|
||||
sw.TryPrintErr()
|
||||
if context.Api != nil && context.Api.Responsive {
|
||||
bytes := sw.ClearBuffer()
|
||||
if err = conn.Write(context, ty, bytes); err != nil {
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var WebsocketRouter *WrappedWebsocketRouter
|
||||
|
||||
func init() {
|
||||
WebsocketRouter = &WrappedWebsocketRouter{proto.NewConnectorMappingManager[*WebsocketProtocol, *websocket.Conn]("pb-websocket")}
|
||||
}
|
79
proto/util.go
Normal file
79
proto/util.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"github.com/idrunk/dce-go/router"
|
||||
"github.com/idrunk/dce-go/util"
|
||||
"log/slog"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewConnectorMappingManager[Rp router.RoutableProtocol, C any](routerId string) ConnectorMappingManager[Rp, C] {
|
||||
return ConnectorMappingManager[Rp, C]{router.ProtoRouter[Rp](routerId), util.NewStruct[sync.Map](), util.NewStruct[sync.Map]()}
|
||||
}
|
||||
|
||||
type ConnectorMappingManager[Rp router.RoutableProtocol, C any] struct {
|
||||
*router.Router[Rp]
|
||||
connMapping sync.Map
|
||||
uidMapping sync.Map
|
||||
}
|
||||
|
||||
func (w *ConnectorMappingManager[Rp, C]) SetMapping(addr string, conn C) {
|
||||
w.connMapping.Store(addr, conn)
|
||||
}
|
||||
|
||||
func (w *ConnectorMappingManager[Rp, C]) Unmapping(addr string) {
|
||||
w.connMapping.Delete(addr)
|
||||
w.UidUnmapping(addr)
|
||||
}
|
||||
|
||||
func (w *ConnectorMappingManager[Rp, C]) Except(addr string, err error) bool {
|
||||
w.Unmapping(addr)
|
||||
slog.Debug("Client disconnected with: %s", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *ConnectorMappingManager[Rp, C]) Warn(err error) bool {
|
||||
slog.Warn(err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *ConnectorMappingManager[Rp, C]) ConnMapping() map[string]C {
|
||||
cm := make(map[string]C)
|
||||
w.connMapping.Range(func(key, value interface{}) bool {
|
||||
cm[key.(string)] = value.(C)
|
||||
return true
|
||||
})
|
||||
return cm
|
||||
}
|
||||
|
||||
func (w *ConnectorMappingManager[Rp, C]) ListBy(filter func(s string) bool) []util.Tuple2[string, C] {
|
||||
return util.MapSeq2From[string, C, util.Tuple2[string, C]](w.ConnMapping()).Filter2(func(s string, _ C) bool {
|
||||
return filter(s)
|
||||
}).Map2(func(addr string, conn C) util.Tuple2[string, C] {
|
||||
return util.NewTuple2(addr, conn)
|
||||
}).Collect()
|
||||
}
|
||||
|
||||
func (w *ConnectorMappingManager[Rp, C]) ConnBy(addr string) (C, bool) {
|
||||
if conn, ok := w.connMapping.Load(addr); ok {
|
||||
return conn.(C), true
|
||||
}
|
||||
return util.NewStruct[C](), false
|
||||
}
|
||||
|
||||
func (w *ConnectorMappingManager[Rp, C]) UidSetMapping(addr string, uid uint64) {
|
||||
w.uidMapping.Store(addr, uid)
|
||||
}
|
||||
|
||||
func (w *ConnectorMappingManager[Rp, C]) UidUnmapping(addr string) {
|
||||
w.uidMapping.Delete(addr)
|
||||
}
|
||||
|
||||
func (w *ConnectorMappingManager[Rp, C]) UidMapping() map[string]uint64 {
|
||||
um := make(map[string]uint64)
|
||||
w.uidMapping.Range(func(key, value interface{}) bool {
|
||||
um[key.(string)] = value.(uint64)
|
||||
return true
|
||||
})
|
||||
return um
|
||||
}
|
216
router/api.go
Normal file
216
router/api.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/idrunk/dce-go/util"
|
||||
)
|
||||
|
||||
const (
|
||||
MarkPathPartSeparator = "/"
|
||||
MarkSuffixSeparator = "|"
|
||||
MarkSuffixBoundary = "."
|
||||
MarkVariableOpener = "{"
|
||||
MarkVariableClosing = "}"
|
||||
MarkVarTypeOptional = "?"
|
||||
MarkVarTypeEmptableVector = "*"
|
||||
MarkVarTypeVector = "+"
|
||||
)
|
||||
|
||||
const (
|
||||
VarTypeNotVar = 1 << iota >> 1
|
||||
VarTypeRequired
|
||||
VarTypeOptional
|
||||
VarTypeVector
|
||||
VarTypeEmptableVector
|
||||
)
|
||||
|
||||
type RpApi[Rp RoutableProtocol] struct {
|
||||
Controller func(c *Context[Rp])
|
||||
Api
|
||||
}
|
||||
|
||||
func NewRpApi[Rp RoutableProtocol](api Api, controller func(c *Context[Rp])) *RpApi[Rp] {
|
||||
if len(api.Suffixes) > 0 {
|
||||
panic(`Please define the suffixes in the end of "Path" but not defined directly`)
|
||||
}
|
||||
lastPartFrom := strings.LastIndex(api.Path, MarkPathPartSeparator)
|
||||
if lastPartFrom == -1 {
|
||||
lastPartFrom = -len(MarkPathPartSeparator)
|
||||
}
|
||||
lastPartFrom += len(MarkPathPartSeparator)
|
||||
if boundIndex := strings.Index(api.Path[lastPartFrom:], MarkSuffixBoundary); boundIndex != -1 {
|
||||
api.Suffixes = util.MapSeqFrom[string, Suffix](strings.Split(api.Path[lastPartFrom+boundIndex+len(MarkSuffixBoundary):], MarkSuffixSeparator)).Map(func(v string) Suffix {
|
||||
return Suffix(v)
|
||||
}).Collect()
|
||||
api.Path = api.Path[:lastPartFrom+boundIndex]
|
||||
} else {
|
||||
api.Suffixes = []Suffix{""}
|
||||
}
|
||||
if controller == nil {
|
||||
controller = func(c *Context[Rp]) {}
|
||||
}
|
||||
return &RpApi[Rp]{Controller: controller, Api: api}
|
||||
}
|
||||
|
||||
// Api represents a structured definition of an API endpoint. It encapsulates various properties
|
||||
// such as the method, path, suffixes, and additional metadata like redirection, naming,
|
||||
// and custom extras. The struct is designed to be flexible and extensible, allowing for the
|
||||
// addition of custom key-value pairs and slice-based data through the `Extras` field.
|
||||
//
|
||||
// Fields:
|
||||
// - Method: Generally used to specify HTTP methods, but can also be used for other routable protocols.
|
||||
// - Path: The request path of the API endpoint, which may include dynamic parts or variables.
|
||||
// - Suffixes: A list of suffixes that can be appended to the path, typically used for versioning
|
||||
// or content negotiation.
|
||||
// - Id: A unique identifier for the API endpoint. (Not yet applied)
|
||||
// - Omission: A boolean flag indicating whether the endpoint should be omitted from request Path.
|
||||
// - Responsive: A boolean flag indicating whether the endpoint is responsive or not.
|
||||
// - Redirect: A URL to which requests to this endpoint should be redirected.
|
||||
// - Name: A human-readable name for the API endpoint.
|
||||
// - Extras: A map of additional key-value pairs that can be used to store custom data or
|
||||
// metadata related to the API endpoint.
|
||||
type Api struct {
|
||||
Method Method
|
||||
Path string
|
||||
Suffixes []Suffix
|
||||
Id string
|
||||
Omission bool
|
||||
Responsive bool
|
||||
Redirect string
|
||||
Name string
|
||||
Extras map[string]any
|
||||
}
|
||||
|
||||
func (a Api) ByMethod(method Method) Api {
|
||||
a.Method = method
|
||||
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 {
|
||||
a.Omission = true
|
||||
return a
|
||||
}
|
||||
|
||||
func (a Api) IsResponsive() Api {
|
||||
a.Responsive = true
|
||||
return a
|
||||
}
|
||||
|
||||
func (a Api) Unresponsive() Api {
|
||||
a.Responsive = false
|
||||
return a
|
||||
}
|
||||
|
||||
func (a Api) ByRedirect(redirect string) Api {
|
||||
a.Redirect = redirect
|
||||
return a
|
||||
}
|
||||
|
||||
func (a Api) ByName(name string) Api {
|
||||
a.Name = name
|
||||
return a
|
||||
}
|
||||
|
||||
// With adds or updates a key-value pair in the `Extras` map of the `Api` struct.
|
||||
// If the `Extras` map is nil, it initializes it before adding the key-value pair.
|
||||
// This method is useful for attaching additional metadata or custom data to the API endpoint.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The key under which the value will be stored in the `Extras` map.
|
||||
// - val: The value to be associated with the specified key.
|
||||
//
|
||||
// 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{})
|
||||
}
|
||||
a.Extras[key] = val
|
||||
return a
|
||||
}
|
||||
|
||||
// Append adds one or more items to a slice associated with the specified key in the `Extras` map of the `Api` struct.
|
||||
// If the key does not exist in the `Extras` map, it initializes the key with a new slice containing the provided items.
|
||||
// If the key exists but the associated value is not a slice, the function will panic, as it expects the value to be a slice.
|
||||
// This method is useful for appending additional data to a slice stored in the `Extras` map, such as adding multiple hosts or other slice-based metadata.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The key in the `Extras` map to which the items will be appended.
|
||||
// - items: One or more items to append to the slice associated with the key.
|
||||
//
|
||||
// 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{})
|
||||
}
|
||||
a.Extras[key] = new([]any)
|
||||
}
|
||||
val := a.Extras[key]
|
||||
if vec, ok := val.([]any); ok || vec == nil {
|
||||
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)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a Api) ExtraBy(key string) any {
|
||||
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 vec, ok := val.([]any); ok {
|
||||
return vec
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BindHosts appends one or more host addresses to the `Extras` map of the `Api` struct under the key `extraServeAddrKey`.
|
||||
// This method is used to specify the host addresses to which the API endpoint should be bound. The host addresses are stored
|
||||
// as a slice in the `Extras` map, allowing for multiple hosts to be associated with the same API endpoint.
|
||||
//
|
||||
// Parameters:
|
||||
// - hosts: One or more host addresses to be appended to the list of bound hosts for the API endpoint.
|
||||
//
|
||||
// Returns:
|
||||
// - The modified `Api` instance with the updated `Extras` map containing the appended host addresses.
|
||||
func (a Api) BindHosts(hosts ...string) Api {
|
||||
return a.Append(extraServeAddrKey, util.MapSeqFrom[string, any](hosts).Map(func(v string) any {
|
||||
return any(v)
|
||||
}).Collect()...)
|
||||
}
|
||||
|
||||
func (a Api) Hosts() []string {
|
||||
return util.MapSeqFrom[any, string](a.ExtrasBy(extraServeAddrKey)).Map(func(v any) string {
|
||||
return v.(string)
|
||||
}).Collect()
|
||||
}
|
||||
|
||||
func Path(path string) Api {
|
||||
return Api{Path: path}
|
||||
}
|
||||
|
||||
const extraServeAddrKey = "$#BIND-HOSTS#"
|
||||
|
||||
type Suffix string
|
||||
|
||||
type Method uint
|
118
router/context.go
Normal file
118
router/context.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/idrunk/dce-go/util"
|
||||
)
|
||||
|
||||
// Context is a generic struct that encapsulates the state and functionality for handling RoutableProtocol requests.
|
||||
// It is parameterized by the type Rp, which must implement the RoutableProtocol interface.
|
||||
// The Context struct holds references to the request protocol (Rp), the API (Api), the router (router),
|
||||
// a suffix (suffix) for path matching, and a map of path parameters (params).
|
||||
// It provides methods to set routes, retrieve path parameters, handle request bodies, write responses,
|
||||
// and manage request context such as deadlines, cancellation, and error handling.
|
||||
type Context[Rp RoutableProtocol] struct {
|
||||
Rp Rp
|
||||
Api *RpApi[Rp]
|
||||
router *Router[Rp]
|
||||
suffix util.Tuple2[*Suffix, bool]
|
||||
params map[string]Param
|
||||
}
|
||||
|
||||
func (c *Context[Rp]) SetRoutes(router *Router[Rp], api *RpApi[Rp], pathParams map[string]Param, suffix *Suffix) {
|
||||
c.Api = api
|
||||
c.suffix = util.NewTuple2(suffix, false)
|
||||
c.params = pathParams
|
||||
c.router = router
|
||||
}
|
||||
|
||||
func (c *Context[Rp]) Suffix() *Suffix {
|
||||
if c.suffix.B {
|
||||
return c.suffix.A
|
||||
} else if c.suffix.A == nil {
|
||||
if suffix, ok := util.SeqFrom(c.Api.Suffixes).Find(func(s Suffix) bool {
|
||||
return strings.HasSuffix(c.Rp.Path(), string(s))
|
||||
}); ok {
|
||||
c.suffix.A = &suffix
|
||||
}
|
||||
}
|
||||
c.suffix.B = true
|
||||
return c.suffix.A
|
||||
}
|
||||
|
||||
func (c *Context[Rp]) Param(key string) string {
|
||||
if param, ok := c.params[key]; ok {
|
||||
return param.Value()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Context[Rp]) Params(key string) []string {
|
||||
if param, ok := c.params[key]; ok {
|
||||
return param.Values()
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (c *Context[Rp]) Body() ([]byte, error) {
|
||||
return c.Rp.Body()
|
||||
}
|
||||
|
||||
func (c *Context[Rp]) Write(bytes []byte) (int, error) {
|
||||
return c.Rp.Write(bytes)
|
||||
}
|
||||
|
||||
func (c *Context[Rp]) WriteString(str string) (int, error) {
|
||||
return c.Rp.WriteString(str)
|
||||
}
|
||||
|
||||
func (c *Context[Rp]) SetError(err error) bool {
|
||||
c.Rp.SetError(err)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Context[Rp]) Deadline() (deadline time.Time, ok bool) {
|
||||
return c.Rp.Deadline()
|
||||
}
|
||||
|
||||
func (c *Context[Rp]) Done() <-chan struct{} {
|
||||
return c.Rp.Done()
|
||||
}
|
||||
|
||||
func (c *Context[Rp]) Err() error {
|
||||
return c.Rp.Err()
|
||||
}
|
||||
|
||||
func (c *Context[Rp]) Value(key any) any {
|
||||
return c.Rp.Value(key)
|
||||
}
|
||||
|
||||
func NewContext[Rp RoutableProtocol](rp Rp) *Context[Rp] {
|
||||
return &Context[Rp]{Rp: rp}
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
string
|
||||
vec []string
|
||||
Type int
|
||||
}
|
||||
|
||||
func NewParam(val any, ty int) Param {
|
||||
param := Param{Type: ty}
|
||||
if param.Type&(VarTypeEmptableVector|VarTypeVector) > 0 {
|
||||
param.vec = val.([]string)
|
||||
} else {
|
||||
param.string = val.(string)
|
||||
}
|
||||
return param
|
||||
}
|
||||
|
||||
func (p *Param) Value() string {
|
||||
return p.string
|
||||
}
|
||||
|
||||
func (p *Param) Values() []string {
|
||||
return p.vec
|
||||
}
|
84
router/converter.go
Normal file
84
router/converter.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/idrunk/dce-go/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 Parser[Obj any] interface {
|
||||
Parse() (Obj, bool)
|
||||
}
|
||||
|
||||
type Serializer[Dto any] interface {
|
||||
Serialize(dto Dto) ([]byte, error)
|
||||
}
|
||||
|
||||
type Deserializer[Dto any] interface {
|
||||
Deserialize(bytes []byte) (Dto, error)
|
||||
}
|
||||
|
||||
type Into[T any] interface {
|
||||
Into() (T, error)
|
||||
}
|
||||
|
||||
type From[S, T any] interface {
|
||||
From(src S) (T, error)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
var obj Obj
|
||||
return 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)
|
||||
}
|
||||
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"`
|
||||
}
|
3
router/go.mod
Normal file
3
router/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/idrunk/dce-go/router
|
||||
|
||||
go 1.23.3
|
242
router/protocol.go
Normal file
242
router/protocol.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/idrunk/dce-go/session"
|
||||
"github.com/idrunk/dce-go/util"
|
||||
)
|
||||
|
||||
const (
|
||||
ContextKeyRespSid = "Resp-Session-Id"
|
||||
HttpContentTypeKey = "Content-Type"
|
||||
sessionKey = "$#session#"
|
||||
)
|
||||
|
||||
// Meta is a generic struct that encapsulates metadata and state associated with a request.
|
||||
// It provides methods for managing the request context, session data, response buffer,
|
||||
// and error handling. The struct is parameterized by the type of the request (Req),
|
||||
// allowing it to be used with different request types while maintaining type safety.
|
||||
//
|
||||
// Fields:
|
||||
// - Req: The request object of type Req, which holds the actual request data.
|
||||
// - respBuffer: A bytes.Buffer used to accumulate the response data before it is sent.
|
||||
// - err: An error that can be set during request processing to indicate a failure.
|
||||
// - ctxData: A map[string]any that stores arbitrary context data associated with the request.
|
||||
// - context: A context.Context that manages the lifecycle and cancellation of the request.
|
||||
// - mu: A pointer to a sync.RWMutex used to synchronize access to the struct's fields.
|
||||
type Meta[Req any] struct {
|
||||
Req Req
|
||||
respBuffer bytes.Buffer
|
||||
err error
|
||||
ctxData map[string]any
|
||||
context context.Context
|
||||
mu *sync.RWMutex
|
||||
}
|
||||
|
||||
func NewMeta[Req any](req Req, ctxData map[string]any, initContext bool) Meta[Req] {
|
||||
if ctxData == nil {
|
||||
ctxData = make(map[string]any)
|
||||
}
|
||||
m := Meta[Req]{Req: req, ctxData: ctxData, mu: &sync.RWMutex{}}
|
||||
if initContext {
|
||||
m.context = context.Background()
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) ClearBuffer() []byte {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
bs := m.respBuffer.Bytes()
|
||||
m.respBuffer.Reset()
|
||||
return bs
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) NotResponse() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.respBuffer.Len() == 0
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) TryPrintErr() {
|
||||
if m.err != nil {
|
||||
println(m.err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) Id() uint32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) Sid() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) MatchApi([]*Api) (index int) {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) Write(bytes []byte) (int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.respBuffer.Write(bytes)
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) WriteString(str string) (int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.respBuffer.WriteString(str)
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) SetError(err error) {
|
||||
m.err = err
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) Error() error {
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) ErrorUnits() (int, string) {
|
||||
if err := m.Error(); err != nil {
|
||||
return util.ResponseUnits(err)
|
||||
} else {
|
||||
return 0, ""
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) SetCtxData(key string, val any) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.ctxData[key] = val
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) CtxData(key string) (any, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if v, ok := m.ctxData[key]; ok {
|
||||
return v, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) SetSession(session session.IfSession) {
|
||||
m.SetCtxData(sessionKey, session)
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) Session() session.IfSession {
|
||||
if v, ok := m.CtxData(sessionKey); ok {
|
||||
return v.(session.IfSession)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) SetRespSid(sid string) {
|
||||
m.SetCtxData(ContextKeyRespSid, sid)
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) RespSid() string {
|
||||
if v, ok := m.CtxData(ContextKeyRespSid); ok {
|
||||
return v.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) Deadline() (deadline time.Time, ok bool) {
|
||||
return m.context.Deadline()
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) Done() <-chan struct{} {
|
||||
return m.context.Done()
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) Err() error {
|
||||
return m.context.Err()
|
||||
}
|
||||
|
||||
func (m *Meta[Req]) Value(key any) any {
|
||||
return m.context.Value(key)
|
||||
}
|
||||
|
||||
// RoutableProtocol defines the interface for a protocol that can be routed within the application.
|
||||
// It provides methods for handling requests, managing session data, and writing responses.
|
||||
// Implementations of this interface are expected to provide functionality for identifying
|
||||
// the request, matching it to an API, managing errors, and interacting with the context.
|
||||
type RoutableProtocol interface {
|
||||
// Id returns a unique identifier for the request. This is typically used to distinguish
|
||||
// between different requests in a system.
|
||||
Id() uint32
|
||||
|
||||
// Path returns the path associated with the request. This is typically the URL path
|
||||
// or a similar identifier that specifies the resource being accessed.
|
||||
Path() string
|
||||
|
||||
// MatchApi matches the request against a list of APIs and returns the index of the
|
||||
// matching API. If no match is found, it returns -1.
|
||||
MatchApi(apis []*Api) (index int)
|
||||
|
||||
// Body retrieves the body of the request as a byte slice. It returns an error if
|
||||
// the body cannot be read or processed.
|
||||
Body() ([]byte, error)
|
||||
|
||||
// Sid returns the session ID associated with the request. This is typically used
|
||||
// to identify the user session.
|
||||
Sid() string
|
||||
|
||||
// Write writes the provided byte slice to the response buffer. It returns the number
|
||||
// of bytes written and any error encountered.
|
||||
Write(bytes []byte) (int, error)
|
||||
|
||||
// WriteString writes the provided string to the response buffer. It returns the number
|
||||
// of bytes written and any error encountered.
|
||||
WriteString(str string) (int, error)
|
||||
|
||||
// SetError sets an error on the request. This is typically used to propagate errors
|
||||
// that occur during request processing.
|
||||
SetError(error)
|
||||
|
||||
// Error returns the error associated with the request, if any.
|
||||
Error() error
|
||||
|
||||
// SetCtxData sets a key-value pair in the context data map. This is used to store
|
||||
// arbitrary data associated with the request.
|
||||
SetCtxData(key string, val any)
|
||||
|
||||
// CtxData retrieves the value associated with the given key from the context data map.
|
||||
// It returns the value and a boolean indicating whether the key was found.
|
||||
CtxData(key string) (any, bool)
|
||||
|
||||
// SetSession sets the session associated with the request. This is typically used
|
||||
// to manage user sessions.
|
||||
SetSession(session session.IfSession)
|
||||
|
||||
// Session retrieves the session associated with the request. It returns nil if no
|
||||
// session is set.
|
||||
Session() session.IfSession
|
||||
|
||||
// SetRespSid sets the session ID in the response context. This is typically used
|
||||
// to propagate session information to the response.
|
||||
SetRespSid(sid string)
|
||||
|
||||
// RespSid retrieves the session ID from the response context. It returns an empty
|
||||
// string if no session ID is set.
|
||||
RespSid() string
|
||||
|
||||
// Deadline returns the time when the request context will be canceled, if any.
|
||||
// It also returns a boolean indicating whether a deadline is set.
|
||||
Deadline() (deadline time.Time, ok bool)
|
||||
|
||||
// Done returns a channel that is closed when the request context is canceled.
|
||||
// This can be used to detect when the request should be terminated.
|
||||
Done() <-chan struct{}
|
||||
|
||||
// Err returns the error that caused the request context to be canceled, if any.
|
||||
Err() error
|
||||
|
||||
// Value retrieves the value associated with the given key from the request context.
|
||||
// It returns nil if the key is not found.
|
||||
Value(key any) any
|
||||
}
|
512
router/router.go
Normal file
512
router/router.go
Normal file
@@ -0,0 +1,512 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/idrunk/dce-go/util"
|
||||
)
|
||||
|
||||
const CodeNotFound = 404
|
||||
|
||||
// Router is a generic struct that provides routing functionality for a given RoutableProtocol type.
|
||||
// It manages API routes, handles path matching, and supports various routing features such as
|
||||
// path variables, suffixes, and event handling. The Router is designed to be flexible and
|
||||
// extensible, allowing for custom API matchers, event handlers, and route configurations.
|
||||
//
|
||||
// The Router maintains a tree structure for efficient path matching and supports features like
|
||||
// route redirection, path omission, and dynamic path variables. It also provides methods to
|
||||
// push new routes, set custom separators, and configure event handlers for pre- and post-controller
|
||||
// execution.
|
||||
//
|
||||
// The Router is thread-safe and uses a mutex to ensure concurrent access to its internal state.
|
||||
type Router[Rp RoutableProtocol] struct {
|
||||
pathPartSeparator string
|
||||
suffixBoundary string
|
||||
apiBuffer []*RpApi[Rp]
|
||||
rawOmittedPaths []string
|
||||
idApiMapping map[string]*RpApi[Rp]
|
||||
apisMapping map[string][]*RpApi[Rp]
|
||||
apisTree util.Tree[ApiBranch[Rp], string]
|
||||
apiMatcher func(rp Rp, apis []*Api) (index int)
|
||||
beforeController func(ctx *Context[Rp]) error
|
||||
afterController func(ctx *Context[Rp]) error
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewRouter[Rp RoutableProtocol]() *Router[Rp] {
|
||||
return &Router[Rp]{
|
||||
pathPartSeparator: MarkPathPartSeparator,
|
||||
suffixBoundary: MarkSuffixBoundary,
|
||||
idApiMapping: make(map[string]*RpApi[Rp]),
|
||||
apisMapping: make(map[string][]*RpApi[Rp]),
|
||||
apisTree: util.NewTree(newApiBranch("", make([]*RpApi[Rp], 0))),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router[Rp]) SetSeparator(pps string, sb string) *Router[Rp] {
|
||||
r.pathPartSeparator = pps
|
||||
r.suffixBoundary = sb
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Router[Rp]) SetEventHandler(before func(ctx *Context[Rp]) error, after func(ctx *Context[Rp]) error) *Router[Rp] {
|
||||
r.beforeController = before
|
||||
r.afterController = after
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Router[Rp]) SetApiMatcher(apiMatcher func(rp Rp, apis []*Api) (index int)) *Router[Rp] {
|
||||
r.apiMatcher = apiMatcher
|
||||
return r
|
||||
}
|
||||
|
||||
// Push adds a new route to the router with the specified path and controller function.
|
||||
// The path is the URL pattern that the route will match, and the controller function
|
||||
// is the handler that will be executed when the route is matched. The controller
|
||||
// function receives a context object that contains information about the request
|
||||
// and provides methods to send a response.
|
||||
//
|
||||
// This method returns the router instance itself, allowing for method chaining.
|
||||
// The route is added to the router's internal buffer and will be processed
|
||||
// when the router is ready to build its routing tree.
|
||||
func (r *Router[Rp]) Push(path string, controller func(c *Context[Rp])) *Router[Rp] {
|
||||
return r.PushApi(Api{Path: path, Responsive: true}, controller)
|
||||
}
|
||||
|
||||
// PushApi adds a new route to the router with the specified API configuration and controller function.
|
||||
// The `api` parameter defines the route's path, suffixes, and other properties, while the `controller`
|
||||
// function is the handler that will be executed when the route is matched. The controller function
|
||||
// receives a context object that contains information about the request and provides methods to send
|
||||
// a response.
|
||||
//
|
||||
// This method returns the router instance itself, allowing for method chaining. The route is added
|
||||
// to the router's internal buffer and will be processed when the router is ready to build its routing
|
||||
// tree.
|
||||
func (r *Router[Rp]) PushApi(api Api, controller func(c *Context[Rp])) *Router[Rp] {
|
||||
return r.PushConf(NewRpApi(api, controller))
|
||||
}
|
||||
|
||||
func (r *Router[Rp]) PushConf(api *RpApi[Rp]) *Router[Rp] {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if strings.HasPrefix(api.Path, MarkPathPartSeparator) {
|
||||
log.Fatalf("`Api.Path` \"%s\" cannot start with \"%s\"\n", MarkPathPartSeparator, api.Path)
|
||||
}
|
||||
if api.Omission {
|
||||
r.rawOmittedPaths = append(r.rawOmittedPaths, api.Path)
|
||||
}
|
||||
if len(api.Id) > 0 {
|
||||
r.idApiMapping[api.Id] = api
|
||||
}
|
||||
r.apiBuffer = append(r.apiBuffer, api)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Router[Rp]) ready() {
|
||||
if !r.mu.TryLock() {
|
||||
return
|
||||
}
|
||||
defer r.mu.Unlock()
|
||||
r.buildTree()
|
||||
// build apisMapping
|
||||
for len(r.apiBuffer) > 0 {
|
||||
api := r.apiBuffer[0]
|
||||
path := r.omittedPath(api.Path)
|
||||
apis := []*RpApi[Rp]{api}
|
||||
suffixes := util.Set(api.Suffixes...)
|
||||
r.apiBuffer = r.apiBuffer[1:]
|
||||
for i := 0; i < len(r.apiBuffer); i++ {
|
||||
// collect the omitted same path into an array
|
||||
if path == r.omittedPath(r.apiBuffer[i].Path) {
|
||||
apis = append(apis, r.apiBuffer[i])
|
||||
suffixes.Append(r.apiBuffer[i].Suffixes...)
|
||||
// remove the collected item
|
||||
r.apiBuffer = slices.Delete(r.apiBuffer, i, i+1)
|
||||
i--
|
||||
}
|
||||
}
|
||||
// append suffix to path as api mapping key to grouping the apis
|
||||
for _, suffix := range suffixes {
|
||||
// insert suffix matched apis into the mapping
|
||||
r.apisMapping[r.suffixPath(path, &suffix)] = util.SeqFrom(apis).Filter(func(a *RpApi[Rp]) bool {
|
||||
return slices.Contains(api.Suffixes, suffix)
|
||||
}).Collect()
|
||||
}
|
||||
}
|
||||
r.apiBuffer = nil
|
||||
if r.apiMatcher == nil {
|
||||
r.apiMatcher = func(rp Rp, apis []*Api) (index int) {
|
||||
return rp.MatchApi(apis)
|
||||
}
|
||||
}
|
||||
if r.beforeController == nil {
|
||||
r.beforeController = func(ctx *Context[Rp]) error { return nil }
|
||||
}
|
||||
if r.afterController == nil {
|
||||
r.afterController = func(ctx *Context[Rp]) error { return nil }
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router[Rp]) omittedPath(path string) string {
|
||||
// Path in api field should always be `MarkPathPartSeparator`
|
||||
parts := strings.Split(path, MarkPathPartSeparator)
|
||||
return strings.Join(util.NewMapSeq2[int, string, string](slices.All(parts)).Filter2(func(i int, _ string) bool {
|
||||
return !slices.Contains(r.rawOmittedPaths, strings.Join(parts[:i+1], MarkPathPartSeparator))
|
||||
}).Map2(func(_ int, p string) string {
|
||||
return p
|
||||
}).Collect(), MarkPathPartSeparator)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (r *Router[Rp]) buildTree() {
|
||||
// 1. make apis to ApiBranches
|
||||
apiBuffer := slices.Clone(r.apiBuffer)
|
||||
apiBranches := util.NewMapSeq[[]*RpApi[Rp], ApiBranch[Rp]](util.MapSeqFrom[string, []*RpApi[Rp]](util.MapSeqFrom[*RpApi[Rp], string](apiBuffer).Map(func(a *RpApi[Rp]) string {
|
||||
return a.Path
|
||||
}).Unique(slices.Contains[[]string])).Map(func(s string) []*RpApi[Rp] {
|
||||
var apis []*RpApi[Rp]
|
||||
for i := len(apiBuffer) - 1; i >= 0; i-- {
|
||||
if apiBuffer[i].Path == s {
|
||||
apis = append(apis, apiBuffer[i])
|
||||
// remove the appended
|
||||
apiBuffer = slices.Delete(apiBuffer, i, i+1)
|
||||
i--
|
||||
}
|
||||
}
|
||||
return apis
|
||||
}).Seq()).Map(func(apis []*RpApi[Rp]) ApiBranch[Rp] {
|
||||
return newApiBranch(apis[0].Path, apis)
|
||||
}).Collect()
|
||||
// 2. init the apisTree
|
||||
r.apisTree.Build(apiBranches, func(tree *util.Tree[ApiBranch[Rp], string], remains []ApiBranch[Rp]) {
|
||||
var fills []util.Tuple2[string, ApiBranch[Rp]]
|
||||
for _, remain := range remains {
|
||||
paths := strings.Split(remain.Path, MarkPathPartSeparator)
|
||||
for i := 0; i < len(paths)-1; i++ {
|
||||
path := strings.Join(paths[:i+1], MarkPathPartSeparator)
|
||||
if _, ok := tree.ChildByPath(paths[:i+1]); !ok && !util.MapSeqFrom[util.Tuple2[string, ApiBranch[Rp]], string](fills).Map(func(f util.Tuple2[string, ApiBranch[Rp]]) string {
|
||||
return f.A
|
||||
}).Contains(path, util.Equal) {
|
||||
fills = append(fills, util.NewTuple2(path, newApiBranch(path, []*RpApi[Rp]{})))
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
// 3. fill the apisTree item properties
|
||||
r.apisTree.Traversal(func(t *util.Tree[ApiBranch[Rp], string]) int {
|
||||
isOmittedPassedChild := false
|
||||
for parent := t.Parent; parent != nil; parent = parent.Parent {
|
||||
if !parent.Element.IsOmission {
|
||||
switch parent.Element.VarType {
|
||||
case VarTypeRequired:
|
||||
parent.Element.IsMidVar = true
|
||||
case VarTypeNotVar:
|
||||
break
|
||||
default:
|
||||
panic(fmt.Sprintf("Ambiguous type var '%s' cannot in middle.", parent.Element.Key()))
|
||||
}
|
||||
if t.Element.VarType != VarTypeNotVar {
|
||||
parent.Element.VarChildren = append(parent.Element.VarChildren, t)
|
||||
} else if isOmittedPassedChild {
|
||||
parent.Element.OmittedPassedChildren[t.Element.Key()] = t
|
||||
}
|
||||
break
|
||||
}
|
||||
isOmittedPassedChild = true
|
||||
}
|
||||
return util.TreeTraverContinue
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Router[Rp]) locate(path string, apiFinder func([]*RpApi[Rp]) (*RpApi[Rp], bool)) (*RpApi[Rp], map[string]Param, *Suffix, error) {
|
||||
var api *RpApi[Rp]
|
||||
var suffix *Suffix
|
||||
var pathParams map[string]Param
|
||||
reqPath := path
|
||||
// this loop just for the RpApi.Redirect property to redirect
|
||||
for {
|
||||
apis, ok := r.apisMapping[path]
|
||||
if !ok {
|
||||
if tmpPath, tmpPathParams, tmpSuffix, ok2 := r.matchVarPath(path); ok2 {
|
||||
apis, ok = r.apisMapping[r.suffixPath(tmpPath, tmpSuffix)]
|
||||
pathParams, suffix = tmpPathParams, tmpSuffix
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
if api, ok = apiFinder(apis); ok {
|
||||
if len(api.Redirect) == 0 {
|
||||
break
|
||||
}
|
||||
path = api.Redirect
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(r.apisMapping) < 1 {
|
||||
if len(r.apiBuffer) > 0 {
|
||||
r.ready()
|
||||
return r.locate(reqPath, apiFinder)
|
||||
} else {
|
||||
panic(`locate failed, "Router.apiBuffer" is empty, you may need to call the "Router.Push()" to bind apis`)
|
||||
}
|
||||
}
|
||||
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)
|
||||
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}}
|
||||
pathParams := map[string]Param{}
|
||||
var targetApiBranch *util.Tree[ApiBranch[Rp], string]
|
||||
var suffix *Suffix
|
||||
Outer:
|
||||
for i := 0; i >= 0; i = len(loopItems) - 1 {
|
||||
apiBranch, partNumber := loopItems[i].Values()
|
||||
loopItems = loopItems[:i]
|
||||
isLastPart := partNumber == len(pathParts)-1
|
||||
isOverflowed := partNumber >= len(pathParts)
|
||||
if isOverflowed && len(apiBranch.Element.Apis) > 0 {
|
||||
// should be finished at last request path part if not a bare tree
|
||||
targetApiBranch = apiBranch
|
||||
break
|
||||
}
|
||||
// if not overflow and request path matched, then it must be a normal path
|
||||
if !isOverflowed {
|
||||
if subApiBranch, matchedSuffix, ok := r.findConsiderSuffix(pathParts[partNumber], isLastPart, apiBranch.Children, apiBranch.Element.OmittedPassedChildren); ok {
|
||||
loopItems = append(loopItems, util.NewTuple2(subApiBranch, 1+partNumber))
|
||||
suffix = matchedSuffix
|
||||
continue
|
||||
}
|
||||
}
|
||||
insertPos := len(loopItems)
|
||||
for _, varApiBranch := range apiBranch.Element.VarChildren {
|
||||
if !varApiBranch.Element.IsMidVar {
|
||||
// just need to check is_last_part because should already handle suffix if overflowed
|
||||
// pop out the last part to clean (cut off the suffix)
|
||||
suffixTrimmer := func(pathParts []string, consumer func([]string)) {
|
||||
if len(pathParts) > 0 {
|
||||
lastPart := pathParts[len(pathParts)-1]
|
||||
pathParts = pathParts[:len(pathParts)-1]
|
||||
if tmpSuffix, ok := util.MapSeqFrom[*RpApi[Rp], Suffix](varApiBranch.Element.Apis).FlatMap(func(a *RpApi[Rp]) []Suffix {
|
||||
return a.Suffixes
|
||||
}).Find(func(s Suffix) bool {
|
||||
return strings.HasSuffix(lastPart, r.suffixBoundary+string(s))
|
||||
}); ok {
|
||||
lastPart = lastPart[:len(lastPart)-len(r.suffixBoundary)-len(tmpSuffix)]
|
||||
suffix = &tmpSuffix
|
||||
}
|
||||
pathParts = append(pathParts, lastPart)
|
||||
}
|
||||
consumer(pathParts)
|
||||
}
|
||||
// if not a middle var, then should finish var path match and collect vars and end the outer loop
|
||||
if varApiBranch.Element.VarType == VarTypeOptional && isOverflowed {
|
||||
//pathParams[varApiBranch.Element.VarName] = Param{Type: varApiBranch.Element.VarType}
|
||||
} else if slices.Contains([]int{VarTypeOptional, VarTypeRequired}, varApiBranch.Element.VarType) && isLastPart {
|
||||
suffixTrimmer(pathParts, func(pp []string) {
|
||||
pathParams[varApiBranch.Element.VarName] = NewParam(pp[partNumber], varApiBranch.Element.VarType)
|
||||
})
|
||||
} else if varApiBranch.Element.VarType == VarTypeEmptableVector && isOverflowed {
|
||||
//pathParams[varApiBranch.Element.VarName] = Param{vec: []string{}, Type: varApiBranch.Element.VarType}
|
||||
} else if slices.Contains([]int{VarTypeEmptableVector, VarTypeVector}, varApiBranch.Element.VarType) && !isOverflowed {
|
||||
suffixTrimmer(pathParts, func(pp []string) {
|
||||
pathParams[varApiBranch.Element.VarName] = NewParam(pp[partNumber:], varApiBranch.Element.VarType)
|
||||
})
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
targetApiBranch = varApiBranch
|
||||
break Outer
|
||||
} else if varApiBranch.Element.VarType == VarTypeRequired {
|
||||
// if it's middle var then insert to loop queue to handle it next cycle
|
||||
pathParams[varApiBranch.Element.VarName] = NewParam(pathParts[partNumber], varApiBranch.Element.VarType)
|
||||
loopItems = slices.Insert(loopItems, insertPos, util.NewTuple2(varApiBranch, 1+partNumber))
|
||||
}
|
||||
}
|
||||
}
|
||||
if targetApiBranch == nil {
|
||||
return "", nil, nil, false
|
||||
}
|
||||
return targetApiBranch.Element.Path, pathParams, suffix, true
|
||||
}
|
||||
|
||||
func (r *Router[Rp]) findConsiderSuffix(
|
||||
part string,
|
||||
isLastPart bool,
|
||||
children map[string]*util.Tree[ApiBranch[Rp], string],
|
||||
omittedPassedChildren map[string]*util.Tree[ApiBranch[Rp], string],
|
||||
) (*util.Tree[ApiBranch[Rp], string], *Suffix, bool) {
|
||||
matches, ok := children[part]
|
||||
if !ok {
|
||||
matches, ok = omittedPassedChildren[part]
|
||||
}
|
||||
// try to trim the suffix to match if not matched directly
|
||||
if !ok && isLastPart {
|
||||
for boundary := len(part); boundary > -1; boundary = strings.LastIndex(part[:boundary], r.suffixBoundary) {
|
||||
matches, ok = children[part[:boundary]]
|
||||
if !ok {
|
||||
matches, ok = omittedPassedChildren[part[:boundary]]
|
||||
}
|
||||
if ok {
|
||||
if suffix, ok := util.MapSeqFrom[*RpApi[Rp], Suffix](matches.Element.Apis).FlatMap(func(a *RpApi[Rp]) []Suffix {
|
||||
return a.Suffixes
|
||||
}).Find(func(s Suffix) bool {
|
||||
return string(s) == part[boundary+1:]
|
||||
}); ok {
|
||||
return matches, &suffix, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches, nil, matches != nil
|
||||
}
|
||||
|
||||
// Route processes an incoming request by matching the request path against the router's configured routes.
|
||||
// It locates the appropriate API handler based on the request path, extracts path parameters and suffixes,
|
||||
// and invokes the corresponding controller function. If the route is not found, it sets an error on the
|
||||
// request context.
|
||||
//
|
||||
// The method first attempts to locate the API using the request path. If the path contains dynamic segments
|
||||
// (e.g., path variables), it extracts and maps them to the corresponding parameters. If a suffix is present
|
||||
// in the path, it is also extracted and used to further refine the route matching.
|
||||
//
|
||||
// Once the API is located, the method invokes the `beforeController` event handler (if configured), executes
|
||||
// the API's controller function, and then invokes the `afterController` event handler. If any of these steps
|
||||
// result in an error, the error is set on the request context.
|
||||
//
|
||||
// This method is thread-safe and ensures that the routing logic is executed in a consistent manner, even
|
||||
// when multiple requests are processed concurrently.
|
||||
func (r *Router[Rp]) Route(context *Context[Rp]) {
|
||||
api, pathParams, suffix, err := r.locate(context.Rp.Path(), func(apis []*RpApi[Rp]) (*RpApi[Rp], bool) {
|
||||
if index := r.apiMatcher(context.Rp, util.MapSeqFrom[*RpApi[Rp], *Api](apis).Map(func(a *RpApi[Rp]) *Api {
|
||||
return &a.Api
|
||||
}).Collect()); index > -1 {
|
||||
return apis[index], true
|
||||
}
|
||||
return nil, false
|
||||
})
|
||||
if err == nil {
|
||||
err = r.routedHandle(api, pathParams, suffix, context)
|
||||
}
|
||||
if err != nil {
|
||||
context.Rp.SetError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router[Rp]) routedHandle(api *RpApi[Rp], pathParams map[string]Param, suffix *Suffix, context *Context[Rp]) error {
|
||||
context.SetRoutes(r, api, pathParams, suffix)
|
||||
if err := r.beforeController(context); err != nil {
|
||||
return err
|
||||
}
|
||||
api.Controller(context)
|
||||
return r.afterController(context)
|
||||
}
|
||||
|
||||
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)
|
||||
return api, nil
|
||||
}
|
||||
return nil, util.Openly(CodeNotFound, `Uid "%s" route failed, could not matched by Router`, id)
|
||||
}
|
||||
|
||||
func (r *Router[Rp]) IdRoute(context *Context[Rp]) {
|
||||
api, err := r.idLocate(context.Rp.Path())
|
||||
if err == nil {
|
||||
err = r.routedHandle(api, map[string]Param{}, nil, context)
|
||||
}
|
||||
if err != nil {
|
||||
context.Rp.SetError(err)
|
||||
}
|
||||
}
|
||||
|
||||
type ApiBranch[Rp RoutableProtocol] struct {
|
||||
Path string
|
||||
VarType int
|
||||
VarName string
|
||||
IsMidVar bool
|
||||
IsOmission bool
|
||||
Apis []*RpApi[Rp]
|
||||
VarChildren []*util.Tree[ApiBranch[Rp], string]
|
||||
OmittedPassedChildren map[string]*util.Tree[ApiBranch[Rp], string]
|
||||
}
|
||||
|
||||
func (ab ApiBranch[Rp]) Key() string {
|
||||
if index := strings.LastIndex(ab.Path, MarkPathPartSeparator); index > -1 {
|
||||
return ab.Path[index+1:]
|
||||
}
|
||||
return ab.Path
|
||||
}
|
||||
|
||||
func (ab ApiBranch[Rp]) ChildOf(parent any) bool {
|
||||
if index := strings.LastIndex(ab.Path, MarkPathPartSeparator); index > -1 {
|
||||
return ab.Path[:index] == parent.(ApiBranch[Rp]).Path
|
||||
}
|
||||
return len(parent.(ApiBranch[Rp]).Path) < 1
|
||||
}
|
||||
|
||||
func (ab ApiBranch[Rp]) EqualTo(other any) bool {
|
||||
return ab.Path == other.(ApiBranch[Rp]).Path
|
||||
}
|
||||
|
||||
func (ab ApiBranch[Rp]) fillVarType() ApiBranch[Rp] {
|
||||
key := ab.Key()
|
||||
if strings.HasPrefix(key, MarkVariableOpener) && strings.HasSuffix(key, MarkVariableClosing) {
|
||||
if ab.IsOmission {
|
||||
panic("Var path could not be omissible.")
|
||||
}
|
||||
varName := key[len(MarkVariableOpener) : len(key)-len(MarkVariableOpener)]
|
||||
if strings.HasSuffix(varName, MarkVarTypeOptional) {
|
||||
ab.VarType = VarTypeOptional
|
||||
ab.VarName = varName[:len(varName)-len(MarkVarTypeOptional)]
|
||||
} else if strings.HasSuffix(varName, MarkVarTypeEmptableVector) {
|
||||
ab.VarType = VarTypeEmptableVector
|
||||
ab.VarName = varName[:len(varName)-len(MarkVarTypeEmptableVector)]
|
||||
} else if strings.HasSuffix(varName, MarkVarTypeVector) {
|
||||
ab.VarType = VarTypeVector
|
||||
ab.VarName = varName[:len(varName)-len(MarkVarTypeVector)]
|
||||
} else {
|
||||
ab.VarType = VarTypeRequired
|
||||
ab.VarName = varName
|
||||
}
|
||||
}
|
||||
return ab
|
||||
}
|
||||
|
||||
func newApiBranch[Rp RoutableProtocol](path string, apis []*RpApi[Rp]) ApiBranch[Rp] {
|
||||
return ApiBranch[Rp]{
|
||||
Path: path,
|
||||
Apis: apis,
|
||||
OmittedPassedChildren: make(map[string]*util.Tree[ApiBranch[Rp], string]),
|
||||
}.fillVarType()
|
||||
}
|
||||
|
||||
func ProtoRouter[Rp RoutableProtocol](key string) *Router[Rp] {
|
||||
router, ok := protoRouterMap.Load(key)
|
||||
if !ok {
|
||||
router = NewRouter[Rp]()
|
||||
protoRouterMap.Store(key, router)
|
||||
}
|
||||
return router.(*Router[Rp])
|
||||
}
|
||||
|
||||
var protoRouterMap sync.Map
|
126
session/auto.go
Normal file
126
session/auto.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/idrunk/dce-go/util"
|
||||
)
|
||||
|
||||
const DefaultNewSidField = "$newid"
|
||||
const DefaultRenewIntervalSeconds uint16 = 600
|
||||
const DefaultOriginalJudgmentSeconds uint16 = 120
|
||||
const DefaultClonedInactiveJudgmentSeconds uint16 = 60
|
||||
|
||||
// AutoRenew is a generic struct that provides automatic renewal functionality for sessions.
|
||||
// It is parameterized with a type S that must implement the IfSession interface.
|
||||
// The struct contains fields to manage session renewal, including the session itself,
|
||||
// a field name for storing new session IDs, and various timing configurations for
|
||||
// renewal and judgment intervals.
|
||||
type AutoRenew[S IfSession] struct {
|
||||
S S
|
||||
newSidField string
|
||||
renewIntervalSeconds uint16
|
||||
originalJudgmentSeconds uint16
|
||||
clonedInactiveJudgmentSeconds uint16
|
||||
}
|
||||
|
||||
func NewAutoRenew[S IfSession](s S) *AutoRenew[S] {
|
||||
return &AutoRenew[S]{
|
||||
S: s,
|
||||
newSidField: DefaultNewSidField,
|
||||
renewIntervalSeconds: DefaultRenewIntervalSeconds,
|
||||
originalJudgmentSeconds: DefaultOriginalJudgmentSeconds,
|
||||
clonedInactiveJudgmentSeconds: DefaultClonedInactiveJudgmentSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AutoRenew[S]) Config(renewIntervalSeconds uint16, originalJudgmentSeconds uint16, clonedInactiveJudgmentSeconds uint16) *AutoRenew[S] {
|
||||
if renewIntervalSeconds > 0 {
|
||||
a.renewIntervalSeconds = renewIntervalSeconds
|
||||
}
|
||||
if originalJudgmentSeconds > 0 {
|
||||
a.originalJudgmentSeconds = originalJudgmentSeconds
|
||||
}
|
||||
if clonedInactiveJudgmentSeconds > 0 {
|
||||
a.clonedInactiveJudgmentSeconds = clonedInactiveJudgmentSeconds
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *AutoRenew[S]) doClone(filters map[string]any) error {
|
||||
cl, err := a.S.Clone("")
|
||||
if err != nil {
|
||||
return err
|
||||
} else if err = a.S.Renew(filters); err != nil {
|
||||
return err
|
||||
}
|
||||
cloned := cl.(S)
|
||||
_ = cloned.Touch()
|
||||
return cloned.SilentSet(a.newSidField, a.S.Id())
|
||||
}
|
||||
|
||||
// TryRenew attempts to renew the session if necessary. It returns a boolean indicating whether the session was renewed
|
||||
// and an error if any occurred during the renewal process.
|
||||
//
|
||||
// The function first checks if the session is a newborn ancestor. If it is, the function immediately returns true,
|
||||
// indicating that no renewal is needed.
|
||||
//
|
||||
// If the session is not a newborn, the function calculates the time elapsed since the session was created minus the
|
||||
// renewal interval. If this value is negative, it means the session has not yet expired, and the function attempts
|
||||
// to touch the session (update its last accessed time) and returns false, indicating no renewal was performed.
|
||||
//
|
||||
// If the session has exceeded the renewal interval, the function checks if a new session ID (newSid) is stored in the
|
||||
// session. If a newSid exists and the session has exceeded the original judgment interval, the function clones the
|
||||
// session using the newSid. It then compares the TTL (Time To Live) of the cloned session with the original session.
|
||||
// If the cloned session's TTL is less than the cloned inactive judgment interval and also less than the original
|
||||
// session's TTL, the original session is destroyed, and an error is returned indicating that the session was destroyed.
|
||||
//
|
||||
// If the cloned session's TTL is not less than the cloned inactive judgment interval, the cloned session is destroyed,
|
||||
// and the original session is renewed by cloning it again with the newSid field cleared. The function then returns true,
|
||||
// indicating that the session was renewed.
|
||||
//
|
||||
// If no newSid is found, the function attempts to clone the session without any filters and returns true if successful.
|
||||
func (a *AutoRenew[S]) TryRenew() (bool, error) {
|
||||
if a.S.Newborn() {
|
||||
// directly return true if is a newborn ancestor
|
||||
return true, nil
|
||||
}
|
||||
secondFromRenew := time.Now().Unix() - a.S.CreateStamp() - int64(a.renewIntervalSeconds)
|
||||
if secondFromRenew < 0 {
|
||||
// directly touch if not expired
|
||||
_ = a.S.TryTouch()
|
||||
return false, nil
|
||||
} else if newSid, e := a.S.SilentGet(a.newSidField); e == nil {
|
||||
if secondFromRenew > int64(a.originalJudgmentSeconds) {
|
||||
cl, err := a.S.Clone(newSid.(string))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
cloned := cl.(S)
|
||||
newTp, err := cloned.TtlPassed()
|
||||
if err != nil {
|
||||
newTp = math.MaxUint32
|
||||
}
|
||||
if newTp < uint32(a.clonedInactiveJudgmentSeconds) {
|
||||
if oldTp, err := a.S.TtlPassed(); err == nil && newTp < oldTp {
|
||||
if err := a.S.Destroy(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, util.Closed0(`session "%s" was destroyed, unable to continue use`, a.S.Id())
|
||||
}
|
||||
}
|
||||
if err = cloned.Destroy(); err != nil {
|
||||
return false, err
|
||||
} else if err = a.doClone(map[string]any{a.newSidField: nil}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
_ = a.S.TryTouch()
|
||||
return false, nil
|
||||
} else if e = a.doClone(map[string]any{}); e != nil {
|
||||
return false, e
|
||||
}
|
||||
return true, nil
|
||||
}
|
106
session/connection.go
Normal file
106
session/connection.go
Normal file
@@ -0,0 +1,106 @@
|
||||
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,
|
||||
// and cloning for request-specific sessions.
|
||||
type ConnectionSession struct {
|
||||
*BasicSession
|
||||
IfConnection
|
||||
// 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
|
||||
serverField string
|
||||
clientField string
|
||||
// log the connection info to let the request session to store it
|
||||
serverToBind string
|
||||
clientToBind string
|
||||
}
|
||||
|
||||
func NewConnectionSession(basic *BasicSession) *ConnectionSession {
|
||||
return &ConnectionSession{BasicSession: basic, serverField: DefaultServerField, clientField: DefaultClientField}
|
||||
}
|
||||
|
||||
func (c *ConnectionSession) CloneConnection(cloned IfConnection, basic *BasicSession) *ConnectionSession {
|
||||
connection := *c
|
||||
connection.IfConnection = cloned
|
||||
connection.BasicSession = basic
|
||||
return &connection
|
||||
}
|
||||
|
||||
func (c *ConnectionSession) Connect(server string, client string, entry bool) *ConnectionSession {
|
||||
c.serverToBind = server
|
||||
c.clientToBind = client
|
||||
if entry {
|
||||
c.ty = typeEntryShadow
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *ConnectionSession) Disconnect() error {
|
||||
if err := c.SilentDel(c.serverField); err != nil {
|
||||
return err
|
||||
} else if err = c.SilentDel(c.clientField); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConnectionSession) Request() bool {
|
||||
return c.ty == typeRequest
|
||||
}
|
||||
|
||||
func (c *ConnectionSession) UpdateShadow(newSid string) error {
|
||||
return c.ReMeta(newSid)
|
||||
}
|
||||
|
||||
func (c *ConnectionSession) CloneForRequest(sid string) (any, error) {
|
||||
cl, err := c.Clone(sid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cloned := cl.(IfConnection)
|
||||
cloned.handleClonedRequest(c)
|
||||
return cloned, nil
|
||||
}
|
||||
|
||||
func (c *ConnectionSession) handleClonedRequest(original *ConnectionSession) {
|
||||
c.newborn = false
|
||||
c.shadow = original
|
||||
c.ty = typeRequest
|
||||
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 {
|
||||
original.serverToBind, original.clientToBind = "", ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type IfConnection interface {
|
||||
handleClonedRequest(original *ConnectionSession)
|
||||
}
|
3
session/go.mod
Normal file
3
session/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/idrunk/dce-go/session
|
||||
|
||||
go 1.23.3
|
5
session/redises/go.mod
Normal file
5
session/redises/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/idrunk/dce-go/session/redises
|
||||
|
||||
go 1.23.3
|
||||
|
||||
require github.com/redis/go-redis/v9 v9.7.0
|
199
session/redises/redis.go
Normal file
199
session/redises/redis.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package redises
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/idrunk/dce-go/session"
|
||||
"github.com/idrunk/dce-go/util"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Session is a generic struct that represents a session in a Redis-backed session management system.
|
||||
// It combines functionality from BasicSession, UserSession, and ConnectionSession to provide
|
||||
// a comprehensive session management solution. The struct is parameterized with a type U that
|
||||
// implements the UidGetter interface, allowing it to work with any user type that provides a UID.
|
||||
//
|
||||
// The Session struct holds references to:
|
||||
// - BasicSession: Manages basic session properties like session ID and TTL.
|
||||
// - UserSession: Manages user-specific session data and operations.
|
||||
// - ConnectionSession: Manages connection-related session data and operations.
|
||||
// - redis: A Redis client used to interact with the Redis database.
|
||||
// - ctx: A context.Context used for Redis operations.
|
||||
//
|
||||
// This struct provides methods for session manipulation, including setting, getting, and deleting
|
||||
// session fields, as well as more complex operations like session renewal, cloning, and synchronization
|
||||
// with user data.
|
||||
type Session[U session.UidGetter] struct {
|
||||
*session.BasicSession
|
||||
*session.UserSession[U]
|
||||
*session.ConnectionSession
|
||||
redis *redis.Client
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewSession[U session.UidGetter](rdb *redis.Client, sidPool []string, ttlMinutes uint16) (*Session[U], error) {
|
||||
basic, err := session.NewBasicSession(sidPool, ttlMinutes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rs := &Session[U]{
|
||||
BasicSession: basic,
|
||||
UserSession: session.NewUserSession[U](basic),
|
||||
ConnectionSession: session.NewConnectionSession(basic),
|
||||
redis: rdb,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
rs.BasicSession.IfSession = rs
|
||||
rs.UserSession.IfUserSession = rs
|
||||
rs.ConnectionSession.IfConnection = rs
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func redisGenKey(prefix string, id string) string {
|
||||
return fmt.Sprintf("%s:%s", prefix, id)
|
||||
}
|
||||
|
||||
func (r *Session[U]) Key() string {
|
||||
return redisGenKey(r.SidName, r.BasicSession.Id())
|
||||
}
|
||||
|
||||
func (r *Session[U]) SilentSet(field string, value any) error {
|
||||
return r.redis.HSet(r.ctx, r.Key(), field, value).Err()
|
||||
}
|
||||
|
||||
func (r *Session[U]) SilentGet(field string) (any, error) {
|
||||
return r.redis.HGet(r.ctx, r.Key(), field).Result()
|
||||
}
|
||||
|
||||
func (r *Session[U]) SilentDel(field string) error {
|
||||
return r.redis.HDel(r.ctx, r.Key(), field).Err()
|
||||
}
|
||||
|
||||
func (r *Session[U]) Destroy() error {
|
||||
if err := r.Unmapping(); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.redis.Del(r.ctx, r.Key()).Err()
|
||||
}
|
||||
|
||||
func (r *Session[U]) Touch() error {
|
||||
return r.redis.Expire(r.ctx, r.Key(), time.Duration(r.TtlSeconds())*time.Second).Err()
|
||||
}
|
||||
|
||||
func (r *Session[U]) Load(data map[string]any) error {
|
||||
return r.redis.HSet(r.ctx, r.Key(), data).Err()
|
||||
}
|
||||
|
||||
func (r *Session[U]) Raw() (map[string]any, error) {
|
||||
data, err := r.redis.HGetAll(r.ctx, r.Key()).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]any)
|
||||
for k, v := range data {
|
||||
result[k] = v
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Session[U]) TtlPassed() (uint32, error) {
|
||||
ttl, err := r.redis.TTL(r.ctx, r.Key()).Result()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if ttl.Seconds() < 1 {
|
||||
return 0, util.Closed0("ttl was not initialized yet.")
|
||||
}
|
||||
return r.TtlSeconds() - uint32(ttl.Seconds()), nil
|
||||
}
|
||||
|
||||
func (r *Session[U]) Renew(filters map[string]any) error {
|
||||
err := r.UserSession.Renew(filters)
|
||||
if err == nil && r.Request() {
|
||||
return r.UpdateShadow(r.BasicSession.Id())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Session[U]) Clone(id string) (any, error) {
|
||||
cloned := *r
|
||||
cl := &cloned
|
||||
basic, err := r.CloneBasic(cl, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cl.BasicSession = basic
|
||||
cl.UserSession = r.CloneUser(cl, basic)
|
||||
cl.ConnectionSession = r.CloneConnection(cl, basic)
|
||||
return cl, nil
|
||||
}
|
||||
|
||||
func redisGenUserKey(userPrefix string, id uint64) string {
|
||||
return fmt.Sprintf("%s:%d", userPrefix, id)
|
||||
}
|
||||
|
||||
func (r *Session[U]) UserKey() (string, error) {
|
||||
uid, err := r.Uid()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return redisGenUserKey(r.KeyPrefix, uid), nil
|
||||
}
|
||||
|
||||
func (r *Session[U]) Mapping() error {
|
||||
userKey, err := r.UserKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pipe := r.redis.Pipeline()
|
||||
pipe.SAdd(r.ctx, userKey, r.BasicSession.Id())
|
||||
pipe.Expire(r.ctx, userKey, time.Duration(session.MappingTtlSeconds)*time.Second)
|
||||
_, err = pipe.Exec(r.ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Session[U]) Unmapping() error {
|
||||
userKey, err := r.UserKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.redis.SRem(r.ctx, userKey, r.BasicSession.Id()).Err()
|
||||
}
|
||||
|
||||
func (r *Session[U]) Sync(user *U) error {
|
||||
userJson, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sids, err := r.Sids((*user).Uid())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pipe := r.redis.Pipeline()
|
||||
for _, sid := range sids {
|
||||
pipe.HSet(r.ctx, redisGenKey(r.SidName, sid), r.UserField, userJson)
|
||||
}
|
||||
_, err = pipe.Exec(r.ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Session[U]) AllSid(uid uint64) ([]string, error) {
|
||||
return r.redis.SMembers(r.ctx, redisGenUserKey(r.KeyPrefix, uid)).Result()
|
||||
}
|
||||
|
||||
func (r *Session[U]) FilterSids(uid uint64, sids []string) ([]string, error) {
|
||||
userKey := redisGenUserKey(r.KeyPrefix, uid)
|
||||
var filtered []string
|
||||
pipe := r.redis.Pipeline()
|
||||
for _, sid := range sids {
|
||||
if r.redis.Exists(r.ctx, redisGenKey(r.SidName, sid)).Val() > 0 {
|
||||
filtered = append(filtered, sid)
|
||||
} else {
|
||||
pipe.SRem(r.ctx, userKey, sid)
|
||||
}
|
||||
}
|
||||
_, err := pipe.Exec(r.ctx)
|
||||
return filtered, err
|
||||
}
|
318
session/session.go
Normal file
318
session/session.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/idrunk/dce-go/util"
|
||||
)
|
||||
|
||||
const DefaultIdName = "dcesid"
|
||||
const MinSidLen = 76
|
||||
const DefaultTtlMinutes = 60
|
||||
|
||||
// BasicSession is the core implementation of the IfSession interface.
|
||||
// It represents a session with a unique session ID (SID), a creation timestamp,
|
||||
// and a time-to-live (TTL) in minutes. The session can be newly created or loaded
|
||||
// from an existing session. It supports operations like setting, getting, and
|
||||
// deleting session fields, as well as renewing the session with new metadata.
|
||||
// The session can also be cloned, and its data can be serialized or deserialized
|
||||
// as needed. The BasicSession struct is designed to be extended or embedded in
|
||||
// other session implementations to provide additional functionality.
|
||||
type BasicSession struct {
|
||||
IfSession
|
||||
SidName string
|
||||
ttlMinutes uint16
|
||||
sid string
|
||||
createStamp int64
|
||||
touches bool
|
||||
newborn bool
|
||||
sidPool []string
|
||||
}
|
||||
|
||||
func NewBasicSession(sidPool []string, ttlMinutes uint16) (*BasicSession, error) {
|
||||
if len(sidPool) == 0 && ttlMinutes == 0 {
|
||||
panic(`"NewWithSid()" was called with an empty "sidPool"`)
|
||||
}
|
||||
var err error
|
||||
var sid string
|
||||
var createStamp int64
|
||||
newborn := true
|
||||
if len(sidPool) > 0 && sidPool[0] != "" {
|
||||
newborn = false
|
||||
sid = sidPool[0]
|
||||
sidPool = slices.Delete(sidPool, 0, 1)
|
||||
if ttlMinutes, createStamp, err = parseSid(sid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if sid, createStamp, err = generateSid(ttlMinutes, &[]string{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BasicSession{
|
||||
SidName: DefaultIdName,
|
||||
ttlMinutes: ttlMinutes,
|
||||
createStamp: createStamp,
|
||||
sid: sid,
|
||||
touches: false,
|
||||
newborn: newborn,
|
||||
sidPool: sidPool,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseSid(sid string) (uint16, int64, error) {
|
||||
if len(sid) < MinSidLen {
|
||||
return 0, 0, util.Closed0(`invalid sid "%s", less then %d chars`, sid, MinSidLen)
|
||||
}
|
||||
ttlMinutes, err := strconv.ParseUint(sid[64:68], 16, 16)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
stamp, err := strconv.ParseInt(sid[68:], 16, 64)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return uint16(ttlMinutes), stamp, nil
|
||||
}
|
||||
|
||||
func generateSid(ttlMinutes uint16, sidPool *[]string) (string, int64, error) {
|
||||
arrSidPool := *sidPool
|
||||
if len(arrSidPool) == 0 || arrSidPool[0] == "" {
|
||||
sid, createStamp := GenSid(ttlMinutes)
|
||||
return sid, createStamp, nil
|
||||
}
|
||||
sid := arrSidPool[0]
|
||||
*sidPool = slices.Delete(arrSidPool, 0, 1)
|
||||
_, stamp, err := parseSid(sid)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return sid, stamp, nil
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func (b *BasicSession) CloneBasic(cloned IfSession, id string) (*BasicSession, error) {
|
||||
basic := *b
|
||||
// if passed an id then means with a special id to clone, or else directly clone with original id
|
||||
if id != "" {
|
||||
if err := basic.ReMeta(id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
basic.IfSession = cloned
|
||||
return &basic, nil
|
||||
}
|
||||
|
||||
func (b *BasicSession) Id() string {
|
||||
return b.sid
|
||||
}
|
||||
|
||||
func (b *BasicSession) Key() string {
|
||||
return b.Id()
|
||||
}
|
||||
|
||||
func (b *BasicSession) CreateStamp() int64 {
|
||||
return b.createStamp
|
||||
}
|
||||
|
||||
func (b *BasicSession) TtlSeconds() uint32 {
|
||||
return uint32(b.ttlMinutes) * 60
|
||||
}
|
||||
|
||||
func (b *BasicSession) Newborn() bool {
|
||||
return b.newborn
|
||||
}
|
||||
|
||||
func (b *BasicSession) NeedSerial() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
return b.SilentSet(field, val)
|
||||
}
|
||||
|
||||
func (b *BasicSession) Get(field string, target any) error {
|
||||
val, err := b.SilentGet(field)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if err = b.TryTouch(); err != nil {
|
||||
return err
|
||||
}
|
||||
return TryUnmarshal(val, target, b.IfSession.NeedSerial())
|
||||
}
|
||||
|
||||
func TryMarshal(val any, needSerial bool) (any, error) {
|
||||
if !needSerial {
|
||||
return val, nil
|
||||
}
|
||||
return json.Marshal(val)
|
||||
}
|
||||
|
||||
func TryUnmarshal(val any, target any, needSerial bool) error {
|
||||
if !needSerial {
|
||||
rv := reflect.ValueOf(target)
|
||||
rv.Elem().Set(reflect.ValueOf(val))
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
if v, ok := val.(string); ok {
|
||||
err = json.Unmarshal([]byte(v), target)
|
||||
} else if v, ok := val.([]byte); ok {
|
||||
err = json.Unmarshal(v, target)
|
||||
} else {
|
||||
return fmt.Errorf("TryUnmarshal: unknown type %T", val)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *BasicSession) Del(field string) error {
|
||||
err := b.SilentDel(field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.TryTouch()
|
||||
}
|
||||
|
||||
func (b *BasicSession) TryTouch() error {
|
||||
if !b.touches {
|
||||
if err := b.Touch(); err != nil {
|
||||
return err
|
||||
}
|
||||
b.touches = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BasicSession) ReMeta(sid string) error {
|
||||
b.touches = false
|
||||
if len(sid) > 0 {
|
||||
ttl, createStamp, err := parseSid(sid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.ttlMinutes, b.createStamp, b.sid = ttl, createStamp, sid
|
||||
} else {
|
||||
id, createStamp, err := generateSid(b.ttlMinutes, &b.sidPool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.sid, b.createStamp = id, createStamp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BasicSession) Renew(filters map[string]any) error {
|
||||
raw, err := b.Raw()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range filters {
|
||||
if v == nil {
|
||||
delete(raw, k)
|
||||
} else {
|
||||
if val, err := TryMarshal(v, b.IfSession.NeedSerial()); err == nil {
|
||||
raw[k] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = b.ReMeta(""); err != nil {
|
||||
return err
|
||||
} else if len(raw) == 0 {
|
||||
return nil
|
||||
} else if err = b.Load(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.TryTouch()
|
||||
}
|
||||
|
||||
func (b *BasicSession) ListBySids(sids []string) ([]any, error) {
|
||||
var sessions []any
|
||||
for _, sid := range sids {
|
||||
if cl, err := b.Clone(sid); err == nil {
|
||||
sessions = append(sessions, cl)
|
||||
}
|
||||
}
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
type IfSession interface {
|
||||
// Id returns the session ID (SID) associated with the session.
|
||||
Id() string
|
||||
|
||||
// Key returns the key used to identify the session, which is typically the session ID.
|
||||
Key() string
|
||||
|
||||
// CreateStamp returns the timestamp (in Unix time) when the session was created.
|
||||
CreateStamp() int64
|
||||
|
||||
// TtlSeconds returns the time-to-live (TTL) for the session in seconds.
|
||||
TtlSeconds() uint32
|
||||
|
||||
// Newborn returns a boolean indicating whether the session is newly created (true) or was loaded from an existing session (false).
|
||||
Newborn() bool
|
||||
|
||||
// NeedSerial returns a boolean indicating whether the session data needs to be serialized (e.g., JSON) when stored or retrieved.
|
||||
NeedSerial() bool
|
||||
|
||||
// Set assigns a value to a specific field in the session. It also handles serialization if needed and triggers a touch event.
|
||||
Set(field string, value any) error
|
||||
|
||||
// Get retrieves the value of a specific field from the session and assigns it to the target. It also triggers a touch event.
|
||||
Get(field string, target any) error
|
||||
|
||||
// Del removes a specific field from the session. It also triggers a touch event.
|
||||
Del(field string) error
|
||||
|
||||
// SilentSet assigns a value to a specific field in the session without triggering a touch event.
|
||||
SilentSet(field string, value any) error
|
||||
|
||||
// SilentGet retrieves the value of a specific field from the session without triggering a touch event.
|
||||
SilentGet(field string) (any, error)
|
||||
|
||||
// SilentDel removes a specific field from the session without triggering a touch event.
|
||||
SilentDel(field string) error
|
||||
|
||||
// Destroy removes the session and all its associated data.
|
||||
Destroy() error
|
||||
|
||||
// Touch updates the session's last accessed timestamp, effectively extending its TTL.
|
||||
Touch() error
|
||||
|
||||
// TryTouch attempts to update the session's last accessed timestamp if it hasn't been touched recently.
|
||||
TryTouch() error
|
||||
|
||||
// Load populates the session with data from the provided map.
|
||||
Load(data map[string]any) error
|
||||
|
||||
// Raw returns the raw session data as a map.
|
||||
Raw() (map[string]any, error)
|
||||
|
||||
// TtlPassed returns the amount of time (in seconds) that has passed since the session was last accessed.
|
||||
TtlPassed() (uint32, error)
|
||||
|
||||
// Renew updates the session with new data based on the provided filters and resets its metadata (e.g., SID and creation timestamp).
|
||||
Renew(filters map[string]any) error
|
||||
|
||||
// Clone creates a new session instance with the same properties as the current session, optionally with a new session ID.
|
||||
Clone(id string) (any, error)
|
||||
|
||||
// ListBySids retrieves a list of sessions based on the provided session IDs.
|
||||
ListBySids(sids []string) ([]any, error)
|
||||
}
|
246
session/shm.go
Normal file
246
session/shm.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/idrunk/dce-go/util"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// sessionMapping map[string]*shmMeta
|
||||
var sessionMapping = util.NewStruct[sync.Map]()
|
||||
|
||||
// userMapping map[string][]string
|
||||
var userMapping = util.NewStruct[sync.Map]()
|
||||
|
||||
func ShmDumpMapping() {
|
||||
sessionMapping.Range(func(sid, meta any) bool {
|
||||
fmt.Printf("%s\n%v\n", sid, meta)
|
||||
return true
|
||||
})
|
||||
userMapping.Range(func(uid, sids any) bool {
|
||||
fmt.Printf("%s\n%v\n", uid, sids)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
type shmMeta struct {
|
||||
data map[string]any
|
||||
expireStamp int64
|
||||
}
|
||||
|
||||
type ShmSession[U UidGetter] struct {
|
||||
*BasicSession
|
||||
*UserSession[U]
|
||||
*ConnectionSession
|
||||
}
|
||||
|
||||
func NewShmSession[U UidGetter](sidPool []string, ttlMinutes uint16) (*ShmSession[U], error) {
|
||||
basic, err := NewBasicSession(sidPool, ttlMinutes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rs := &ShmSession[U]{
|
||||
BasicSession: basic,
|
||||
UserSession: NewUserSession[U](basic),
|
||||
ConnectionSession: NewConnectionSession(basic),
|
||||
}
|
||||
rs.BasicSession.IfSession = rs
|
||||
rs.UserSession.IfUserSession = rs
|
||||
rs.ConnectionSession.IfConnection = rs
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) meta(autoGen bool) (*shmMeta, error) {
|
||||
if m, ok := sessionMapping.Load(s.Key()); ok {
|
||||
return m.(*shmMeta), nil
|
||||
} else if autoGen {
|
||||
sessionMapping.Store(s.Key(), &shmMeta{data: make(map[string]any)})
|
||||
return s.meta(autoGen)
|
||||
}
|
||||
return nil, util.Closed0(`Sid "%s" could not be found id mapping`, s.Key())
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) NeedSerial() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) SilentSet(field string, value any) error {
|
||||
meta, _ := s.meta(true)
|
||||
meta.data[field] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) SilentGet(field string) (any, error) {
|
||||
if meta, err := s.meta(false); err != nil {
|
||||
return nil, err
|
||||
} else if v, ok := meta.data[field]; ok {
|
||||
return v, nil
|
||||
}
|
||||
return nil, util.Silent("No session value with key \"%s\"", field)
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) SilentDel(field string) error {
|
||||
if meta, err := s.meta(false); err == nil {
|
||||
delete(meta.data, field)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) Destroy() error {
|
||||
sessionMapping.Delete(s.Key())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) Touch() error {
|
||||
meta, err := s.meta(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
meta.expireStamp = time.Now().Unix() + int64(s.TtlSeconds())
|
||||
s.tryClear()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) Load(data map[string]any) error {
|
||||
meta, _ := s.meta(true)
|
||||
meta.data = data
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) Raw() (map[string]any, error) {
|
||||
if meta, err := s.meta(false); err == nil {
|
||||
return meta.data, nil
|
||||
}
|
||||
return make(map[string]any), nil
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) TtlPassed() (uint32, error) {
|
||||
meta, err := s.meta(false)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if meta.expireStamp < 1 {
|
||||
return 0, util.Closed0("ttl was not initialized yet.")
|
||||
}
|
||||
return uint32(time.Now().Unix() - meta.expireStamp + int64(s.TtlSeconds())), nil
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) Renew(filters map[string]any) error {
|
||||
err := s.UserSession.Renew(filters)
|
||||
if err == nil && s.Request() {
|
||||
return s.UpdateShadow(s.BasicSession.Id())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) Clone(id string) (any, error) {
|
||||
cloned := *s
|
||||
cl := &cloned
|
||||
basic, err := s.CloneBasic(cl, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cl.BasicSession = basic
|
||||
cl.UserSession = s.CloneUser(cl, basic)
|
||||
cl.ConnectionSession = s.CloneConnection(cl, basic)
|
||||
return cl, nil
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
func (s *ShmSession[U]) tryClear() {
|
||||
if !mu.TryLock() || rand.UintN(10) > 2 {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer mu.Unlock()
|
||||
now := time.Now().Unix()
|
||||
sessionMapping.Range(func(sid, meta any) bool {
|
||||
m := meta.(*shmMeta)
|
||||
if m.expireStamp > 0 && m.expireStamp <= now {
|
||||
sessionMapping.Delete(sid)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
func shmGenUserKey(id uint64) string {
|
||||
return strconv.FormatUint(id, 10)
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) filterSids(userKey string) []string {
|
||||
sids := make([]string, 0, 1)
|
||||
if v, ok := userMapping.LoadOrStore(userKey, make([]string, 1)); ok {
|
||||
sids = v.([]string)
|
||||
for i := len(sids) - 1; i >= 0; i-- {
|
||||
// If the session to witch the sid belongs does not exist, remove it
|
||||
if _, ok = sessionMapping.Load(sids[i]); !ok {
|
||||
sids = slices.Delete(sids, i, i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return sids
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) Mapping() error {
|
||||
userKey, err := s.UserKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sids := append(s.filterSids(userKey), s.Id())
|
||||
userMapping.Store(userKey, sids)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) Unmapping() error {
|
||||
userKey, err := s.UserKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sids := s.filterSids(userKey)
|
||||
if index := slices.Index(sids, s.Id()); index > -1 {
|
||||
sids = slices.Delete(sids, index, index+1)
|
||||
if len(sids) == 0 {
|
||||
userMapping.Delete(userKey)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
userMapping.Store(userKey, sids)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) Sync(user *U) error {
|
||||
sids, err := s.Sids((*user).Uid())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, sid := range sids {
|
||||
if meta, ok := sessionMapping.Load(sid); ok {
|
||||
m := meta.(*shmMeta)
|
||||
m.data[s.UserField] = user
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) AllSid(uid uint64) ([]string, error) {
|
||||
userKey := shmGenUserKey(uid)
|
||||
if v, ok := userMapping.Load(userKey); ok {
|
||||
return v.([]string), nil
|
||||
}
|
||||
return nil, util.Silent("No mapping with key \"%s\"", userKey)
|
||||
}
|
||||
|
||||
func (s *ShmSession[U]) FilterSids(uid uint64, sids []string) ([]string, error) {
|
||||
allValid := s.filterSids(shmGenUserKey(uid))
|
||||
for i := len(sids) - 1; i >= 0; i-- {
|
||||
if !slices.Contains(allValid, sids[i]) {
|
||||
sids = slices.Delete(sids, i, i+1)
|
||||
}
|
||||
}
|
||||
return sids, nil
|
||||
}
|
227
session/user.go
Normal file
227
session/user.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/idrunk/dce-go/util"
|
||||
)
|
||||
|
||||
const DefaultUserPrefix = "dceusmap"
|
||||
|
||||
const DefaultUserField = "$user"
|
||||
|
||||
const MappingTtlSeconds = 60 * 60 * 24 * 7
|
||||
|
||||
const (
|
||||
notLoaded int8 = iota - 1
|
||||
loadedNone
|
||||
loadedSome
|
||||
)
|
||||
|
||||
// UserSession is a generic struct that represents a session associated with a user.
|
||||
// It embeds a BasicSession and implements the IfUserSession interface, providing
|
||||
// functionality to manage user sessions, including login, logout, and session renewal.
|
||||
// The struct is parameterized with a type U that must implement the UidGetter interface,
|
||||
// which requires a method to retrieve the user's unique identifier (UID).
|
||||
//
|
||||
// Fields:
|
||||
// - BasicSession: The embedded basic session that provides core session management capabilities.
|
||||
// - IfUserSession[U]: The interface that defines user-specific session operations.
|
||||
// - KeyPrefix: A string prefix used for generating user-specific keys.
|
||||
// - UserField: A string field name used to store user data within the session.
|
||||
// - loadState: An int8 value indicating the current state of user data loading.
|
||||
// - user: The user data of type U associated with the session.
|
||||
type UserSession[U UidGetter] struct {
|
||||
*BasicSession
|
||||
IfUserSession[U]
|
||||
KeyPrefix string
|
||||
UserField string
|
||||
loadState int8
|
||||
user U
|
||||
}
|
||||
|
||||
func NewUserSession[U UidGetter](basic *BasicSession) *UserSession[U] {
|
||||
return &UserSession[U]{BasicSession: basic, KeyPrefix: DefaultUserPrefix, UserField: DefaultUserField, loadState: notLoaded}
|
||||
}
|
||||
|
||||
func (s *UserSession[U]) CloneUser(cloned IfUserSession[U], basic *BasicSession) *UserSession[U] {
|
||||
user := *s
|
||||
user.IfUserSession = cloned
|
||||
user.BasicSession = basic
|
||||
user.user = util.NewStruct[U]()
|
||||
user.loadState = notLoaded
|
||||
return &user
|
||||
}
|
||||
|
||||
func (s *UserSession[U]) UserKey() (string, error) {
|
||||
uid, err := s.Uid()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strconv.FormatUint(uid, 10), nil
|
||||
}
|
||||
|
||||
func (s *UserSession[U]) User() (U, bool) {
|
||||
if s.loadState == notLoaded {
|
||||
if val, err := s.SilentGet(s.UserField); err == nil {
|
||||
var user U
|
||||
if err = TryUnmarshal(val, &user, s.IfSession.NeedSerial()); err == nil {
|
||||
s.user = user
|
||||
s.loadState = loadedSome
|
||||
return s.user, true
|
||||
}
|
||||
}
|
||||
s.loadState = loadedNone
|
||||
}
|
||||
return s.user, s.loadState > loadedNone
|
||||
}
|
||||
|
||||
func (s *UserSession[U]) Uid() (uint64, error) {
|
||||
if u, ok := s.User(); !ok {
|
||||
return 0, util.Closed0("User not loaded, cannot get uid")
|
||||
} else {
|
||||
return u.Uid(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserSession[U]) doLogin(user *U, ttlMinutes uint16) error {
|
||||
cl, err := s.Clone("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cloned := cl.(IfSession)
|
||||
if ttlMinutes > 0 {
|
||||
s.ttlMinutes = ttlMinutes
|
||||
}
|
||||
if user != nil {
|
||||
s.user = *user
|
||||
s.loadState = loadedSome
|
||||
}
|
||||
filters := make(map[string]any)
|
||||
if s.loadState == loadedSome {
|
||||
filters[s.UserField] = s.user
|
||||
}
|
||||
// should call this via the interface, it will locate from the implementation
|
||||
// for login just directly regenerate a new sid
|
||||
if err := s.IfSession.Renew(filters); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = cloned.Destroy()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserSession[U]) Login(user U, ttlMinutes uint16) error {
|
||||
return s.doLogin(&user, ttlMinutes)
|
||||
}
|
||||
|
||||
func (s *UserSession[U]) AutoLogin() error {
|
||||
return s.doLogin(nil, 0)
|
||||
}
|
||||
|
||||
func (s *UserSession[U]) Logout() error {
|
||||
if s.loadState < loadedSome {
|
||||
return nil
|
||||
} else if err := s.Unmapping(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.user = util.NewStruct[U]()
|
||||
s.loadState = loadedNone
|
||||
return s.SilentDel(s.UserField)
|
||||
}
|
||||
|
||||
func (s *UserSession[U]) Sids(uid uint64) ([]string, error) {
|
||||
sids, err := s.AllSid(uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.FilterSids(uid, sids)
|
||||
}
|
||||
|
||||
func (s *UserSession[U]) Renew(filters map[string]any) error {
|
||||
if err := s.BasicSession.Renew(filters); err != nil {
|
||||
return err
|
||||
}
|
||||
s.loadState = notLoaded
|
||||
_ = s.Mapping()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserSession[U]) ListByUid(uid uint64) ([]any, error) {
|
||||
if sids, err := s.Sids(uid); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return s.ListBySids(sids)
|
||||
}
|
||||
}
|
||||
|
||||
type IfUserSession[U UidGetter] interface {
|
||||
// Mapping establishes a mapping between the user and the session.
|
||||
// It returns an error if the mapping fails.
|
||||
Mapping() error
|
||||
|
||||
// Unmapping removes the mapping between the user and the session.
|
||||
// It returns an error if the unmapping fails.
|
||||
Unmapping() error
|
||||
|
||||
// Sync synchronizes the session with the provided user data.
|
||||
// It returns an error if the synchronization fails.
|
||||
Sync(user *U) error
|
||||
|
||||
// AllSid retrieves all session IDs (SIDs) associated with the given user ID (UID).
|
||||
// It returns a slice of SIDs and an error if the retrieval fails.
|
||||
AllSid(uid uint64) ([]string, error)
|
||||
|
||||
// FilterSids filters the provided session IDs (SIDs) based on the given user ID (UID).
|
||||
// It returns a filtered slice of SIDs and an error if the filtering fails.
|
||||
FilterSids(uid uint64, sids []string) ([]string, error)
|
||||
|
||||
// UserKey generates a unique key for the user based on their UID.
|
||||
// It returns the generated key and an error if the key generation fails.
|
||||
UserKey() (string, error)
|
||||
|
||||
// User retrieves the user associated with the session.
|
||||
// It returns the user and a boolean indicating whether the user was successfully loaded.
|
||||
User() (U, bool)
|
||||
|
||||
// Uid retrieves the UID of the user associated with the session.
|
||||
// It returns the UID and an error if the UID retrieval fails.
|
||||
Uid() (uint64, error)
|
||||
|
||||
// doLogin performs the login operation for the session, optionally with a provided user and TTL (Time-To-Live) in minutes.
|
||||
// It returns an error if the login operation fails.
|
||||
doLogin(user *U, ttlMinutes uint16) error
|
||||
|
||||
// Login logs in the provided user with the specified TTL (Time-To-Live) in minutes.
|
||||
// It returns an error if the login operation fails.
|
||||
Login(user U, ttlMinutes uint16) error
|
||||
|
||||
// AutoLogin attempts to automatically log in the user without providing explicit user data.
|
||||
// It returns an error if the auto-login operation fails.
|
||||
AutoLogin() error
|
||||
|
||||
// Logout logs out the user from the session.
|
||||
// It returns an error if the logout operation fails.
|
||||
Logout() error
|
||||
|
||||
// Sids retrieves all session IDs (SIDs) associated with the given user ID (UID).
|
||||
// It returns a slice of SIDs and an error if the retrieval fails.
|
||||
Sids(uid uint64) ([]string, error)
|
||||
|
||||
// ListByUid retrieves a list of session data associated with the given user ID (UID).
|
||||
// It returns a slice of session data and an error if the retrieval fails.
|
||||
ListByUid(uid uint64) ([]any, error)
|
||||
}
|
||||
|
||||
type UidGetter interface {
|
||||
Uid() uint64
|
||||
}
|
||||
|
||||
type SimpleUser struct {
|
||||
Id uint64 `json:"id"`
|
||||
RoleId uint16 `json:"role_id,omitempty"`
|
||||
Nick string `json:"nick"`
|
||||
}
|
||||
|
||||
func (s SimpleUser) Uid() uint64 {
|
||||
return s.Id
|
||||
}
|
60
util/config.go
Normal file
60
util/config.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
scalars sync.Map
|
||||
vectors sync.Map
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
scalars: NewStruct[sync.Map](),
|
||||
vectors: NewStruct[sync.Map](),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) SetScalar(key string, value any) *Config {
|
||||
c.scalars.Store(key, value)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Config) Scalar(key string) (any, bool) {
|
||||
v, ok := c.scalars.Load(key)
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (c *Config) SetVector(key string, value []any) *Config {
|
||||
c.vectors.Store(key, value)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Config) Vector(key string) []any {
|
||||
if v, ok := c.vectors.Load(key); ok {
|
||||
return v.([]any)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) VecPut(key string, value any) *Config {
|
||||
if vec, ok := c.vectors.LoadOrStore(key, []any{}); ok {
|
||||
vector := vec.([]any)
|
||||
c.vectors.Store(key, append(vector, value))
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Config) VecPutIfAbsent(key string, value any) []any {
|
||||
var vector []any
|
||||
if vec, ok := c.vectors.Load(key); ok {
|
||||
vector = vec.([]any)
|
||||
}
|
||||
if !slices.Contains(vector, value) {
|
||||
vector = append(vector, value)
|
||||
c.vectors.Store(key, vector)
|
||||
}
|
||||
return vector
|
||||
}
|
3
util/go.mod
Normal file
3
util/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/idrunk/dce-go/util
|
||||
|
||||
go 1.23.3
|
201
util/iter.go
Normal file
201
util/iter.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"maps"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func NewSeq[V any](seq iter.Seq[V]) Sequence[V, V, int8] {
|
||||
return NewMapSeq[V, V](seq)
|
||||
}
|
||||
|
||||
func SeqFrom[V any](seq []V) Sequence[V, V, int8] {
|
||||
return MapSeqFrom[V, V](seq)
|
||||
}
|
||||
|
||||
func NewMapSeq[V, T any](seq iter.Seq[V]) Sequence[V, T, int8] {
|
||||
return Sequence[V, T, int8]{seq: seq}
|
||||
}
|
||||
|
||||
func MapSeqFrom[V, T any](seq []V) Sequence[V, T, int8] {
|
||||
return Sequence[V, T, int8]{seq: slices.Values(seq)}
|
||||
}
|
||||
|
||||
func NewSeq2[K comparable, V any](seq2 iter.Seq2[K, V]) Sequence[V, V, K] {
|
||||
return NewMapSeq2[K, V, V](seq2)
|
||||
}
|
||||
|
||||
func Seq2From[K comparable, V any](seq2 map[K]V) Sequence[V, V, K] {
|
||||
return MapSeq2From[K, V, V](seq2)
|
||||
}
|
||||
|
||||
func NewMapSeq2[K comparable, V, T any](seq2 iter.Seq2[K, V]) Sequence[V, T, K] {
|
||||
return Sequence[V, T, K]{seq2: seq2}
|
||||
}
|
||||
|
||||
func MapSeq2From[K comparable, V, T any](seq2 map[K]V) Sequence[V, T, K] {
|
||||
return Sequence[V, T, K]{seq2: maps.All(seq2)}
|
||||
}
|
||||
|
||||
type Sequence[V, T any, K comparable] struct {
|
||||
seq iter.Seq[V]
|
||||
seq2 iter.Seq2[K, V]
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) Filter(filter func(V) bool) Sequence[V, T, K] {
|
||||
return Sequence[V, T, K]{
|
||||
seq: func(yield func(V) bool) {
|
||||
for v := range s.seq {
|
||||
if filter(v) && !yield(v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) Flat() Sequence[T, T, K] {
|
||||
return Sequence[T, T, K]{
|
||||
seq: func(yield func(T) bool) {
|
||||
for v := range s.seq {
|
||||
if vv, ok := any(v).([]T); ok {
|
||||
for i := 0; i < len(vv); i++ {
|
||||
if !yield(vv[i]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) FlatMap(mapper func(V) []T) Sequence[T, T, K] {
|
||||
return Sequence[T, T, K]{
|
||||
seq: func(yield func(T) bool) {
|
||||
for v := range s.seq {
|
||||
vv := mapper(v)
|
||||
for i := 0; i < len(vv); i++ {
|
||||
if !yield(vv[i]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) Map(mapper func(V) T) Sequence[T, T, K] {
|
||||
return Sequence[T, T, K]{
|
||||
seq: func(yield func(T) bool) {
|
||||
for v := range s.seq {
|
||||
if !yield(mapper(v)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) Filter2(filter func(K, V) bool) Sequence[V, T, K] {
|
||||
return Sequence[V, T, K]{
|
||||
seq2: func(yield func(K, V) bool) {
|
||||
for k, v := range s.seq2 {
|
||||
if filter(k, v) && !yield(k, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) Map2(mapper func(K, V) T) Sequence[T, T, K] {
|
||||
return Sequence[T, T, K]{
|
||||
seq: func(yield func(T) bool) {
|
||||
for k, v := range s.seq2 {
|
||||
if !yield(mapper(k, v)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) Seq() iter.Seq[V] {
|
||||
return s.seq
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) Seq2() iter.Seq2[K, V] {
|
||||
return s.seq2
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) Contains(t any, equal func(one, other any) bool) bool {
|
||||
for v := range s.seq {
|
||||
if equal(v, t) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) Find(finder func(V) bool) (V, bool) {
|
||||
for v := range s.seq {
|
||||
if finder(v) {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
return NewStruct[V](), false
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) Unique(contains func(set []V, v V) bool) []V {
|
||||
var set []V
|
||||
for v := range s.seq {
|
||||
if !contains(set, v) {
|
||||
set = append(set, v)
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) Collect() []V {
|
||||
return slices.Collect(s.seq)
|
||||
}
|
||||
|
||||
func (s Sequence[V, T, K]) Collect2() map[K]V {
|
||||
return maps.Collect(s.seq2)
|
||||
}
|
||||
|
||||
func Equal(one, other any) bool {
|
||||
return one == other
|
||||
}
|
||||
|
||||
type UniVec[T comparable] []T
|
||||
|
||||
func (s *UniVec[T]) Append(items ...T) *UniVec[T] {
|
||||
for i := 0; i < len(items); i++ {
|
||||
vec := *s
|
||||
if !slices.Contains(vec, items[i]) {
|
||||
*s = append(vec, items[i])
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func Set[T comparable](items ...T) UniVec[T] {
|
||||
uv := UniVec[T]{}
|
||||
uv.Append(items...)
|
||||
return uv
|
||||
}
|
||||
|
||||
func SliceDeleteGet[S ~[]E, E any](s *S, i int, j int) (S, error) {
|
||||
slice := *s
|
||||
if len(slice) < j {
|
||||
return nil, Closed0("Delete index out of range")
|
||||
} else if i > j {
|
||||
return nil, Closed0("Delete index i cannot be greater than j")
|
||||
}
|
||||
deleted := slices.Clone(slice[i:j])
|
||||
*s = slices.Delete(slice, i, j)
|
||||
return deleted, nil
|
||||
}
|
87
util/mixed.go
Normal file
87
util/mixed.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceUnavailable = 503
|
||||
ServiceUnavailableMessage = "Service Unavailable"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrorSilent uint8 = iota
|
||||
ErrorOpenly
|
||||
ErrorClosed
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Type uint8
|
||||
Message string
|
||||
Code int
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
tyCode := ""
|
||||
switch e.Type {
|
||||
case ErrorOpenly:
|
||||
tyCode = "Openly"
|
||||
case ErrorClosed:
|
||||
tyCode = "Closed"
|
||||
}
|
||||
if e.Code != 0 {
|
||||
tyCode += " " + strconv.Itoa(e.Code)
|
||||
}
|
||||
return fmt.Sprintf("[%s %s] %s", time.Now().Format("2006-01-02 15:04:05"), tyCode, e.Message)
|
||||
}
|
||||
|
||||
func (e Error) IsOpenly() bool {
|
||||
return e.Type == ErrorOpenly
|
||||
}
|
||||
|
||||
func ResponseUnits(err error) (int, string) {
|
||||
var e Error
|
||||
if errors.As(err, &e) && e.Type == ErrorOpenly {
|
||||
return e.Code, e.Message
|
||||
}
|
||||
return ServiceUnavailable, ServiceUnavailableMessage
|
||||
}
|
||||
|
||||
func newError(tp uint8, code int, msg string) Error {
|
||||
return Error{Type: tp, Message: msg, Code: code}
|
||||
}
|
||||
|
||||
func Openly(code int, format string, args ...any) Error {
|
||||
return newError(ErrorOpenly, code, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Closed(code int, format string, args ...any) Error {
|
||||
return newError(ErrorClosed, code, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Openly0(format string, args ...any) Error {
|
||||
return Openly(0, format, args...)
|
||||
}
|
||||
|
||||
func Closed0(format string, args ...any) Error {
|
||||
return Closed(0, format, args...)
|
||||
}
|
||||
|
||||
func Silent(format string, args ...any) Error {
|
||||
return newError(ErrorSilent, 0, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Iif[T any](test bool, trueVal T, falseVal T) T {
|
||||
if test {
|
||||
return trueVal
|
||||
}
|
||||
return falseVal
|
||||
}
|
||||
|
||||
func NewStruct[T any]() T {
|
||||
var t T
|
||||
return t
|
||||
}
|
181
util/tree.go
Normal file
181
util/tree.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
)
|
||||
|
||||
const (
|
||||
TreeTraverStop = iota + 1
|
||||
TreeTraverBreak
|
||||
TreeTraverSkip
|
||||
TreeTraverContinue
|
||||
)
|
||||
|
||||
type TreeElement[K comparable] interface {
|
||||
Key() K
|
||||
ChildOf(parent any) bool
|
||||
EqualTo(other any) bool
|
||||
}
|
||||
|
||||
// Tree represents a generic tree structure where each node contains an element of type E.
|
||||
// The element type E must implement the TreeElement interface, which requires methods for
|
||||
// retrieving the node's key, determining if it is a child of a given parent, and comparing
|
||||
// equality with another element. The tree is parameterized by two types:
|
||||
// - E: The type of the element stored in each node, which must satisfy the TreeElement[K] interface.
|
||||
// - K: The type of the key used to identify nodes, which must be comparable.
|
||||
//
|
||||
// The tree structure includes:
|
||||
// - Element: The element stored in the current node.
|
||||
// - Children: A map of child nodes, indexed by their keys.
|
||||
// - Parent: A reference to the parent node, if any.
|
||||
//
|
||||
// This structure is designed to support operations such as adding, retrieving, and traversing nodes,
|
||||
// as well as building and managing hierarchical relationships between elements.
|
||||
type Tree[E TreeElement[K], K comparable] struct {
|
||||
Element E
|
||||
Children map[K]*Tree[E, K]
|
||||
Parent *Tree[E, K]
|
||||
}
|
||||
|
||||
func (t *Tree[E, K]) Set(element E) *Tree[E, K] {
|
||||
child := NewTree(element)
|
||||
child.Parent = t
|
||||
t.Children[element.Key()] = &child
|
||||
return &child
|
||||
}
|
||||
|
||||
func (t *Tree[E, K]) SetIfAbsent(element E) *Tree[E, K] {
|
||||
if val, ok := t.Children[element.Key()]; ok {
|
||||
return val
|
||||
}
|
||||
return t.Set(element)
|
||||
}
|
||||
|
||||
func (t *Tree[E, K]) SetByPath(path []K, elem E) (*Tree[E, K], error) {
|
||||
return t.actualSetByPath(path, elem, true)
|
||||
}
|
||||
|
||||
func (t *Tree[E, K]) SetByPathIfAbsent(path []K, elem E) (*Tree[E, K], error) {
|
||||
return t.actualSetByPath(path, elem, false)
|
||||
}
|
||||
|
||||
func (t *Tree[E, K]) actualSetByPath(path []K, elem E, force bool) (*Tree[E, K], error) {
|
||||
if len(path) == 0 {
|
||||
return nil, Closed0("Cannot get by an empty path")
|
||||
}
|
||||
parent, ok := t.ChildByPath(path[:len(path)-1])
|
||||
if !ok {
|
||||
return nil, Closed0("Cannot find Parent by path '%v'", path)
|
||||
}
|
||||
if force {
|
||||
parent.Set(elem)
|
||||
} else {
|
||||
parent.SetIfAbsent(elem)
|
||||
}
|
||||
return parent, nil
|
||||
}
|
||||
|
||||
func (t *Tree[E, K]) Child(key K) (*Tree[E, K], bool) {
|
||||
value, ok := t.Children[key]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func (t *Tree[E, K]) ChildByPath(path []K) (*Tree[E, K], bool) {
|
||||
child := t
|
||||
for _, part := range path {
|
||||
if c, ok := child.Children[part]; ok {
|
||||
child = c
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return child, true
|
||||
}
|
||||
|
||||
func (t *Tree[E, K]) Parents() []*Tree[E, K] {
|
||||
return t.ParentsUntil(nil, true)
|
||||
}
|
||||
|
||||
func (t *Tree[E, K]) ParentsUntil(until *Tree[E, K], elderFirst bool) []*Tree[E, K] {
|
||||
var parents []*Tree[E, K]
|
||||
for parent := t; parent != nil && (until == nil || until.Element.EqualTo(parent.Element)); parent = parent.Parent {
|
||||
parents = append(parents, parent)
|
||||
}
|
||||
if elderFirst {
|
||||
slices.Reverse(parents)
|
||||
}
|
||||
return parents
|
||||
}
|
||||
|
||||
// Traversal performs a breadth-first traversal of the tree starting from the current node.
|
||||
// The traversal is controlled by the provided callback function, which is called for each node visited.
|
||||
// The callback function should return one of the following constants to control the traversal behavior:
|
||||
// - TreeTraverStop: Stops the traversal entirely.
|
||||
// - TreeTraverBreak: Breaks out of the current level of traversal (i.e., stops processing children of the current node).
|
||||
// - TreeTraverSkip: Skips the current node's children and continues with the next node at the same level.
|
||||
// - TreeTraverContinue: Continues the traversal normally, including the current node's children.
|
||||
//
|
||||
// This method is useful for scenarios where you need to traverse the tree and perform actions or checks on each node,
|
||||
// with the ability to control the flow of the traversal based on the node's content or other conditions.
|
||||
func (t *Tree[E, K]) Traversal(callback func(*Tree[E, K]) int) {
|
||||
nodes := []*Tree[E, K]{t}
|
||||
Outer:
|
||||
for i := 0; i < len(nodes); i++ {
|
||||
parent := nodes[i]
|
||||
for child := range maps.Values(parent.Children) {
|
||||
switch callback(child) {
|
||||
case TreeTraverStop:
|
||||
break Outer
|
||||
case TreeTraverBreak:
|
||||
break
|
||||
case TreeTraverSkip:
|
||||
continue
|
||||
case TreeTraverContinue:
|
||||
nodes = append(nodes, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build constructs a tree structure from a list of elements. The function takes two parameters:
|
||||
// - elements: A slice of elements to be added to the tree. Each element must implement the TreeElement interface.
|
||||
// - remainsHandler: An optional callback function that is called with any elements that could not be added to the tree.
|
||||
// This function receives the root of the tree and the remaining elements as arguments.
|
||||
//
|
||||
// The function works by iteratively adding elements to the tree based on their hierarchical relationships.
|
||||
// Elements are added as children of the current node if they satisfy the `ChildOf` condition with respect to the node's element.
|
||||
// If an element cannot be added to the tree (i.e., it does not satisfy the `ChildOf` condition with any existing node),
|
||||
// it is passed to the `remainsHandler` callback, if provided.
|
||||
//
|
||||
// This method is useful for building a tree from a flat list of elements, where the hierarchical structure is determined
|
||||
// by the `ChildOf` method of the elements.
|
||||
func (t *Tree[E, K]) Build(elements []E, remainsHandler func(tree *Tree[E, K], remains []E)) {
|
||||
parents := []*Tree[E, K]{t}
|
||||
for i := 0; i < len(parents); i++ {
|
||||
pa := parents[i]
|
||||
var childIndexes []int
|
||||
for j := len(elements) - 1; j >= 0; j-- {
|
||||
if elements[j].ChildOf(pa.Element) {
|
||||
childIndexes = append(childIndexes, j)
|
||||
}
|
||||
}
|
||||
for _, childIndex := range childIndexes {
|
||||
elem := elements[childIndex]
|
||||
parents = append(parents, pa.Set(elem))
|
||||
elements = slices.Delete(elements, childIndex, childIndex+1)
|
||||
}
|
||||
}
|
||||
if remainsHandler != nil {
|
||||
remainsHandler(t, elements)
|
||||
}
|
||||
}
|
||||
|
||||
// NewTree init a tree instance use the given element
|
||||
func NewTree[E TreeElement[K], K comparable](elem E) Tree[E, K] {
|
||||
return Tree[E, K]{
|
||||
Element: elem,
|
||||
Children: make(map[K]*Tree[E, K]),
|
||||
Parent: nil,
|
||||
}
|
||||
}
|
59
util/tuple.go
Normal file
59
util/tuple.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package util
|
||||
|
||||
type Tuple2[A, B any] struct {
|
||||
A A
|
||||
B B
|
||||
}
|
||||
|
||||
func (t Tuple2[A, B]) Values() (A, B) {
|
||||
return t.A, t.B
|
||||
}
|
||||
|
||||
func NewTuple2[A, B any](a A, b B) Tuple2[A, B] {
|
||||
return Tuple2[A, B]{a, b}
|
||||
}
|
||||
|
||||
type Tuple3[A, B, C any] struct {
|
||||
A A
|
||||
B B
|
||||
C C
|
||||
}
|
||||
|
||||
func (t Tuple3[A, B, C]) Values() (A, B, C) {
|
||||
return t.A, t.B, t.C
|
||||
}
|
||||
|
||||
func NewTuple3[A, B, C any](a A, b B, c C) Tuple3[A, B, C] {
|
||||
return Tuple3[A, B, C]{a, b, c}
|
||||
}
|
||||
|
||||
type Tuple4[A, B, C, D any] struct {
|
||||
A A
|
||||
B B
|
||||
C C
|
||||
D D
|
||||
}
|
||||
|
||||
func (t Tuple4[A, B, C, D]) Values() (A, B, C, D) {
|
||||
return t.A, t.B, t.C, t.D
|
||||
}
|
||||
|
||||
func NewTuple4[A, B, C, D any](a A, b B, c C, d D) Tuple4[A, B, C, D] {
|
||||
return Tuple4[A, B, C, D]{a, b, c, d}
|
||||
}
|
||||
|
||||
type Tuple5[A, B, C, D, E any] struct {
|
||||
A A
|
||||
B B
|
||||
C C
|
||||
D D
|
||||
E E
|
||||
}
|
||||
|
||||
func (t Tuple5[A, B, C, D, E]) Values() (A, B, C, D, E) {
|
||||
return t.A, t.B, t.C, t.D, t.E
|
||||
}
|
||||
|
||||
func NewTuple5[A, B, C, D, E any](a A, b B, c C, d D, e E) Tuple5[A, B, C, D, E] {
|
||||
return Tuple5[A, B, C, D, E]{a, b, c, d, e}
|
||||
}
|
120
util/utils_test.go
Normal file
120
util/utils_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Tuple struct {
|
||||
id uint8
|
||||
pid uint8
|
||||
msg string
|
||||
}
|
||||
|
||||
func newTuple(id uint8, pid uint8, msg string) Tuple {
|
||||
return Tuple{
|
||||
id: id,
|
||||
pid: pid,
|
||||
msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Tuple) Key() uint8 {
|
||||
return t.id
|
||||
}
|
||||
|
||||
func (t Tuple) ChildOf(parent any) bool {
|
||||
return t.pid == parent.(Tuple).id
|
||||
}
|
||||
|
||||
func (t Tuple) EqualTo(other any) bool {
|
||||
return t.id == other.(Tuple).id
|
||||
}
|
||||
|
||||
func TestNewTree(t *testing.T) {
|
||||
root := NewTree(newTuple(0, 0, "x"))
|
||||
root.Set(newTuple(1, 0, "a"))
|
||||
root.Set(newTuple(2, 0, "b"))
|
||||
root.SetByPath([]uint8{8}, newTuple(8, 0, "h"))
|
||||
root.SetByPath([]uint8{1, 3}, newTuple(3, 1, "c"))
|
||||
root.SetByPath([]uint8{1, 4}, newTuple(4, 1, "d"))
|
||||
root.SetByPath([]uint8{8, 5}, newTuple(5, 8, "e"))
|
||||
root.SetByPath([]uint8{8, 5, 6}, newTuple(6, 5, "f"))
|
||||
root.SetByPath([]uint8{8, 5, 6}, newTuple(7, 5, "g"))
|
||||
root.SetByPath([]uint8{1, 3, 9}, newTuple(9, 3, "i"))
|
||||
root.SetByPath([]uint8{1, 3, 10}, newTuple(10, 3, "j"))
|
||||
t.Logf("Root: %v", root)
|
||||
root.Traversal(func(tr *Tree[Tuple, uint8]) int {
|
||||
t.Logf("Traversal: %v", tr.Element.msg)
|
||||
return TreeTraverContinue
|
||||
})
|
||||
}
|
||||
|
||||
type Path string
|
||||
|
||||
func (p Path) Key() string {
|
||||
path := string(p)
|
||||
if index := strings.LastIndex(path, "/"); index >= 0 {
|
||||
path = path[index+1:]
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func (p Path) ChildOf(parent any) bool {
|
||||
path := string(p)
|
||||
ppath := string(parent.(Path))
|
||||
if index := strings.LastIndex(path, "/"); index >= 0 {
|
||||
return path[:index] == ppath
|
||||
}
|
||||
return len(ppath) > 0
|
||||
}
|
||||
|
||||
func (p Path) EqualTo(other any) bool {
|
||||
return string(p) == string(other.(Path))
|
||||
}
|
||||
|
||||
func TestTreeBuild(t *testing.T) {
|
||||
tree := NewTree(Path(""))
|
||||
tree.Build([]Path{
|
||||
"hello",
|
||||
"hello/world",
|
||||
"hello/world/!",
|
||||
"hello/rust!",
|
||||
"hello/examples/for/rust!",
|
||||
}, func(tree *Tree[Path, string], remains []Path) {
|
||||
var fills []Path
|
||||
for _, remain := range remains {
|
||||
paths := strings.Split(string(remain), "/")
|
||||
for i := 0; i < len(paths); i++ {
|
||||
path := strings.Join(paths[:i+1], "/")
|
||||
elem := Path(path)
|
||||
if _, ok := tree.ChildByPath(paths[:i+1]); !ok && !slices.Contains(fills, elem) {
|
||||
fills = append(fills, elem)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, fill := range fills {
|
||||
_, _ = tree.SetByPath(strings.Split(string(fill), "/"), fill)
|
||||
}
|
||||
})
|
||||
tr, ok := tree.ChildByPath([]string{"hello", "world"})
|
||||
if !ok {
|
||||
t.Fatalf("Failed to ChildByPath")
|
||||
}
|
||||
tr2, ok := tr.Child("!")
|
||||
if !ok {
|
||||
t.Fatalf("Failed to Child")
|
||||
}
|
||||
tr3, _ := tree.Child("hello")
|
||||
parents := tr2.ParentsUntil(tr3, false)
|
||||
t.Logf("tr: %v", tr)
|
||||
t.Logf("tr2: %v", tr2)
|
||||
t.Logf("tr3: %v", tr3)
|
||||
t.Logf("tree: %v", tree)
|
||||
t.Logf("parents: %v", parents)
|
||||
tree.Traversal(func(tr *Tree[Path, string]) int {
|
||||
t.Logf("Traversal: %v", tr)
|
||||
return TreeTraverContinue
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user