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,8 +11,8 @@ familiar.
- Ready-to-use pagination
- 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
challenge of making a project with zero dependencies. With Go version 1.22, it
was the perfect opportunity thanks to the changes made in the http package.
was the perfect opportunity thanks to the changes made in the http package.

2
go.mod
View File

@ -1,3 +1,3 @@
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)
})
}
}

103
ron.go
View File

@ -9,6 +9,7 @@ import (
"net/http"
"os"
"strings"
"sync"
"time"
)
@ -26,9 +27,10 @@ type (
}
CTX struct {
W *responseWriterWrapper
R *http.Request
E *Engine
W *responseWriterWrapper
R *http.Request
E *Engine
Ctx context.Context
}
Config struct {
@ -52,12 +54,18 @@ type (
}
)
var rwPool = sync.Pool{
New: func() any {
return &responseWriterWrapper{}
},
}
const (
RequestID string = "request_id"
HeaderJSON string = "application/json"
HeaderHTML_UTF8 string = "text/html; 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"
)
@ -77,7 +85,9 @@ func (w *responseWriterWrapper) Write(b []byte) (int, error) {
}
func (w *responseWriterWrapper) Flush() {
w.ResponseWriter.(http.Flusher).Flush()
if flusher, ok := w.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
}
func defaultEngine() *Engine {
@ -116,8 +126,13 @@ func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
handler = createStack(e.middleware...)(handler)
rw := &responseWriterWrapper{ResponseWriter: w}
rw := rwPool.Get().(*responseWriterWrapper)
rw.ResponseWriter = w
rw.headerWritten = false
handler.ServeHTTP(rw, r)
rw.Flush()
rwPool.Put(rw)
}
func (e *Engine) Run(addr string) error {
@ -139,17 +154,43 @@ func (e *Engine) USE(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) {
rw := &responseWriterWrapper{ResponseWriter: w}
handler(&CTX{W: rw, R: r, E: e}, r.Context())
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) 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) {
rw := &responseWriterWrapper{ResponseWriter: w}
handler(&CTX{W: rw, R: r, E: e}, r.Context())
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) 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)
}
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) {
rw := &responseWriterWrapper{ResponseWriter: w}
handler(&CTX{W: rw, R: r, E: g.engine}, r.Context())
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) 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) {
rw := &responseWriterWrapper{ResponseWriter: w}
handler(&CTX{W: rw, R: r, E: g.engine}, r.Context())
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) 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
import (
"context"
"fmt"
"log/slog"
"net/http"
@ -83,7 +82,7 @@ func Test_applyEngineConfig(t *testing.T) {
func Test_ServeHTTP(t *testing.T) {
e := New()
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.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"},
}
e.GET("/", func(c *CTX, ctx context.Context) {
e.GET("/", func(c *CTX) {
c.W.WriteHeader(http.StatusOK)
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.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.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.Write([]byte("GET Resource"))
})
@ -207,7 +206,7 @@ func Test_GET(t *testing.T) {
func Test_POST(t *testing.T) {
e := New()
e.POST("/", func(c *CTX, ctx context.Context) {
e.POST("/", func(c *CTX) {
c.W.WriteHeader(http.StatusOK)
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) {
e := New()
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.Write([]byte("GET API"))
})
@ -248,7 +287,7 @@ func Test_GROUP(t *testing.T) {
func Test_GROUPWithMiddleware(t *testing.T) {
e := New()
e.GET("/index", func(c *CTX, ctx context.Context) {
e.GET("/index", func(c *CTX) {
c.W.WriteHeader(http.StatusOK)
c.W.Write([]byte("GET Root"))
})
@ -266,7 +305,7 @@ func Test_GROUPWithMiddleware(t *testing.T) {
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.Write([]byte("GET API"))
})
@ -287,7 +326,7 @@ func Test_GROUPWithMiddleware(t *testing.T) {
func Test_GROUPPOST(t *testing.T) {
e := New()
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.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) {
tests := map[string]struct {
givenPath string
@ -328,7 +409,7 @@ func Test_Static(t *testing.T) {
givenDirectory: "assets",
expectedResponse: testhelpers.ExpectedResponse{
Code: http.StatusOK,
Header: HeaderAppJS,
Header: HeaderAppJS_UTF8,
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{
"templates\\layout.base.gohtml",
"templates\\layout.another.gohtml",
"templates\\fragment.button.gohtml",
"templates\\component.list.gohtml",
"templates\\page.index.gohtml",
"templates\\page.tindex.gohtml",
"templates\\page.another.gohtml",
"templates/layout.base.gohtml",
"templates/layout.another.gohtml",
"templates/fragment.button.gohtml",
"templates/component.list.gohtml",
"templates/page.index.gohtml",
"templates/page.tindex.gohtml",
"templates/page.another.gohtml",
}
actual, err := render.findHTMLFiles()
if err != nil {