Compare commits

..

4 Commits

Author SHA1 Message Date
695ea0a704 add recover middelware 2025-05-09 18:24:10 +02:00
ec78f9f77d update to latest go version 2025-05-09 18:23:22 +02:00
787ead652d sync.Pool for rw 2025-05-09 18:14:24 +02:00
fd78380cb4 integrate context to struct and fix paths 2025-05-09 17:50:18 +02:00
6 changed files with 220 additions and 40 deletions

View File

@ -11,7 +11,7 @@ familiar.
- Ready-to-use pagination - Ready-to-use pagination
- Binding form inputs and JSON to structured types - Binding form inputs and JSON to structured types
## Motivación ## Motivation
It was created from the need to explore the standard library in depth and the It was created from the need to explore the standard library in depth and the
challenge of making a project with zero dependencies. With Go version 1.22, it challenge of making a project with zero dependencies. With Go version 1.22, it

2
go.mod
View File

@ -1,3 +1,3 @@
module ron module ron
go 1.23.2 go 1.24.3

View File

@ -48,3 +48,17 @@ func (e *Engine) RequestIdMiddleware() Middleware {
}) })
} }
} }
func (e *Engine) RecoverMiddleware() Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
slog.Error("panic", "error", r)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
}

97
ron.go
View File

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
) )
@ -29,6 +30,7 @@ type (
W *responseWriterWrapper W *responseWriterWrapper
R *http.Request R *http.Request
E *Engine E *Engine
Ctx context.Context
} }
Config struct { Config struct {
@ -52,12 +54,18 @@ type (
} }
) )
var rwPool = sync.Pool{
New: func() any {
return &responseWriterWrapper{}
},
}
const ( const (
RequestID string = "request_id" RequestID string = "request_id"
HeaderJSON string = "application/json" HeaderJSON string = "application/json"
HeaderHTML_UTF8 string = "text/html; charset=utf-8" HeaderHTML_UTF8 string = "text/html; charset=utf-8"
HeaderCSS_UTF8 string = "text/css; charset=utf-8" HeaderCSS_UTF8 string = "text/css; charset=utf-8"
HeaderAppJS string = "application/javascript" HeaderAppJS_UTF8 string = "text/javascript; charset=utf-8"
HeaderPlain_UTF8 string = "text/plain; charset=utf-8" HeaderPlain_UTF8 string = "text/plain; charset=utf-8"
) )
@ -77,7 +85,9 @@ func (w *responseWriterWrapper) Write(b []byte) (int, error) {
} }
func (w *responseWriterWrapper) Flush() { func (w *responseWriterWrapper) Flush() {
w.ResponseWriter.(http.Flusher).Flush() if flusher, ok := w.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
} }
func defaultEngine() *Engine { func defaultEngine() *Engine {
@ -116,8 +126,13 @@ func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
handler = createStack(e.middleware...)(handler) handler = createStack(e.middleware...)(handler)
rw := &responseWriterWrapper{ResponseWriter: w} rw := rwPool.Get().(*responseWriterWrapper)
rw.ResponseWriter = w
rw.headerWritten = false
handler.ServeHTTP(rw, r) handler.ServeHTTP(rw, r)
rw.Flush()
rwPool.Put(rw)
} }
func (e *Engine) Run(addr string) error { func (e *Engine) Run(addr string) error {
@ -139,17 +154,43 @@ func (e *Engine) USE(middleware Middleware) {
e.middleware = append(e.middleware, middleware) e.middleware = append(e.middleware, middleware)
} }
func (e *Engine) GET(path string, handler func(*CTX, context.Context)) { func (e *Engine) GET(path string, handler func(*CTX)) {
e.mux.HandleFunc(fmt.Sprintf("GET %s", path), func(w http.ResponseWriter, r *http.Request) { e.mux.HandleFunc(fmt.Sprintf("GET %s", path), func(w http.ResponseWriter, r *http.Request) {
rw := &responseWriterWrapper{ResponseWriter: w} rw := rwPool.Get().(*responseWriterWrapper)
handler(&CTX{W: rw, R: r, E: e}, r.Context()) rw.ResponseWriter = w
rw.headerWritten = false
handler(&CTX{W: rw, R: r, E: e, Ctx: r.Context()})
rwPool.Put(rw)
}) })
} }
func (e *Engine) POST(path string, handler func(*CTX, context.Context)) { func (e *Engine) POST(path string, handler func(*CTX)) {
e.mux.HandleFunc(fmt.Sprintf("POST %s", path), func(w http.ResponseWriter, r *http.Request) { e.mux.HandleFunc(fmt.Sprintf("POST %s", path), func(w http.ResponseWriter, r *http.Request) {
rw := &responseWriterWrapper{ResponseWriter: w} rw := rwPool.Get().(*responseWriterWrapper)
handler(&CTX{W: rw, R: r, E: e}, r.Context()) rw.ResponseWriter = w
rw.headerWritten = false
handler(&CTX{W: rw, R: r, E: e, Ctx: r.Context()})
rwPool.Put(rw)
})
}
func (e *Engine) PUT(path string, handler func(*CTX)) {
e.mux.HandleFunc(fmt.Sprintf("PUT %s", path), func(w http.ResponseWriter, r *http.Request) {
rw := rwPool.Get().(*responseWriterWrapper)
rw.ResponseWriter = w
rw.headerWritten = false
handler(&CTX{W: rw, R: r, E: e, Ctx: r.Context()})
rwPool.Put(rw)
})
}
func (e *Engine) DELETE(path string, handler func(*CTX)) {
e.mux.HandleFunc(fmt.Sprintf("DELETE %s", path), func(w http.ResponseWriter, r *http.Request) {
rw := rwPool.Get().(*responseWriterWrapper)
rw.ResponseWriter = w
rw.headerWritten = false
handler(&CTX{W: rw, R: r, E: e, Ctx: r.Context()})
rwPool.Put(rw)
}) })
} }
@ -171,17 +212,43 @@ func (g *groupMux) USE(middleware Middleware) {
g.middleware = append(g.middleware, middleware) g.middleware = append(g.middleware, middleware)
} }
func (g *groupMux) GET(path string, handler func(*CTX, context.Context)) { func (g *groupMux) GET(path string, handler func(*CTX)) {
g.mux.HandleFunc(fmt.Sprintf("GET %s", path), func(w http.ResponseWriter, r *http.Request) { g.mux.HandleFunc(fmt.Sprintf("GET %s", path), func(w http.ResponseWriter, r *http.Request) {
rw := &responseWriterWrapper{ResponseWriter: w} rw := rwPool.Get().(*responseWriterWrapper)
handler(&CTX{W: rw, R: r, E: g.engine}, r.Context()) rw.ResponseWriter = w
rw.headerWritten = false
handler(&CTX{W: rw, R: r, E: g.engine, Ctx: r.Context()})
rwPool.Put(rw)
}) })
} }
func (g *groupMux) POST(path string, handler func(*CTX, context.Context)) { func (g *groupMux) POST(path string, handler func(*CTX)) {
g.mux.HandleFunc(fmt.Sprintf("POST %s", path), func(w http.ResponseWriter, r *http.Request) { g.mux.HandleFunc(fmt.Sprintf("POST %s", path), func(w http.ResponseWriter, r *http.Request) {
rw := &responseWriterWrapper{ResponseWriter: w} rw := rwPool.Get().(*responseWriterWrapper)
handler(&CTX{W: rw, R: r, E: g.engine}, r.Context()) rw.ResponseWriter = w
rw.headerWritten = false
handler(&CTX{W: rw, R: r, E: g.engine, Ctx: r.Context()})
rwPool.Put(rw)
})
}
func (g *groupMux) PUT(path string, handler func(*CTX)) {
g.mux.HandleFunc(fmt.Sprintf("PUT %s", path), func(w http.ResponseWriter, r *http.Request) {
rw := rwPool.Get().(*responseWriterWrapper)
rw.ResponseWriter = w
rw.headerWritten = false
handler(&CTX{W: rw, R: r, E: g.engine, Ctx: r.Context()})
rwPool.Put(rw)
})
}
func (g *groupMux) DELETE(path string, handler func(*CTX)) {
g.mux.HandleFunc(fmt.Sprintf("DELETE %s", path), func(w http.ResponseWriter, r *http.Request) {
rw := rwPool.Get().(*responseWriterWrapper)
rw.ResponseWriter = w
rw.headerWritten = false
handler(&CTX{W: rw, R: r, E: g.engine, Ctx: r.Context()})
rwPool.Put(rw)
}) })
} }

