diff --git a/context-finalize.go b/context-finalize.go index 9502d59..b7ba01d 100644 --- a/context-finalize.go +++ b/context-finalize.go @@ -31,9 +31,9 @@ func (c *Context) finalize() { case Redirect: redirect = &body case string: - finalBodyReader = bytes.NewBufferString(body) + finalBodyReader = strings.NewReader(body) case []byte: - finalBodyReader = bytes.NewBuffer(body) + finalBodyReader = bytes.NewReader(body) default: marshalledReader, err := c.marshallResponseBody() if err != nil { @@ -111,8 +111,6 @@ func (c *Context) finalize() { } } - delete(contextData, c) - c.FinalError = c.Error c.FinalErrorStack = c.ErrorStack for _, doneHandler := range c.doneHandlers { diff --git a/context-next.go b/context-next.go index e69bd8a..d90c64a 100644 --- a/context-next.go +++ b/context-next.go @@ -43,7 +43,7 @@ func (c *Context) next() { // until we find a matching handler node. if c.matchingHandlerNode == nil { for c.currentHandlerNode != nil { - if c.tryMatchHandlerNode(c.currentHandlerNode) { + if c.currentHandlerNode.tryMatch(c) { c.matchingHandlerNode = c.currentHandlerNode break } diff --git a/context-utils.go b/context-utils.go index 9bdf6b9..ef42a0c 100644 --- a/context-utils.go +++ b/context-utils.go @@ -1,8 +1,6 @@ package navaros import ( - "fmt" - "reflect" "time" ) @@ -19,65 +17,3 @@ func CtxFinalize(ctx *Context) { func CtxSetDeadline(ctx *Context, deadline time.Time) { ctx.deadline = &deadline } - -// CtxSet associates a value by it's type with a context. This is for handlers -// and middleware to share data with other handlers and middleware associated -// with the context. -func CtxSet(ctx *Context, value any) { - for ctx.parentContext != nil { - ctx = ctx.parentContext - } - - valueType := reflect.TypeOf(value).String() - - if contextData[ctx] == nil { - contextData[ctx] = make(map[string]any) - } - contextData[ctx][valueType] = value -} - -// CtxGet retrieves a value by it's type from a context. This is for handlers -// and middleware to retrieve data set in association with the context by -// other handlers and middleware. -func CtxGet[V any](ctx *Context) (V, bool) { - for ctx.parentContext != nil { - ctx = ctx.parentContext - } - - var v V - targetType := reflect.TypeOf(v).String() - - var target V - contextData, ok := contextData[ctx] - if !ok { - return target, false - } - value, ok := contextData[targetType] - if !ok { - return target, false - } - - return value.(V), true -} - -// CtxMustGet like CtxGet retrieves a value by it's type from a context, but -// unlike CtxGet it panics if the value is not found. -func CtxMustGet[V any](ctx *Context) V { - for ctx.parentContext != nil { - ctx = ctx.parentContext - } - - var v V - targetType := reflect.TypeOf(v).String() - - contextData, ok := contextData[ctx] - if !ok { - panic("Context data not found for context") - } - value, ok := contextData[targetType] - if !ok { - panic(fmt.Sprintf("Context data not found for type: %s", targetType)) - } - - return value.(V) -} diff --git a/context.go b/context.go index 33915cc..5eabce6 100644 --- a/context.go +++ b/context.go @@ -50,12 +50,12 @@ type Context struct { currentHandlerOrTransformerIndex int currentHandlerOrTransformer any + associatedValues map[string]any + deadline *time.Time doneHandlers []func() } -var contextData = make(map[*Context]map[string]any) - // NewContext creates a new Context from go's http.ResponseWriter and // http.Request. It also takes a variadic list of handlers. This is useful for // creating a new Context outside of a router, and can be used by libraries @@ -83,11 +83,12 @@ func NewContextWithNode(res http.ResponseWriter, req *http.Request, firstHandler bodyWriter: res, currentHandlerNode: &HandlerNode{ - Method: All, - HandlersAndTransformers: []any{}, - Next: firstHandlerNode, + Method: All, + Next: firstHandlerNode, }, + associatedValues: map[string]any{}, + doneHandlers: []func(){}, } } @@ -127,6 +128,14 @@ func (c *Context) Next() { c.next() } +func (s Context) Set(key string, value any) { + s.associatedValues[key] = value +} + +func (s Context) Get(key string) any { + return s.associatedValues[key] +} + // Method returns the HTTP method of the request. func (c *Context) Method() HTTPMethod { return c.method @@ -359,24 +368,3 @@ func (c *Context) tryUpdateParent() { c.parentContext.hasWrittenHeaders = c.hasWrittenHeaders c.parentContext.hasWrittenBody = c.hasWrittenBody } - -// tryMatchHandlerNode attempts to match a handler node's route pattern and http -// method to the current context. It will return true if the handler node -// matches, and false if it does not. -func (c *Context) tryMatchHandlerNode(node *HandlerNode) bool { - if node.Method != All && node.Method != c.method { - return false - } - - if node.Pattern != nil { - params, ok := node.Pattern.Match(c.path) - if !ok { - return false - } - c.params = params - } else { - c.params = make(map[string]string) - } - - return true -} diff --git a/go.mod b/go.mod index 0d79752..98a8679 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/RobertWHurst/navaros go 1.20 + +require github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db diff --git a/go.sum b/go.sum index e69de29..8e9fabb 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db h1:7aN5cccjIqCLTzedH7MZzRZt5/lsAHch6Z3L2ZGn5FA= +github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= diff --git a/handler-node.go b/handler-node.go new file mode 100644 index 0000000..e2829db --- /dev/null +++ b/handler-node.go @@ -0,0 +1,24 @@ +package navaros + +// HandlerNode is used to build the handler chains used by the context. The +// router builds a chain from these objects then attaches them to the context. +// It then calls Next on the context to execute the chain. +type HandlerNode struct { + Method HTTPMethod + Pattern *Pattern + HandlersAndTransformers []any + Next *HandlerNode +} + +// tryMatch attempts to match the handler node's route pattern and http +// method to the a context. It will return true if the handler node +// matches, and false if it does not. +func (n *HandlerNode) tryMatch(ctx *Context) bool { + if n.Method != All && n.Method != ctx.method { + return false + } + if n.Pattern == nil { + return true + } + return n.Pattern.MatchInto(ctx.path, &ctx.params) +} diff --git a/handler.go b/handler.go index 8ecb9ac..b096a93 100644 --- a/handler.go +++ b/handler.go @@ -1,24 +1,5 @@ package navaros -// Transformer is a special type of handler object that can be used to -// transform the context before and after handlers have processed the request. -// This is most useful for modifying or re-encoding the request and response -// bodies. -type Transformer interface { - TransformRequest(ctx *Context) - TransformResponse(ctx *Context) -} - -// HandlerNode is used to build the handler chains used by the context. The -// router builds a chain from these objects then attaches them to the context. -// It then calls Next on the context to execute the chain. -type HandlerNode struct { - Method HTTPMethod - Pattern *Pattern - HandlersAndTransformers []any - Next *HandlerNode -} - // Handler is a handler object interface. Any object that implements this // interface can be used as a handler in a handler chain. type Handler interface { diff --git a/pattern.go b/pattern.go index e5a2845..71527fe 100644 --- a/pattern.go +++ b/pattern.go @@ -2,7 +2,8 @@ package navaros import ( "errors" - "regexp" + + "github.com/grafana/regexp" ) // Pattern is used to compare and match request paths to route patterns. @@ -32,7 +33,7 @@ func NewPattern(patternStr string) (*Pattern, error) { // extracted from the path as per the pattern. If the path matches the pattern, // the second return value will be true. If the path does not match the pattern, // the second return value will be false. -func (p *Pattern) Match(path string) (map[string]string, bool) { +func (p *Pattern) Match(path string) (RequestParams, bool) { matches := p.regExp.FindStringSubmatch(path) if len(matches) == 0 { return nil, false @@ -40,7 +41,7 @@ func (p *Pattern) Match(path string) (map[string]string, bool) { keys := p.regExp.SubexpNames() - params := make(map[string]string) + params := make(RequestParams, len(keys)) for i := 1; i < len(keys); i += 1 { if keys[i] != "" { params[keys[i]] = matches[i] @@ -50,6 +51,30 @@ func (p *Pattern) Match(path string) (map[string]string, bool) { return params, true } +func (p *Pattern) MatchInto(path string, params *RequestParams) bool { + matches := p.regExp.FindStringSubmatch(path) + if len(matches) == 0 { + return false + } + + keys := p.regExp.SubexpNames() + + if *params == nil { + *params = make(map[string]string, len(keys)) + } + + for key := range *params { + delete(*params, key) + } + for i := 1; i < len(keys); i += 1 { + if keys[i] != "" { + (*params)[keys[i]] = matches[i] + } + } + + return true +} + // String returns the string representation of the pattern. func (p *Pattern) String() string { return p.str diff --git a/router_test.go b/router_test.go index 3682637..2f2c2f1 100644 --- a/router_test.go +++ b/router_test.go @@ -2,6 +2,7 @@ package navaros_test import ( "errors" + "net/http" "net/http/httptest" "testing" @@ -36,20 +37,17 @@ func TestRouterGetWithMiddlewareAndHandler(t *testing.T) { r := httptest.NewRequest("GET", "/a/b/c", nil) w := httptest.NewRecorder() - type myStr1 string - type myStr2 string - m := navaros.NewRouter() m.Get("/a/b/c", func(ctx *navaros.Context) { - navaros.CtxSet(ctx, myStr1("Hello")) - navaros.CtxSet(ctx, myStr2("World")) + ctx.Set("str1", "Hello") + ctx.Set("str2", "World") ctx.Next() ctx.Status = 201 }) m.Get("/a/b/c", func(ctx *navaros.Context) { - str1 := navaros.CtxMustGet[myStr1](ctx) - str2 := navaros.CtxMustGet[myStr2](ctx) - ctx.Body = string(str1) + " " + string(str2) + str1 := ctx.Get("str1").(string) + str2 := ctx.Get("str2").(string) + ctx.Body = str1 + " " + str2 }) m.ServeHTTP(w, r) @@ -66,18 +64,15 @@ func TestRouterGetWithMiddlewareAndHandlerInline(t *testing.T) { r := httptest.NewRequest("GET", "/a/b/c", nil) w := httptest.NewRecorder() - type myStr1 string - type myStr2 string - m := navaros.NewRouter() m.Get("/a/b/c", func(ctx *navaros.Context) { - navaros.CtxSet(ctx, myStr1("Hello")) - navaros.CtxSet(ctx, myStr2("World")) + ctx.Set("str1", "Hello") + ctx.Set("str2", "World") ctx.Next() }, func(ctx *navaros.Context) { ctx.Status = 200 - str1 := navaros.CtxMustGet[myStr1](ctx) - str2 := navaros.CtxMustGet[myStr2](ctx) + str1 := ctx.Get("str1").(string) + str2 := ctx.Get("str2").(string) ctx.Body = string(str1) + " " + string(str2) }) @@ -292,3 +287,87 @@ func TestRouterPublicRouteDescriptorsWithSubRouter(t *testing.T) { t.Errorf("expected /a/b/c/a/b/c pattern, got %s", descriptors[3].Pattern.String()) } } + +func BenchmarkRouter(b *testing.B) { + m := navaros.NewRouter() + m.Get("/a/b/c", func(ctx *navaros.Context) { + ctx.Status = 201 + ctx.Body = "Hello World" + }) + + r := httptest.NewRequest("GET", "/a/b/c", nil) + w := httptest.NewRecorder() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + m.ServeHTTP(w, r) + } +} + +func BenchmarkRouterOnHTTPServer(b *testing.B) { + m := navaros.NewRouter() + m.Get("/a/b/c", func(ctx *navaros.Context) { + ctx.Status = 200 + ctx.Body = "Hello World" + }) + + s := httptest.NewServer(m) + defer s.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + var res *http.Response + var err error + b.StartTimer() + res, err = http.Get(s.URL + "/a/b/c") + b.StopTimer() + if err != nil { + b.Error(err) + } + if res.StatusCode != 200 { + b.Errorf("expected 200, got %d", res.StatusCode) + } + } +} + +func BenchmarkGoMux(b *testing.B) { + m := http.NewServeMux() + m.HandleFunc("/a/b/c", func(res http.ResponseWriter, _ *http.Request) { + res.WriteHeader(200) + res.Write([]byte("Hello World")) + }) + + r := httptest.NewRequest("GET", "/a/b/c", nil) + w := httptest.NewRecorder() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + m.ServeHTTP(w, r) + } +} + +func BenchmarkGoMuxOnHTTPServer(b *testing.B) { + m := http.NewServeMux() + m.HandleFunc("/a/b/c", func(res http.ResponseWriter, _ *http.Request) { + res.WriteHeader(200) + res.Write([]byte("Hello World")) + }) + + s := httptest.NewServer(m) + defer s.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + var res *http.Response + var err error + b.StartTimer() + res, err = http.Get(s.URL + "/a/b/c") + b.StopTimer() + if err != nil { + b.Error(err) + } + if res.StatusCode != 200 { + b.Errorf("expected 200, got %d", res.StatusCode) + } + } +} diff --git a/transformer.go b/transformer.go new file mode 100644 index 0000000..1ad4a29 --- /dev/null +++ b/transformer.go @@ -0,0 +1,10 @@ +package navaros + +// Transformer is a special type of handler object that can be used to +// transform the context before and after handlers have processed the request. +// This is most useful for modifying or re-encoding the request and response +// bodies. +type Transformer interface { + TransformRequest(ctx *Context) + TransformResponse(ctx *Context) +}