Skip to content

Commit

Permalink
Merge pull request #19 from gossie/feature/#18-migrate-to-gos-new-mux…
Browse files Browse the repository at this point in the history
…-api-122

#18 migrates to 1.22
  • Loading branch information
gossie authored Mar 13, 2024
2 parents ef642e8 + cceb949 commit c7c444f
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 483 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: 1.22

- name: Build
run: go build -v ./...
Expand Down
55 changes: 18 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

The module provides an HTTP router that integrates with Go's `net/http` package. That means the application code uses `http.ResponseWriter` and `http.Request` from Go's standard library.
It supports
- path variables
- standard middleware functions
- custom middleware functions

Expand All @@ -11,11 +10,14 @@ It supports
There are already a lot of mux implementations. But after a brief search I only found implementations that did not match my requirements oder that overfullfill them.
The things I wanted were
- path variables
- routes for certain methods
- built-in middleware support
- I wanted to stay as close to Go's `net/http` package as possible

And last but certainly not least: **It's a lot of fun to implement such a thing yourself!**

Since Go 1.22 the standard http package supports path variables and the possibility to specify a handler for a method-path combination. This HTTP router was migrated to make use of the new standard features. So now it just provides a different (but in my opinion clearer) API and the possibility to define middleware functions.

## Usage

A simple server could look like this:
Expand All @@ -33,11 +35,13 @@ func main() {
httpRouter.Post("/books", createBookHandler)
httpRouter.Get("/books/:bookId", getSingleBookHandler)

httpRouter.FinishSetup()

log.Fatal(http.ListenAndServe(":8080", httpRouter))
}
```
The code creates two `GET` and one `POST` route to retrieve and create books. The first parameter is the path, that may contain path variables. Path variables start with a `:`. The second parameter is the handler function that handles the request. A handler function must be of the following type: `type HttpHandler func(http.ResponseWriter, *http.Request, router.Context)`
The first and second parameter are the `ResponseWriter` and the `Request` of Go's `http` package. The third parameter is a `map` containing the path variables. The key is the name the way it was used in the route's path. In this example the third route would contain a value for the key `bookId`.
The code creates two `GET` and one `POST` route to retrieve and create books. The first parameter is the path, that may contain path variables. Path variables start with a `:`. The second parameter is the handler function that handles the request. A handler function must be of the following type: `type HttpHandler func(http.ResponseWriter, *http.Request)`
The first and second parameter are the `ResponseWriter` and the `Request` of Go's `http` package.

## Middleware

Expand All @@ -52,13 +56,13 @@ import (
)

func middleware1(handler router.HttpHandler) router.HttpHandler {
return func(w http.ResponseWriter, r *http.Request, ctx router.Context) {
return func(w http.ResponseWriter, r *http.Request) {
// ...
}
}

func middleware2(handler router.HttpHandler) router.HttpHandler {
return func(w http.ResponseWriter, r *http.Request, ctx router.Context) {
return func(w http.ResponseWriter, r *http.Request) {
// ...
}
}
Expand All @@ -71,46 +75,17 @@ func main() {
httpRouter.Get("/test1", publicHandler)
httpRouter.Post("/test2", protectedHanlder).Use(middleware2)

log.Fatal(http.ListenAndServe(":8080", httpRouter))
}
```

There is a third way to add a middleware function. It is possible to define a middleware function for a certain path and HTTP method.

```go
import (
"net/http"

"github.com/gossie/router"
)

func middleware(handler router.HttpHandler) router.HttpHandler {
return func(w http.ResponseWriter, r *http.Request, ctx router.Context) {
// ...
}
}

func main() {
httpRouter := router.New()

testRouter.UseRecursively(router.GET, "/tests", middleware)

httpRouter.Get("/tests", testsHandler)
httpRouter.Get("/tests/:testId", singleTestHandler)
httpRouter.Get("/tests/:testId/assertions", assertionsHandler)
httpRouter.Get("/other", otherHandler)
httpRouter.FinishSetup()

log.Fatal(http.ListenAndServe(":8080", httpRouter))
}
```

The code makes sure that the middleware function is executed for `GET` request targeting `/tests`, `/tests/:testId` and `/tests/:testId/assertions`. It won't be executed when `/other` is called.

### Standard middleware functions

#### Basic auth

The module provides a standard middleware function for basic authentication. The line `testRouter.Use(router.BasicAuth(userChecker))` adds basic auth to the router. The `userChecker` is a function that checks if the authentication data is correct.
The module provides a standard middleware function for basic authentication. The line `testRouter.Use(router.BasicAuth(userChecker))` adds basic auth to the router. The `userChecker` is a function that checks if the authentication data is correct. If the user was authenticated, the username will be added to the `context` of the request under the key `router.UsernameKey`.

