aboutsummaryrefslogtreecommitdiff
path: root/pathrouter/handler.go
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2026-06-27 22:34:36 +0200
committerStefan Majewsky <majewsky@gmx.net>2026-06-27 22:34:36 +0200
commit8e9889d3e5b2874854832d1d882d124132546f49 (patch)
tree80ab6bf53dac3090630ad58d7ebfbbf2c97019ca /pathrouter/handler.go
parent8804729561d8e5edd7179f494d89c620459d319f (diff)
downloadgo-gg-8e9889d3e5b2874854832d1d882d124132546f49.tar.gz
add package pathrouter
Diffstat (limited to 'pathrouter/handler.go')
-rw-r--r--pathrouter/handler.go99
1 files changed, 99 insertions, 0 deletions
diff --git a/pathrouter/handler.go b/pathrouter/handler.go
new file mode 100644
index 0000000..ca493be
--- /dev/null
+++ b/pathrouter/handler.go
@@ -0,0 +1,99 @@
+// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
+// SPDX-License-Identifier: Apache-2.0
+
+package pathrouter
+
+import (
+ "fmt"
+ "net/http"
+ "slices"
+ "strings"
+
+ . "go.xyrillian.de/gg/option"
+)
+
+// HandlerFunc is like [http.HandlerFunc], but also receives variable values that were extracted from the URL path.
+//
+// Package pathrouter passes variables explicitly like this, instead of via [context.Context.WithValue],
+// because doing so imposes a performance penalty for every usage of the respective context.
+type HandlerFunc = func(w http.ResponseWriter, r *http.Request, vars map[string]string)
+
+// ByMethod is a set of request handlers matching the same request path, keyed on request method.
+// It is commonly constructed as a literal using the respective constants from the net/http package, such as [http.MethodGet].
+// This type appears in the signature of func [Handlers], see documentation over there for details.
+type ByMethod map[string]HandlerFunc
+
+// Handlers is a [Matcher] that accepts empty subpaths only.
+// It appears at the leaf nodes of a [Matcher] tree, which correspond to specific endpoint paths,
+// and contains the actual request handlers for one endpoint path:
+//
+// import pr "go.xyrillian.de/gg/pathrouter"
+// handler := pr.Element("v1", pr.Choice(
+// pr.Element("teams", pr.Variable("id", pr.Handlers(pr.ByMethod{
+// http.MethodGet: handleGetTeam, // GET /v1/teams/:id
+// http.MethodPut: handlePutTeam, // PUT /v1/teams/:id
+// http.MethodDelete: handleDeleteTeam, // DELETE /v1/teams/:id
+// }))),
+// pr.Element("employees", pr.Variable("id", pr.Handlers(pr.ByMethod{
+// http.MethodGet: handleGetEmployee, // GET /v1/employees/:id
+// }))),
+// ))
+//
+// If there is a handler for [http.MethodGet], but none for [http.MethodHead], the GET handler will be called for HEAD as well.
+// To have a GET handler, but no HEAD handler, set the handler for [http.MethodHead] to nil.
+// Any other use of a nil [HandlerFunc] is invalid and will cause a panic.
+func Handlers(m ByMethod) Matcher {
+ // reuse GET handler for HEAD if not overridden
+ if handler, exists := m[http.MethodGet]; exists {
+ if _, exists := m[http.MethodHead]; !exists {
+ m[http.MethodHead] = handler
+ }
+ if m[http.MethodHead] == nil {
+ delete(m, http.MethodHead)
+ }
+ }
+
+ // check that all handlers are valid
+ for method, handler := range m {
+ if handler == nil {
+ panic(fmt.Sprintf("handler is nil for method %q", method))
+ }
+ }
+
+ // precomputations for accept()
+ allowHeader := m.buildAllowHeader()
+ serve := func(w http.ResponseWriter, r *http.Request, vars map[string]string) {
+ handler, ok := m[r.Method]
+ if ok {
+ handler(w, r, vars)
+ return
+ }
+
+ w.Header().Set("Allow", allowHeader)
+ if r.Method == http.MethodOptions {
+ http.Error(w, "", http.StatusOK)
+ } else {
+ http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+ }
+ }
+
+ return realMatcher{
+ minLength: 0,
+ maxLength: Some(0),
+ accept: func(path []string, vars map[string]string) HandlerFunc {
+ if len(path) != 0 {
+ return nil
+ }
+ return serve
+ },
+ }
+}
+
+func (m ByMethod) buildAllowHeader() string {
+ allowedMethods := make([]string, 0, len(m))
+ for method := range m {
+ allowedMethods = append(allowedMethods, method)
+ }
+ slices.Sort(allowedMethods)
+ return strings.Join(allowedMethods, ", ")
+}