aboutsummaryrefslogtreecommitdiff
path: root/pathrouter/handler.go
blob: ca493be64c050f54c78271197cbd26d728d09d79 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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, ", ")
}