added multiple method support for routes

This commit is contained in:
2026-01-24 14:44:38 +11:00
parent 1c49b19197
commit 2f49063432
4 changed files with 161 additions and 10 deletions

View File

@@ -51,6 +51,12 @@ func main() {
Method: hws.MethodGET, Method: hws.MethodGET,
Handler: http.HandlerFunc(getUserHandler), Handler: http.HandlerFunc(getUserHandler),
}, },
{
// Single route handling multiple HTTP methods
Path: "/api/resource",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST, hws.MethodPUT},
Handler: http.HandlerFunc(resourceHandler),
},
} }
// Add routes and middleware // Add routes and middleware
@@ -73,6 +79,18 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") id := r.PathValue("id")
w.Write([]byte("User ID: " + id)) w.Write([]byte("User ID: " + id))
} }
func resourceHandler(w http.ResponseWriter, r *http.Request) {
// Handle GET, POST, and PUT for the same path
switch r.Method {
case "GET":
w.Write([]byte("Getting resource"))
case "POST":
w.Write([]byte("Creating resource"))
case "PUT":
w.Write([]byte("Updating resource"))
}
}
``` ```
## Documentation ## Documentation

View File

@@ -74,6 +74,18 @@
// }, // },
// } // }
// //
// A single route can handle multiple HTTP methods using the Methods field:
//
// routes := []hws.Route{
// {
// Path: "/api/resource",
// Methods: []hws.Method{hws.MethodGET, hws.MethodPOST, hws.MethodPUT},
// Handler: http.HandlerFunc(resourceHandler),
// },
// }
//
// Note: The Methods field takes precedence over Method if both are provided.
//
// Path parameters can be accessed using r.PathValue(): // Path parameters can be accessed using r.PathValue():
// //
// func getUser(w http.ResponseWriter, r *http.Request) { // func getUser(w http.ResponseWriter, r *http.Request) {

View File

@@ -4,11 +4,15 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"slices"
) )
type Route struct { type Route struct {
Path string // Absolute path to the requested resource Path string // Absolute path to the requested resource
Method Method // HTTP Method Method Method // HTTP Method
// Methods is an optional slice of Methods to use, if more than one can use the same handler.
// Will take precedence over the Method field if provided
Methods []Method
Handler http.Handler // Handler to use for the request Handler http.Handler // Handler to use for the request
} }
@@ -28,21 +32,33 @@ const (
// Server.AddRoutes registers the page handlers for the server. // Server.AddRoutes registers the page handlers for the server.
// At least one route must be provided. // At least one route must be provided.
// If any route patterns (path + method) are defined multiple times, the first
// instance will be added and any additional conflicts will be discarded.
func (server *Server) AddRoutes(routes ...Route) error { func (server *Server) AddRoutes(routes ...Route) error {
if len(routes) == 0 { if len(routes) == 0 {
return errors.New("No routes provided") return errors.New("No routes provided")
} }
patterns := []string{}
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(http.ResponseWriter, *http.Request) {}) mux.HandleFunc("GET /healthz", func(http.ResponseWriter, *http.Request) {})
for _, route := range routes { for _, route := range routes {
if !validMethod(route.Method) { if len(route.Methods) == 0 {
return fmt.Errorf("Invalid method %s for path %s", route.Method, route.Path) route.Methods = []Method{route.Method}
} }
if route.Handler == nil { for _, method := range route.Methods {
return fmt.Errorf("No handler provided for %s %s", route.Method, route.Path) if !validMethod(method) {
return fmt.Errorf("Invalid method %s for path %s", method, route.Path)
}
if route.Handler == nil {
return fmt.Errorf("No handler provided for %s %s", method, route.Path)
}
pattern := fmt.Sprintf("%s %s", method, route.Path)
if slices.Contains(patterns, pattern) {
continue
}
patterns = append(patterns, pattern)
mux.Handle(pattern, route.Handler)
} }
pattern := fmt.Sprintf("%s %s", route.Method, route.Path)
mux.Handle(pattern, route.Handler)
} }
server.server.Handler = mux server.server.Handler = mux

View File

@@ -122,6 +122,111 @@ func Test_AddRoutes(t *testing.T) {
}) })
} }
func Test_AddRoutes_MultipleMethods(t *testing.T) {
var buf bytes.Buffer
t.Run("Single route with multiple methods", func(t *testing.T) {
server := createTestServer(t, &buf)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(r.Method + " response"))
})
err := server.AddRoutes(hws.Route{
Path: "/api/resource",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST, hws.MethodPUT},
Handler: handler,
})
require.NoError(t, err)
// Test GET request
req := httptest.NewRequest("GET", "/api/resource", nil)
rr := httptest.NewRecorder()
server.Handler().ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "GET response", rr.Body.String())
// Test POST request
req = httptest.NewRequest("POST", "/api/resource", nil)
rr = httptest.NewRecorder()
server.Handler().ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "POST response", rr.Body.String())
// Test PUT request
req = httptest.NewRequest("PUT", "/api/resource", nil)
rr = httptest.NewRecorder()
server.Handler().ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "PUT response", rr.Body.String())
})
t.Run("Methods field takes precedence over Method field", func(t *testing.T) {
server := createTestServer(t, &buf)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
err := server.AddRoutes(hws.Route{
Path: "/test",
Method: hws.MethodGET, // This should be ignored
Methods: []hws.Method{hws.MethodPOST, hws.MethodPUT},
Handler: handler,
})
require.NoError(t, err)
// GET should not work (Method field ignored)
req := httptest.NewRequest("GET", "/test", nil)
rr := httptest.NewRecorder()
server.Handler().ServeHTTP(rr, req)
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
// POST should work (from Methods field)
req = httptest.NewRequest("POST", "/test", nil)
rr = httptest.NewRecorder()
server.Handler().ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
// PUT should work (from Methods field)
req = httptest.NewRequest("PUT", "/test", nil)
rr = httptest.NewRecorder()
server.Handler().ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
})
t.Run("Invalid method in Methods slice", func(t *testing.T) {
server := createTestServer(t, &buf)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
err := server.AddRoutes(hws.Route{
Path: "/test",
Methods: []hws.Method{hws.MethodGET, hws.Method("INVALID")},
Handler: handler,
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "Invalid method")
})
t.Run("Empty Methods slice falls back to Method field", func(t *testing.T) {
server := createTestServer(t, &buf)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
err := server.AddRoutes(hws.Route{
Path: "/test",
Method: hws.MethodGET,
Methods: []hws.Method{}, // Empty slice
Handler: handler,
})
require.NoError(t, err)
// GET should work (from Method field)
req := httptest.NewRequest("GET", "/test", nil)
rr := httptest.NewRecorder()
server.Handler().ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
})
}
func Test_Routes_EndToEnd(t *testing.T) { func Test_Routes_EndToEnd(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
server := createTestServer(t, &buf) server := createTestServer(t, &buf)