Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package chi

import (
"context"
"net/http"
)

type contextKey struct {
name string
}

var RouteCtxKey = &contextKey{"RouteContext"}

type Context struct {
Routes Routes

// Routing path tracking
RoutePath string

// URLParams key-value pairs
URLParams RouteParams
}

type RouteParams struct {
Keys []string
Values []string
}

func (s *RouteParams) Add(key, value string) {
s.Keys = append(s.Keys, key)
s.Values = append(s.Values, value)
}

func (s *RouteParams) Get(key string) string {
for i, k := range s.Keys {
if k == key {
return s.Values[i]
}
}
return ""
}

func NewRouteContext() *Context {
return &Context{}
}

func RouteContext(ctx context.Context) *Context {
if rctx, ok := ctx.Value(RouteCtxKey).(*Context); ok {
return rctx
}
return nil
}

func URLParam(r *http.Request, key string) string {
if rctx := RouteContext(r.Context()); rctx != nil {
return rctx.URLParams.Get(key)
}
return ""
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/madalynerlge2/chi

go 1.20
126 changes: 126 additions & 0 deletions mux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package chi

import (
"context"
"net/http"
"strings"
)

type Routes interface {
Routes() []Route
}

type Route interface{}

type Router interface {
http.Handler
Routes
Use(middlewares ...func(http.Handler) http.Handler)
Get(pattern string, handlerFn http.HandlerFunc)
Handle(pattern string, handler http.Handler)
}

type Mux struct {
middlewares []func(http.Handler) http.Handler
routes []routeEntry
}

type routeEntry struct {
pattern string
handler http.Handler
}

func NewRouter() *Mux {
return &Mux{}
}

func (m *Mux) Use(middlewares ...func(http.Handler) http.Handler) {
m.middlewares = append(m.middlewares, middlewares...)
}

func (m *Mux) Get(pattern string, handlerFn http.HandlerFunc) {
m.Handle(pattern, handlerFn)
}

func (m *Mux) Handle(pattern string, handler http.Handler) {
m.routes = append(m.routes, routeEntry{
pattern: pattern,
handler: handler,
})
}

func (m *Mux) Routes() []Route {
return nil
}

func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rctx := RouteContext(r.Context())
if rctx == nil {
rctx = NewRouteContext()
rctx.RoutePath = r.URL.Path
r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx))
}

// Chain middlewares
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m.routeHTTP(w, r)
})

for i := len(m.middlewares) - 1; i >= 0; i-- {
handler = m.middlewares[i](handler)
}

handler.ServeHTTP(w, r)
}

func (m *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
rctx := RouteContext(r.Context())
if rctx != nil {
// CRITICAL FIX FOR ISSUE #1:
// Synchronize RoutePath if middleware mutated r.URL.Path
if rctx.RoutePath != r.URL.Path {
rctx.RoutePath = r.URL.Path
}
}

currentPath := r.URL.Path
if rctx != nil && rctx.RoutePath != "" {
currentPath = rctx.RoutePath
}

for _, entry := range m.routes {
params, ok := matchPattern(entry.pattern, currentPath)
if ok {
if rctx != nil {
for k, v := range params {
rctx.URLParams.Add(k, v)
}
}
entry.handler.ServeHTTP(w, r)
return
}
}

http.NotFound(w, r)
}

func matchPattern(pattern, path string) (map[string]string, bool) {
patParts := strings.Split(strings.Trim(pattern, "/"), "/")
pathParts := strings.Split(strings.Trim(path, "/"), "/")

if len(patParts) != len(pathParts) {
return nil, false
}

params := make(map[string]string)
for i := 0; i < len(patParts); i++ {
if strings.HasPrefix(patParts[i], "{") && strings.HasSuffix(patParts[i], "}") {
paramName := patParts[i][1 : len(patParts[i])-1]
params[paramName] = pathParts[i]
} else if patParts[i] != pathParts[i] {
return nil, false
}
}

return params, true
}
44 changes: 44 additions & 0 deletions mux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package chi_test

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/madalynerlge2/chi"
)

func TestMiddlewarePathRewriteURLParams(t *testing.T) {
r := chi.NewRouter()

// Middleware that rewrites the path from /legacy/123 to /users/123
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/legacy/123" {
req.URL.Path = "/users/123"
}
next.ServeHTTP(w, req)
})
})

r.Get("/users/{id}", func(w http.ResponseWriter, req *http.Request) {
id := chi.URLParam(req, "id")
if id != "123" {
t.Errorf("expected URL param 'id' to be '123', got '%s'", id)
}
w.Write([]byte("ok"))
})

ts := httptest.NewServer(r)
defer ts.Close()

res, err := http.Get(ts.URL + "/legacy/123")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", res.StatusCode)
}
}