From 2f490634321a688eb93f65ca743236f1a88fb252 Mon Sep 17 00:00:00 2001 From: Haelnorr Date: Sat, 24 Jan 2026 14:44:38 +1100 Subject: [PATCH] added multiple method support for routes --- hws/README.md | 18 ++++++++ hws/doc.go | 12 +++++ hws/routes.go | 32 +++++++++---- hws/routes_test.go | 109 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 161 insertions(+), 10 deletions(-) diff --git a/hws/README.md b/hws/README.md index 31fdc39..078e5c8 100644 --- a/hws/README.md +++ b/hws/README.md @@ -51,6 +51,12 @@ func main() { Method: hws.MethodGET, 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 @@ -73,6 +79,18 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) { id := r.PathValue("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 diff --git a/hws/doc.go b/hws/doc.go index 3ff6ad6..0db4d55 100644 --- a/hws/doc.go +++ b/hws/doc.go @@ -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(): // // func getUser(w http.ResponseWriter, r *http.Request) { diff --git a/hws/routes.go b/hws/routes.go index 6805e0f..22e59f9 100644 --- a/hws/routes.go +++ b/hws/routes.go @@ -4,11 +4,15 @@ import ( "errors" "fmt" "net/http" + "slices" ) type Route struct { - Path string // Absolute path to the requested resource - Method Method // HTTP Method + Path string // Absolute path to the requested resource + 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 } @@ -28,21 +32,33 @@ const ( // Server.AddRoutes registers the page handlers for the server. // 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 { if len(routes) == 0 { return errors.New("No routes provided") } + patterns := []string{} mux := http.NewServeMux() mux.HandleFunc("GET /healthz", func(http.ResponseWriter, *http.Request) {}) for _, route := range routes { - if !validMethod(route.Method) { - return fmt.Errorf("Invalid method %s for path %s", route.Method, route.Path) + if len(route.Methods) == 0 { + route.Methods = []Method{route.Method} } - if route.Handler == nil { - return fmt.Errorf("No handler provided for %s %s", route.Method, route.Path) + for _, method := range route.Methods { + 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 diff --git a/hws/routes_test.go b/hws/routes_test.go index 64b77a9..f643920 100644 --- a/hws/routes_test.go +++ b/hws/routes_test.go @@ -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) { var buf bytes.Buffer server := createTestServer(t, &buf) @@ -146,7 +251,7 @@ func Test_Routes_EndToEnd(t *testing.T) { req := httptest.NewRequest("GET", "/get", nil) rr := httptest.NewRecorder() server.Handler().ServeHTTP(rr, req) - + assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, "GET response", rr.Body.String()) @@ -154,7 +259,7 @@ func Test_Routes_EndToEnd(t *testing.T) { req = httptest.NewRequest("POST", "/post", nil) rr = httptest.NewRecorder() server.Handler().ServeHTTP(rr, req) - + assert.Equal(t, http.StatusCreated, rr.Code) assert.Equal(t, "POST response", rr.Body.String()) }