View File

@ -1,7 +1,6 @@
package ron package ron
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
@ -83,7 +82,7 @@ func Test_applyEngineConfig(t *testing.T) {
func Test_ServeHTTP(t *testing.T) { func Test_ServeHTTP(t *testing.T) {
e := New() e := New()
api := e.GROUP("/api") api := e.GROUP("/api")
api.GET("/index", func(c *CTX, ctx context.Context) { api.GET("/index", func(c *CTX) {
c.W.WriteHeader(http.StatusOK) c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("GET API")) c.W.Write([]byte("GET API"))
}) })
@ -171,19 +170,19 @@ func Test_GET(t *testing.T) {
{"resource with param", "GET", "/api/v1/resource/1", http.StatusOK, "GET Resource"}, {"resource with param", "GET", "/api/v1/resource/1", http.StatusOK, "GET Resource"},
} }
e.GET("/", func(c *CTX, ctx context.Context) { e.GET("/", func(c *CTX) {
c.W.WriteHeader(http.StatusOK) c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("GET Root")) c.W.Write([]byte("GET Root"))
}) })
e.GET("/api", func(c *CTX, ctx context.Context) { e.GET("/api", func(c *CTX) {
c.W.WriteHeader(http.StatusOK) c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("GET API")) c.W.Write([]byte("GET API"))
}) })
e.GET("/api/v1", func(c *CTX, ctx context.Context) { e.GET("/api/v1", func(c *CTX) {
c.W.WriteHeader(http.StatusOK) c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("GET API v1")) c.W.Write([]byte("GET API v1"))
}) })
e.GET("/api/v1/resource/{id}", func(c *CTX, ctx context.Context) { e.GET("/api/v1/resource/{id}", func(c *CTX) {
c.W.WriteHeader(http.StatusOK) c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("GET Resource")) c.W.Write([]byte("GET Resource"))
}) })
@ -207,7 +206,7 @@ func Test_GET(t *testing.T) {
func Test_POST(t *testing.T) { func Test_POST(t *testing.T) {
e := New() e := New()
e.POST("/", func(c *CTX, ctx context.Context) { e.POST("/", func(c *CTX) {
c.W.WriteHeader(http.StatusOK) c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("POST")) c.W.Write([]byte("POST"))
}) })
@ -225,10 +224,50 @@ func Test_POST(t *testing.T) {
} }
} }
func Test_PUT(t *testing.T) {
e := New()
e.PUT("/", func(c *CTX) {
c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("PUT"))
})
rr := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/", nil)
e.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Expected status code: %d, Actual: %d", http.StatusOK, status)
}
if rr.Body.String() != "PUT" {
t.Errorf("Expected: PUT, Actual: %s", rr.Body.String())
}
}
func Test_DELETE(t *testing.T) {
e := New()
e.DELETE("/", func(c *CTX) {
c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("DELETE"))
})
rr := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/", nil)
e.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Expected status code: %d, Actual: %d", http.StatusOK, status)
}
if rr.Body.String() != "DELETE" {
t.Errorf("Expected: DELETE, Actual: %s", rr.Body.String())
}
}
func Test_GROUP(t *testing.T) { func Test_GROUP(t *testing.T) {
e := New() e := New()
api := e.GROUP("/api") api := e.GROUP("/api")
api.GET("/index", func(c *CTX, ctx context.Context) { api.GET("/index", func(c *CTX) {
c.W.WriteHeader(http.StatusOK) c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("GET API")) c.W.Write([]byte("GET API"))
}) })
@ -248,7 +287,7 @@ func Test_GROUP(t *testing.T) {
func Test_GROUPWithMiddleware(t *testing.T) { func Test_GROUPWithMiddleware(t *testing.T) {
e := New() e := New()
e.GET("/index", func(c *CTX, ctx context.Context) { e.GET("/index", func(c *CTX) {
c.W.WriteHeader(http.StatusOK) c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("GET Root")) c.W.Write([]byte("GET Root"))
}) })
@ -266,7 +305,7 @@ func Test_GROUPWithMiddleware(t *testing.T) {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
}) })
api.GET("/index", func(c *CTX, ctx context.Context) { api.GET("/index", func(c *CTX) {
c.W.WriteHeader(http.StatusOK) c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("GET API")) c.W.Write([]byte("GET API"))
}) })
@ -287,7 +326,7 @@ func Test_GROUPWithMiddleware(t *testing.T) {
func Test_GROUPPOST(t *testing.T) { func Test_GROUPPOST(t *testing.T) {
e := New() e := New()
api := e.GROUP("/api") api := e.GROUP("/api")
api.POST("/index", func(c *CTX, ctx context.Context) { api.POST("/index", func(c *CTX) {
c.W.WriteHeader(http.StatusOK) c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("POST API")) c.W.Write([]byte("POST API"))
}) })
@ -305,6 +344,48 @@ func Test_GROUPPOST(t *testing.T) {
} }
} }
func Test_GROUPPUT(t *testing.T) {
e := New()
api := e.GROUP("/api")
api.PUT("/index", func(c *CTX) {
c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("PUT API"))
})
rr := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/index", nil)
e.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Expected status code: %d, Actual: %d", http.StatusOK, status)
}
if rr.Body.String() != "PUT API" {
t.Errorf("Expected: PUT API, Actual: %s", rr.Body.String())
}
}
func Test_GROUPDELETE(t *testing.T) {
e := New()
api := e.GROUP("/api")
api.DELETE("/index", func(c *CTX) {
c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("DELETE API"))
})
rr := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/index", nil)
e.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Expected status code: %d, Actual: %d", http.StatusOK, status)
}
if rr.Body.String() != "DELETE API" {
t.Errorf("Expected: DELETE API, Actual: %s", rr.Body.String())
}
}
func Test_Static(t *testing.T) { func Test_Static(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
givenPath string givenPath string
@ -328,7 +409,7 @@ func Test_Static(t *testing.T) {
givenDirectory: "assets", givenDirectory: "assets",
expectedResponse: testhelpers.ExpectedResponse{ expectedResponse: testhelpers.ExpectedResponse{
Code: http.StatusOK, Code: http.StatusOK,
Header: HeaderAppJS, Header: HeaderAppJS_UTF8,
Body: "console.log('Hello, World!');", Body: "console.log('Hello, World!');",
}, },
}, },
@ -486,3 +567,21 @@ func Test_newLogger(t *testing.T) {
}) })
} }
} }
var preallocatedHello = []byte("Hello")
func Benchmark_GET(b *testing.B) {
engine := New()
engine.GET("/hello", func(c *CTX) {
c.W.Write(preallocatedHello)
})
req := httptest.NewRequest(http.MethodGet, "/hello", nil)
w := httptest.NewRecorder()
b.ResetTimer()
for i := 0; i < b.N; i++ {
engine.ServeHTTP(w, req)
}
}

View File

@ -98,13 +98,13 @@ func Test_findHTMLFiles(t *testing.T) {
} }
expected := []string{ expected := []string{
"templates\\layout.base.gohtml", "templates/layout.base.gohtml",
"templates\\layout.another.gohtml", "templates/layout.another.gohtml",
"templates\\fragment.button.gohtml", "templates/fragment.button.gohtml",
"templates\\component.list.gohtml", "templates/component.list.gohtml",
"templates\\page.index.gohtml", "templates/page.index.gohtml",
"templates\\page.tindex.gohtml", "templates/page.tindex.gohtml",
"templates\\page.another.gohtml", "templates/page.another.gohtml",
} }
actual, err := render.findHTMLFiles() actual, err := render.findHTMLFiles()
if err != nil { if err != nil {