```go
import (
Expand All @@ -132,6 +107,8 @@ func main() {
httpRouter.Post("/books", createBookHandler)
httpRouter.Get("/books/:bookId", getSingleBookHandler)

httpRouter.FinishSetup()

log.Fatal(http.ListenAndServe(":8080", httpRouter))
}
```
Expand All @@ -156,6 +133,8 @@ func main() {
httpRouter.Post("/books", createBookHandler)
httpRouter.Get("/books/:bookId", getSingleBookHandler)

httpRouter.FinishSetup()

log.Fatal(http.ListenAndServe(":8080", httpRouter))
}
```
Expand All @@ -170,7 +149,7 @@ import (
)

func logRequestTime(handler router.HttpHandler) router.HttpHandler {
return func(w http.ResponseWriter, r *http.Request, ctx router.Context) {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
defer func() {
log.Default().Println("request took", time.Since(start).Milliseconds(), "ms")
Expand All @@ -189,6 +168,8 @@ func main() {
httpRouter.Post("/books", createBookHandler)
httpRouter.Get("/books/:bookId", getSingleBookHandler)

httpRouter.FinishSetup()

log.Fatal(http.ListenAndServe(":8080", httpRouter))
}
```
15 changes: 10 additions & 5 deletions basicauth.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package router

import (
"context"
"net/http"
)

type usernamekey string

const UsernameKey = usernamekey("username")

type UserData struct {
username, password string
}
Expand All @@ -24,16 +29,16 @@ type UserChecker = func(*UserData) bool

func BasicAuth(userChecker UserChecker) Middleware {
return func(next HttpHandler) HttpHandler {
return func(w http.ResponseWriter, r *http.Request, ctx Context) {
performBasicAuth(w, r, ctx, userChecker, next)
return func(w http.ResponseWriter, r *http.Request) {
performBasicAuth(w, r, userChecker, next)
}
}
}

func performBasicAuth(w http.ResponseWriter, r *http.Request, ctx Context, userChecker UserChecker, next HttpHandler) {
func performBasicAuth(w http.ResponseWriter, r *http.Request, userChecker UserChecker, next HttpHandler) {
if user, pass, ok := r.BasicAuth(); ok && userChecker(newUserData(user, pass)) {
ctx.username = user
next(w, r, ctx)

next(w, r.WithContext(context.WithValue(r.Context(), UsernameKey, user)))
return
}
http.Error(w, "", http.StatusUnauthorized)
Expand Down
14 changes: 10 additions & 4 deletions basicauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ func TestBasicAuth_noAuthData(t *testing.T) {
}

testRouter := router.New()
testRouter.Get("/protected", func(_ http.ResponseWriter, _ *http.Request, _ router.Context) {
testRouter.Get("/protected", func(_ http.ResponseWriter, _ *http.Request) {
assert.Fail(t, "handler must not be called")
})
testRouter.Use(router.BasicAuth(userChecker))

testRouter.FinishSetup()

w := &TestResponseWriter{}
r := &http.Request{
Method: "GET",
Expand All @@ -37,11 +39,13 @@ func TestBasicAuth_wrongAuthData(t *testing.T) {
}

testRouter := router.New()
testRouter.Get("/protected", func(_ http.ResponseWriter, _ *http.Request, _ router.Context) {
testRouter.Get("/protected", func(_ http.ResponseWriter, _ *http.Request) {
assert.Fail(t, "handler must not be called")
})
testRouter.Use(router.BasicAuth(userChecker))

testRouter.FinishSetup()

userStr := base64.StdEncoding.EncodeToString([]byte("user2:wrong"))

w := &TestResponseWriter{}
Expand All @@ -61,12 +65,14 @@ func TestBasicAuth_correctAuthData(t *testing.T) {
}

testRouter := router.New()
testRouter.Get("/protected", func(w http.ResponseWriter, r *http.Request, ctx router.Context) {
assert.Equal(t, "user2", ctx.Username())
testRouter.Get("/protected", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "user2", r.Context().Value(router.UsernameKey))
w.WriteHeader(200)
})
testRouter.Use(router.BasicAuth(userChecker))

testRouter.FinishSetup()

userStr := base64.StdEncoding.EncodeToString([]byte("user2:password2"))

w := &TestResponseWriter{}
Expand Down
4 changes: 2 additions & 2 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import (

func Cache(duration time.Duration) Middleware {
return func(next HttpHandler) HttpHandler {
return func(w http.ResponseWriter, r *http.Request, ctx Context) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", fmt.Sprintf("public, maxage=%v, s-maxage=%v, immutable", duration.Seconds(), duration.Seconds()))
w.Header().Set("Expires", time.Now().Add(duration).Local().Format("Mon, 02 Jan 2006 15:04:05 MST"))
next(w, r, ctx)
next(w, r)
}
}
}
8 changes: 6 additions & 2 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import (

func TestCache_noCache(t *testing.T) {
testRouter := router.New()
testRouter.Get("/route", func(w http.ResponseWriter, _ *http.Request, _ router.Context) {
testRouter.Get("/route", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})

testRouter.FinishSetup()

w := &TestResponseWriter{}
r := &http.Request{
Method: "GET",
Expand All @@ -29,11 +31,13 @@ func TestCache_noCache(t *testing.T) {

func TestCache_cache(t *testing.T) {
testRouter := router.New()
testRouter.Get("/route", func(w http.ResponseWriter, _ *http.Request, _ router.Context) {
testRouter.Get("/route", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
testRouter.Use(router.Cache(1 * time.Hour))

testRouter.FinishSetup()

w := &TestResponseWriter{}
r := &http.Request{
Method: "GET",
Expand Down
29 changes: 0 additions & 29 deletions context.go

This file was deleted.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/gossie/router

go 1.19
go 1.22

require github.com/stretchr/testify v1.8.4

Expand Down
Loading

0 comments on commit c7c444f

Please sign in to comment.