diff --git a/context.go b/context.go index b6fbd46..c1e2bb8 100644 --- a/context.go +++ b/context.go @@ -23,7 +23,6 @@ import ( "sync" "time" - "github.com/WJQSERVER/wanf" "github.com/fenthope/reco" "github.com/go-json-experiment/json" @@ -43,7 +42,7 @@ type Context struct { index int8 // 当前执行到处理链的哪个位置 mu sync.RWMutex - Keys map[string]any // 用于在中间件之间传递数据 + Keys map[string]interface{} // 用于在中间件之间传递数据 Errors []error // 用于收集处理过程中的错误 @@ -78,18 +77,20 @@ func (c *Context) reset(w http.ResponseWriter, req *http.Request) { } else { c.Writer = newResponseWriter(w) } + //c.Writer = newResponseWriter(w) c.Request = req c.Params = c.Params[:0] // 清空 Params 切片,而不是重新分配,以复用底层数组 c.handlers = nil c.index = -1 // 初始为 -1,`Next()` 将其设置为 0 - c.Keys = make(map[string]any) // 每次请求重新创建 map,避免数据污染 + c.Keys = make(map[string]interface{}) // 每次请求重新创建 map,避免数据污染 c.Errors = c.Errors[:0] // 清空 Errors 切片 c.queryCache = nil // 清空查询参数缓存 c.formCache = nil // 清空表单数据缓存 c.ctx = req.Context() // 使用请求的上下文,继承其取消信号和值 c.sameSite = http.SameSiteDefaultMode // 默认 SameSite 模式 c.MaxRequestBodySize = c.engine.GlobalMaxRequestBodySize + // c.HTTPClient 和 c.engine 保持不变,它们引用 Engine 实例的成员 } // Next 在处理链中执行下一个处理函数 @@ -121,10 +122,10 @@ func (c *Context) AbortWithStatus(code int) { // Set 将一个键值对存储到 Context 中 // 这是一个线程安全的操作,用于在中间件之间传递数据 -func (c *Context) Set(key string, value any) { +func (c *Context) Set(key string, value interface{}) { c.mu.Lock() // 加写锁 if c.Keys == nil { - c.Keys = make(map[string]any) + c.Keys = make(map[string]interface{}) } c.Keys[key] = value c.mu.Unlock() // 解写锁 @@ -132,7 +133,7 @@ func (c *Context) Set(key string, value any) { // Get 从 Context 中获取一个值 // 这是一个线程安全的操作 -func (c *Context) Get(key string) (value any, exists bool) { +func (c *Context) Get(key string) (value interface{}, exists bool) { c.mu.RLock() // 加读锁 value, exists = c.Keys[key] c.mu.RUnlock() // 解读锁 @@ -207,7 +208,7 @@ func (c *Context) GetDuration(key string) (value time.Duration, exists bool) { // MustGet 从 Context 中获取一个值,如果不存在则 panic // 适用于确定值一定存在的场景 -func (c *Context) MustGet(key string) any { +func (c *Context) MustGet(key string) interface{} { if value, exists := c.Get(key); exists { return value } @@ -268,7 +269,7 @@ func (c *Context) Raw(code int, contentType string, data []byte) { } // String 向响应写入格式化的字符串 -func (c *Context) String(code int, format string, values ...any) { +func (c *Context) String(code int, format string, values ...interface{}) { c.Writer.WriteHeader(code) c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } @@ -282,7 +283,7 @@ func (c *Context) Text(code int, text string) { // JSON 向响应写入 JSON 数据 // 设置 Content-Type 为 application/json -func (c *Context) JSON(code int, obj any) { +func (c *Context) JSON(code int, obj interface{}) { c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8") c.Writer.WriteHeader(code) if err := json.MarshalWrite(c.Writer, obj); err != nil { @@ -294,7 +295,7 @@ func (c *Context) JSON(code int, obj any) { // GOB 向响应写入GOB数据 // 设置 Content-Type 为 application/octet-stream -func (c *Context) GOB(code int, obj any) { +func (c *Context) GOB(code int, obj interface{}) { c.Writer.Header().Set("Content-Type", "application/octet-stream") // 设置合适的 Content-Type c.Writer.WriteHeader(code) // GOB 编码 @@ -306,25 +307,11 @@ func (c *Context) GOB(code int, obj any) { } } -// WANF向响应写入WANF数据 -// 设置 application/vnd.wjqserver.wanf; charset=utf-8 -func (c *Context) WANF(code int, obj any) { - c.Writer.Header().Set("Content-Type", "application/vnd.wjqserver.wanf; charset=utf-8") - c.Writer.WriteHeader(code) - // WANF 编码 - encoder := wanf.NewStreamEncoder(c.Writer) - if err := encoder.Encode(obj); err != nil { - c.AddError(fmt.Errorf("failed to encode WANF: %w", err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to encode WANF: %w", err)) - return - } -} - // HTML 渲染 HTML 模板 // 如果 Engine 配置了 HTMLRender,则使用它进行渲染 // 否则,会进行简单的字符串输出 // 预留接口,可以扩展为支持多种模板引擎 -func (c *Context) HTML(code int, name string, obj any) { +func (c *Context) HTML(code int, name string, obj interface{}) { c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") c.Writer.WriteHeader(code) @@ -355,7 +342,7 @@ func (c *Context) Redirect(code int, location string) { } // ShouldBindJSON 尝试将请求体绑定到 JSON 对象 -func (c *Context) ShouldBindJSON(obj any) error { +func (c *Context) ShouldBindJSON(obj interface{}) error { if c.Request.Body == nil { return errors.New("request body is empty") } @@ -366,28 +353,10 @@ func (c *Context) ShouldBindJSON(obj any) error { return nil } -// ShouldBindWANF -func (c *Context) ShouldBindWANF(obj any) error { - if c.Request.Body == nil { - return errors.New("request body is empty") - } - decoder, err := wanf.NewStreamDecoder(c.Request.Body) - if err != nil { - return fmt.Errorf("failed to create WANF decoder: %w", err) - } - - if err := decoder.Decode(obj); err != nil { - return fmt.Errorf("WANF binding error: %w", err) - } - return nil -} - -// Deprecated: This function is a reserved placeholder for future API extensions -// and is not yet implemented. It will either be properly defined or removed in v2.0.0. Do not use. // ShouldBind 尝试将请求体绑定到各种类型(JSON, Form, XML 等) // 这是一个复杂的通用绑定接口,通常根据 Content-Type 或其他头部来判断绑定方式 // 预留接口,可根据项目需求进行扩展 -func (c *Context) ShouldBind(obj any) error { +func (c *Context) ShouldBind(obj interface{}) error { // TODO: 完整的通用绑定逻辑 // 可以根据 c.Request.Header.Get("Content-Type") 来判断是 JSON, Form, XML 等 // 例如: @@ -440,7 +409,7 @@ func (c *Context) Err() error { // Value returns the value associated with this context for key, or nil if no // value is associated with key. // 可以用于从 Context 中获取与特定键关联的值,包括 Go 原生 Context 的值和 Touka Context 的 Keys -func (c *Context) Value(key any) any { +func (c *Context) Value(key interface{}) interface{} { if keyAsString, ok := key.(string); ok { if val, exists := c.Get(keyAsString); exists { return val diff --git a/fileserver.go b/fileserver.go index 197b681..5b7f248 100644 --- a/fileserver.go +++ b/fileserver.go @@ -6,6 +6,7 @@ package touka import ( "errors" + "fmt" "net/http" "path" "strings" @@ -18,19 +19,13 @@ var allowedFileServerMethods = map[string]struct{}{ http.MethodHead: {}, } -var ( - ErrInputFSisNil = errors.New("input FS is nil") - ErrMethodNotAllowed = errors.New("method not allowed") -) - // FileServer方式, 返回一个HandleFunc, 统一化处理 func FileServer(fs http.FileSystem) HandlerFunc { if fs == nil { return func(c *Context) { - c.ErrorUseHandle(http.StatusInternalServerError, ErrInputFSisNil) + c.ErrorUseHandle(500, errors.New("Input FileSystem is nil")) } } - fileServerInstance := http.FileServer(fs) return func(c *Context) { FileServerHandleServe(c, fileServerInstance) @@ -42,6 +37,7 @@ func FileServer(fs http.FileSystem) HandlerFunc { func FileServerHandleServe(c *Context, fsHandle http.Handler) { if fsHandle == nil { + ErrInputFSisNil := errors.New("Input FileSystem Handle is nil") c.AddError(ErrInputFSisNil) // 500 c.ErrorUseHandle(http.StatusInternalServerError, ErrInputFSisNil) @@ -63,7 +59,7 @@ func FileServerHandleServe(c *Context, fsHandle http.Handler) { return } else { // 否则,返回 405 Method Not Allowed - c.engine.errorHandle.handler(c, http.StatusMethodNotAllowed, ErrMethodNotAllowed) + c.engine.errorHandle.handler(c, http.StatusMethodNotAllowed, fmt.Errorf("Method %s is Not Allowed on FileServer", c.Request.Method)) } } else { c.Next() diff --git a/go.mod b/go.mod index 0b8d97b..e9d0304 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,15 @@ module github.com/infinite-iroha/touka -go 1.25.1 +go 1.24.5 require ( github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 github.com/WJQSERVER-STUDIO/httpc v0.8.2 - github.com/WJQSERVER/wanf v0.0.0-20250810023226-e51d9d0737ee github.com/fenthope/reco v0.0.4 - github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b + github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 ) require ( github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/net v0.43.0 // indirect + golang.org/x/net v0.42.0 // indirect ) diff --git a/go.sum b/go.sum index dcd4f26..d9a63e3 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,11 @@ github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 h1:AiIHXP21LpK7pFfqUlUstgQEWzjbe github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2/go.mod h1:mCLqYU32bTmEE6dpj37MKKiZgz70Jh/xyK9vVbq6pok= github.com/WJQSERVER-STUDIO/httpc v0.8.2 h1:PFPLodV0QAfGEP6915J57vIqoKu9cGuuiXG/7C9TNUk= github.com/WJQSERVER-STUDIO/httpc v0.8.2/go.mod h1:8WhHVRO+olDFBSvL5PC/bdMkb6U3vRdPJ4p4pnguV5Y= -github.com/WJQSERVER/wanf v0.0.0-20250810023226-e51d9d0737ee h1:tJ31DNBn6UhWkk8fiikAQWqULODM+yBcGAEar1tzdZc= -github.com/WJQSERVER/wanf v0.0.0-20250810023226-e51d9d0737ee/go.mod h1:q2Pyg+G+s1acMWxrbI4CwS/Yk76/BzLREEdZ8iFwUNE= github.com/fenthope/reco v0.0.4 h1:yo2g3aWwdoMpaZWZX4SdZOW7mCK82RQIU/YI8ZUQThM= github.com/fenthope/reco v0.0.4/go.mod h1:eMyS8HpdMVdJ/2WJt6Cvt8P1EH9Igzj5lSJrgc+0jeg= -github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b h1:6Q4zRHXS/YLOl9Ng1b1OOOBWMidAQZR3Gel0UKPC/KU= -github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= diff --git a/sse.go b/sse.go deleted file mode 100644 index 3b98800..0000000 --- a/sse.go +++ /dev/null @@ -1,184 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// Copyright 2025 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. -package touka - -import ( - "bytes" - "io" - "net/http" - "strings" -) - -// Event 代表一个服务器发送事件(SSE). -type Event struct { - // Event 是事件的名称. - Event string - // Data 是事件的内容, 可以是多行文本. - Data string - // Id 是事件的唯一标识符. - Id string - // Retry 是指定客户端在连接丢失后应等待多少毫秒后尝试重新连接. - Retry string -} - -// Render 将事件格式化并写入给定的 writer. -// 通过逐行处理数据, 此方法可防止因数据中包含换行符而导致的CRLF注入问题. -// 为了性能, 它使用 bytes.Buffer 并通过 WriteTo 直接写入, 以避免不必要的内存分配. -func (e *Event) Render(w io.Writer) error { - var buf bytes.Buffer - - if len(e.Id) > 0 { - buf.WriteString("id: ") - buf.WriteString(e.Id) - buf.WriteString("\n") - } - if len(e.Event) > 0 { - buf.WriteString("event: ") - buf.WriteString(e.Event) - buf.WriteString("\n") - } - if len(e.Data) > 0 { - lines := strings.Split(e.Data, "\n") - for _, line := range lines { - buf.WriteString("data: ") - buf.WriteString(line) - buf.WriteString("\n") - } - } - if len(e.Retry) > 0 { - buf.WriteString("retry: ") - buf.WriteString(e.Retry) - buf.WriteString("\n") - } - - // 每个事件都以一个额外的换行符结尾. - buf.WriteString("\n") - - // 直接将 buffer 的内容写入 writer, 避免生成中间字符串. - _, err := buf.WriteTo(w) - return err -} - -// EventStream 启动一个 SSE 事件流. -// 这是推荐的、更简单安全的方式, 采用阻塞和回调的设计, 框架负责管理连接生命周期. -// -// 详细用法: -// -// r.GET("/sse/callback", func(c *touka.Context) { -// // streamer 回调函数会在一个循环中被调用. -// c.EventStream(func(w io.Writer) bool { -// event := touka.Event{ -// Event: "time-tick", -// Data: time.Now().Format(time.RFC1123), -// } -// -// if err := event.Render(w); err != nil { -// // 发生写入错误, 停止发送. -// return false // 返回 false 结束事件流. -// } -// -// time.Sleep(2 * time.Second) -// return true // 返回 true 继续事件流. -// }) -// // 当事件流结束后(例如客户端关闭页面), 这行代码会被执行. -// fmt.Println("Client disconnected from /sse/callback") -// }) -func (c *Context) EventStream(streamer func(w io.Writer) bool) { - // 为现代网络协议优化头部. - c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8") - c.Writer.Header().Set("Cache-Control", "no-cache, no-transform") - c.Writer.Header().Del("Connection") - c.Writer.Header().Del("Transfer-Encoding") - - c.Writer.WriteHeader(http.StatusOK) - c.Writer.Flush() // 直接调用, ResponseWriter 接口保证了 Flush 方法的存在. - - for { - select { - case <-c.Request.Context().Done(): - return - default: - if !streamer(c.Writer) { - return - } - c.Writer.Flush() - } - } -} - -// EventStreamChan 返回用于 SSE 事件流的 channel. -// 这是为高级并发场景设计的、更灵活的API. -// -// 重要: -// - 调用者必须 close(eventChan) 来结束事件流. -// - 调用者必须在独立的 goroutine 中消费 errChan 来处理错误和连接断开. -// - 为防止 goroutine 泄漏, 建议发送方在 select 中同时监听 c.Request.Context().Done(). -// -// 详细用法: -// -// r.GET("/sse/channel", func(c *touka.Context) { -// eventChan, errChan := c.EventStreamChan() -// -// // 必须在独立的goroutine中处理错误和连接断开. -// go func() { -// if err := <-errChan; err != nil { -// c.Errorf("SSE channel error: %v", err) -// } -// }() -// -// // 在另一个goroutine中异步发送事件. -// go func() { -// // 重要: 必须在逻辑结束时关闭channel, 以通知框架. -// defer close(eventChan) -// -// for i := 1; i <= 5; i++ { -// select { -// case <-c.Request.Context().Done(): -// return // 客户端已断开, 退出 goroutine. -// default: -// eventChan <- touka.Event{ -// Id: fmt.Sprintf("%d", i), -// Data: "hello from channel", -// } -// time.Sleep(2 * time.Second) -// } -// } -// }() -// }) -func (c *Context) EventStreamChan() (chan<- Event, <-chan error) { - eventChan := make(chan Event) - errChan := make(chan error, 1) - - c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8") - c.Writer.Header().Set("Cache-Control", "no-cache, no-transform") - c.Writer.Header().Del("Connection") - c.Writer.Header().Del("Transfer-Encoding") - - c.Writer.WriteHeader(http.StatusOK) - c.Writer.Flush() - - go func() { - defer close(errChan) - - for { - select { - case event, ok := <-eventChan: - if !ok { - return - } - if err := event.Render(c.Writer); err != nil { - errChan <- err - return - } - c.Writer.Flush() - case <-c.Request.Context().Done(): - errChan <- c.Request.Context().Err() - return - } - } - }() - - return eventChan, errChan -}