aboutsummaryrefslogtreecommitdiff
path: root/pathrouter/catchallvariable.go
blob: 1b986724f39233997a01ff45509fd7c087c99de6 (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
// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
// SPDX-License-Identifier: Apache-2.0

package pathrouter

import (
	"strings"

	. "go.xyrillian.de/gg/option"
)

// CatchAllVariable is a [Matcher] that accepts subpaths with an arbitrary number of path elements.
// The value of any amount of leading path elements will be collected into vars[name],
// such that the remainder is accepted by the next matcher.
// At least one element must be collected into vars[name].
//
// CatchAllVariable() may appear at any point within the routing tree,
// but it may not contain another CatchAllVariable() anywhere within it.
func CatchAllVariable(name string, matcher Matcher) Matcher {
	return catchAllVariable(name, matcher.downcast())
}

func catchAllVariable(name string, matcher realMatcher) Matcher {
	// NOTE: The specific behavior of CatchAllVariable() is why this package exists in the first place.
	//       I wanted to replace gorilla/mux with something more performance in Keppel,
	//       but all the fast routers do not accept catch-all variables in the middle of a path
	//       like the OCI Distribution API requires (e.g. "/v2/*repo/manifests/:reference" with "repo" being a full path).

	innerMinLength := matcher.minLength
	innerMaxLength, ok := matcher.maxLength.Unpack()
	if !ok {
		panic("matcher within CatchAllVariable() may not accept unlimited path lengths")
	}

	accept := func(path []string, vars map[string]string) HandlerFunc {
		for length := innerMinLength; length <= innerMaxLength; length++ {
			if length > len(path) {
				break
			}
			caughtPath, subpath := path[0:len(path)-length], path[len(path)-length:]
			if len(caughtPath) == 0 {
				continue
			}

			handlerFunc := matcher.accept(subpath, vars)
			if handlerFunc == nil {
				continue
			}
			vars[name] = pathUnescape(strings.Join(caughtPath, "/"))
			return handlerFunc
		}
		return nil
	}

	return realMatcher{
		minLength: innerMinLength + 1,
		maxLength: None[int](),
		accept:    accept,
	}
}