~repos /gromer
git clone https://pyrossh.dev/repos/gromer.git
gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.
f3a4c91b
—
pyros2097 5 years ago
make routing work
- app_nowasm.go +42 -26
- app_wasm.go +22 -2
- attributes_test.go +2 -2
- example/main.go +7 -9
- go.mod +1 -0
- go.sum +2 -0
- tree.go +1281 -0
- utils.go +5 -29
app_nowasm.go
CHANGED
|
@@ -8,46 +8,57 @@ import (
|
|
|
8
8
|
"net/http"
|
|
9
9
|
"os"
|
|
10
10
|
"path/filepath"
|
|
11
|
-
"
|
|
11
|
+
"strings"
|
|
12
12
|
|
|
13
13
|
"github.com/akrylysov/algnhsa"
|
|
14
14
|
"github.com/markbates/pkger"
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
+
// ServeFiles serves files from the given file system root.
|
|
18
|
+
// The path must end with "/*filepath", files are then served from the local
|
|
19
|
+
// path /defined/root/dir/*filepath.
|
|
20
|
+
// For example if root is "/etc" and *filepath is "passwd", the local file
|
|
21
|
+
// "/etc/passwd" would be served.
|
|
22
|
+
// Internally a http.FileServer is used, therefore http.NotFound is used instead
|
|
23
|
+
// of the Router's NotFound handler.
|
|
24
|
+
// To use the operating system's file system implementation,
|
|
17
|
-
|
|
25
|
+
// use http.Dir:
|
|
26
|
+
// router.ServeFiles("/src/*filepath", http.Dir("/var/www"))
|
|
27
|
+
// func (r *Router) ServeFiles(path string, root http.FileSystem) {
|
|
28
|
+
// if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
|
|
29
|
+
// panic("path must end with /*filepath in path '" + path + "'")
|
|
18
|
-
|
|
30
|
+
// }
|
|
31
|
+
|
|
19
|
-
|
|
32
|
+
// fileServer := http.FileServer(root)
|
|
20
|
-
Author string
|
|
21
|
-
Keywords string
|
|
22
|
-
}
|
|
23
33
|
|
|
34
|
+
// r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) {
|
|
24
|
-
|
|
35
|
+
// req.URL.Path = ps.ByName("filepath")
|
|
36
|
+
// fileServer.ServeHTTP(w, req)
|
|
37
|
+
// })
|
|
38
|
+
// }
|
|
39
|
+
|
|
40
|
+
func Run() {
|
|
25
41
|
isLambda := os.Getenv("_LAMBDA_SERVER_PORT") != ""
|
|
26
|
-
wd, err := os.Getwd()
|
|
27
|
-
if err != nil {
|
|
28
|
-
fmt.Printf("could not get wd")
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
42
|
if !isLambda {
|
|
43
|
+
wd, err := os.Getwd()
|
|
44
|
+
if err != nil {
|
|
45
|
+
fmt.Printf("could not get wd")
|
|
46
|
+
return
|
|
47
|
+
}
|
|
32
|
-
|
|
48
|
+
assetsFS := http.FileServer(pkger.Dir(filepath.Join(wd, "assets")))
|
|
49
|
+
router.GET("/assets/*filepath", func(w http.ResponseWriter, r *http.Request) {
|
|
50
|
+
r.URL.Path = strings.Replace(r.URL.Path, "/assets", "", 1)
|
|
51
|
+
assetsFS.ServeHTTP(w, r)
|
|
52
|
+
})
|
|
33
53
|
}
|
|
34
|
-
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
35
|
-
println("route: " + r.URL.Path)
|
|
36
|
-
renderFunc := MatchRoute(routes, r.URL.Path)
|
|
37
|
-
page := createPage(info, renderFunc(NewRenderContext()))
|
|
38
|
-
w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
|
|
39
|
-
w.Header().Set("Content-Type", "text/html")
|
|
40
|
-
w.WriteHeader(http.StatusOK)
|
|
41
|
-
w.Write(page.Bytes())
|
|
42
|
-
})
|
|
43
54
|
if isLambda {
|
|
44
55
|
println("running in lambda mode")
|
|
45
|
-
algnhsa.ListenAndServe(
|
|
56
|
+
algnhsa.ListenAndServe(router, &algnhsa.Options{
|
|
46
57
|
BinaryContentTypes: []string{"application/wasm", "image/png"},
|
|
47
58
|
})
|
|
48
59
|
} else {
|
|
49
60
|
println("Serving on HTTP port: 1234")
|
|
50
|
-
http.ListenAndServe(":1234",
|
|
61
|
+
http.ListenAndServe(":1234", router)
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
|
|
@@ -55,7 +66,12 @@ func Reload() {
|
|
|
55
66
|
panic("wasm required")
|
|
56
67
|
}
|
|
57
68
|
|
|
69
|
+
func Route(path string, render RenderFunc, info RouteInfo) {
|
|
70
|
+
println("registering route: " + path)
|
|
71
|
+
router.GET(path, render)
|
|
72
|
+
}
|
|
73
|
+
|
|
58
|
-
func createPage(info
|
|
74
|
+
func createPage(info RouteInfo, ui UI) *bytes.Buffer {
|
|
59
75
|
isLambda := os.Getenv("_LAMBDA_SERVER_PORT") != ""
|
|
60
76
|
page := bytes.NewBuffer(nil)
|
|
61
77
|
page.WriteString("<!DOCTYPE html>\n")
|
app_wasm.go
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
package app
|
|
3
3
|
|
|
4
4
|
import (
|
|
5
|
+
"bytes"
|
|
6
|
+
|
|
5
7
|
"github.com/pyros2097/wapp/js"
|
|
6
8
|
)
|
|
7
9
|
|
|
@@ -9,10 +11,20 @@ var (
|
|
|
9
11
|
body *elem
|
|
10
12
|
content UI
|
|
11
13
|
rootPrefix string
|
|
14
|
+
renderFunc RenderFunc
|
|
12
15
|
)
|
|
13
16
|
|
|
14
|
-
func Run(
|
|
17
|
+
func Run() {
|
|
15
|
-
|
|
18
|
+
handle, _, _ := router.Lookup("GET", js.Window.URL().Path)
|
|
19
|
+
if handle == nil {
|
|
20
|
+
if router.NotFound != nil {
|
|
21
|
+
renderFunc = router.NotFound
|
|
22
|
+
} else {
|
|
23
|
+
renderFunc = DefaultNotFound
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
renderFunc, _ = handle.(RenderFunc)
|
|
27
|
+
}
|
|
16
28
|
defer func() {
|
|
17
29
|
err := recover()
|
|
18
30
|
// show alert
|
|
@@ -40,6 +52,10 @@ func Reload() {
|
|
|
40
52
|
})
|
|
41
53
|
}
|
|
42
54
|
|
|
55
|
+
func Route(path string, render RenderFunc, info RouteInfo) {
|
|
56
|
+
router.GET(path, render)
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
func initBody() {
|
|
44
60
|
body = &elem{
|
|
45
61
|
jsvalue: js.Window.Get("document").Get("body"),
|
|
@@ -71,3 +87,7 @@ func initContent() {
|
|
|
71
87
|
// func isFragmentNavigation(u *url.URL) bool {
|
|
72
88
|
// return u.Fragment != ""
|
|
73
89
|
// }
|
|
90
|
+
|
|
91
|
+
func createPage(info RouteInfo, ui UI) *bytes.Buffer {
|
|
92
|
+
return &bytes.Buffer{}
|
|
93
|
+
}
|
attributes_test.go
CHANGED
|
@@ -28,7 +28,7 @@ func Counter(c *RenderContext) UI {
|
|
|
28
28
|
)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
func
|
|
31
|
+
func TestRoute(c *RenderContext) UI {
|
|
32
32
|
return Div(
|
|
33
33
|
Div(),
|
|
34
34
|
Counter(c),
|
|
@@ -42,7 +42,7 @@ func TestCreatePage(t *testing.T) {
|
|
|
42
42
|
Head(
|
|
43
43
|
Title("Title"),
|
|
44
44
|
),
|
|
45
|
-
Body(
|
|
45
|
+
Body(TestRoute(NewRenderContext())),
|
|
46
46
|
).Html(page)
|
|
47
47
|
assert.Equal(t, "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\">\n <meta http-equiv=\"encoding\" content=\"utf-8\">\n <title>\n Title\n </title>\n </head>\n <body>\n <div>\n <div></div>\n <div class=\"flex flex-col justify-center align-items-center\">\n <div class=\"flex flex-row justify-center align-items-center yellow\">\n Counter\n </div>\n <div class=\"flex flex-row justify-center align-items-center\">\n <div>\n -\n </div>\n <div>\n 0\n </div>\n <div>\n +\n </div>\n </div>\n </div>\n </div>\n </body>\n</html>", page.String())
|
|
48
48
|
}
|
example/main.go
CHANGED
|
@@ -4,18 +4,16 @@ import (
|
|
|
4
4
|
. "github.com/pyros2097/wapp"
|
|
5
5
|
)
|
|
6
6
|
|
|
7
|
-
var routes = map[string]RenderFunc{
|
|
8
|
-
"/about": About,
|
|
9
|
-
"/clock": Clock,
|
|
10
|
-
"/container": Container,
|
|
11
|
-
"/": Index,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
7
|
func main() {
|
|
15
|
-
|
|
8
|
+
info := RouteInfo{
|
|
16
9
|
Title: "wapp-example",
|
|
17
10
|
Description: "wapp is a framework",
|
|
18
11
|
Author: "pyros2097",
|
|
19
12
|
Keywords: "wapp,wapp-example,golang,framework,frontend,ui,wasm,isomorphic",
|
|
13
|
+
}
|
|
14
|
+
Route("/about", About, info)
|
|
15
|
+
Route("/clock", Clock, info)
|
|
16
|
+
Route("/container", Container, info)
|
|
17
|
+
Route("/", Index, info)
|
|
20
|
-
|
|
18
|
+
Run()
|
|
21
19
|
}
|
go.mod
CHANGED
|
@@ -6,6 +6,7 @@ require (
|
|
|
6
6
|
github.com/akrylysov/algnhsa v0.12.1
|
|
7
7
|
github.com/aws/aws-lambda-go v1.20.0
|
|
8
8
|
github.com/awslabs/goformation/v4 v4.15.6
|
|
9
|
+
github.com/julienschmidt/httprouter v1.3.0
|
|
9
10
|
github.com/kevinpollet/nego v0.0.0-20200702060216-3ff8e9f14a70
|
|
10
11
|
github.com/markbates/pkger v0.17.1
|
|
11
12
|
github.com/shurcooL/httpgzip v0.0.0-20190720172056-320755c1c1b0
|
go.sum
CHANGED
|
@@ -30,6 +30,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|
|
30
30
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
31
31
|
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
|
32
32
|
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
|
33
|
+
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
|
34
|
+
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
|
33
35
|
github.com/kevinpollet/nego v0.0.0-20200702060216-3ff8e9f14a70 h1:vl63cy3DUIN3iZI5oU/2yfVXEFVPc5ahbwjWk0aF3nA=
|
|
34
36
|
github.com/kevinpollet/nego v0.0.0-20200702060216-3ff8e9f14a70/go.mod h1:bST7PtmFt4otZfrYPAUmYA1v3hZBhX4ttQzBSxeRE4E=
|
|
35
37
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
tree.go
ADDED
|
@@ -0,0 +1,1281 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"net/http"
|
|
6
|
+
"strconv"
|
|
7
|
+
"strings"
|
|
8
|
+
"sync"
|
|
9
|
+
"unicode"
|
|
10
|
+
"unicode/utf8"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// CleanPath is the URL version of path.Clean, it returns a canonical URL path
|
|
14
|
+
// for p, eliminating . and .. elements.
|
|
15
|
+
//
|
|
16
|
+
// The following rules are applied iteratively until no further processing can
|
|
17
|
+
// be done:
|
|
18
|
+
// 1. Replace multiple slashes with a single slash.
|
|
19
|
+
// 2. Eliminate each . path name element (the current directory).
|
|
20
|
+
// 3. Eliminate each inner .. path name element (the parent directory)
|
|
21
|
+
// along with the non-.. element that precedes it.
|
|
22
|
+
// 4. Eliminate .. elements that begin a rooted path:
|
|
23
|
+
// that is, replace "/.." by "/" at the beginning of a path.
|
|
24
|
+
//
|
|
25
|
+
// If the result of this process is an empty string, "/" is returned
|
|
26
|
+
func CleanPath(p string) string {
|
|
27
|
+
const stackBufSize = 128
|
|
28
|
+
|
|
29
|
+
// Turn empty string into "/"
|
|
30
|
+
if p == "" {
|
|
31
|
+
return "/"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Reasonably sized buffer on stack to avoid allocations in the common case.
|
|
35
|
+
// If a larger buffer is required, it gets allocated dynamically.
|
|
36
|
+
buf := make([]byte, 0, stackBufSize)
|
|
37
|
+
|
|
38
|
+
n := len(p)
|
|
39
|
+
|
|
40
|
+
// Invariants:
|
|
41
|
+
// reading from path; r is index of next byte to process.
|
|
42
|
+
// writing to buf; w is index of next byte to write.
|
|
43
|
+
|
|
44
|
+
// path must start with '/'
|
|
45
|
+
r := 1
|
|
46
|
+
w := 1
|
|
47
|
+
|
|
48
|
+
if p[0] != '/' {
|
|
49
|
+
r = 0
|
|
50
|
+
|
|
51
|
+
if n+1 > stackBufSize {
|
|
52
|
+
buf = make([]byte, n+1)
|
|
53
|
+
} else {
|
|
54
|
+
buf = buf[:n+1]
|
|
55
|
+
}
|
|
56
|
+
buf[0] = '/'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
trailing := n > 1 && p[n-1] == '/'
|
|
60
|
+
|
|
61
|
+
// A bit more clunky without a 'lazybuf' like the path package, but the loop
|
|
62
|
+
// gets completely inlined (bufApp calls).
|
|
63
|
+
// So in contrast to the path package this loop has no expensive function
|
|
64
|
+
// calls (except make, if needed).
|
|
65
|
+
|
|
66
|
+
for r < n {
|
|
67
|
+
switch {
|
|
68
|
+
case p[r] == '/':
|
|
69
|
+
// empty path element, trailing slash is added after the end
|
|
70
|
+
r++
|
|
71
|
+
|
|
72
|
+
case p[r] == '.' && r+1 == n:
|
|
73
|
+
trailing = true
|
|
74
|
+
r++
|
|
75
|
+
|
|
76
|
+
case p[r] == '.' && p[r+1] == '/':
|
|
77
|
+
// . element
|
|
78
|
+
r += 2
|
|
79
|
+
|
|
80
|
+
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
|
|
81
|
+
// .. element: remove to last /
|
|
82
|
+
r += 3
|
|
83
|
+
|
|
84
|
+
if w > 1 {
|
|
85
|
+
// can backtrack
|
|
86
|
+
w--
|
|
87
|
+
|
|
88
|
+
if len(buf) == 0 {
|
|
89
|
+
for w > 1 && p[w] != '/' {
|
|
90
|
+
w--
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
for w > 1 && buf[w] != '/' {
|
|
94
|
+
w--
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
default:
|
|
100
|
+
// Real path element.
|
|
101
|
+
// Add slash if needed
|
|
102
|
+
if w > 1 {
|
|
103
|
+
bufApp(&buf, p, w, '/')
|
|
104
|
+
w++
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Copy element
|
|
108
|
+
for r < n && p[r] != '/' {
|
|
109
|
+
bufApp(&buf, p, w, p[r])
|
|
110
|
+
w++
|
|
111
|
+
r++
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Re-append trailing slash
|
|
117
|
+
if trailing && w > 1 {
|
|
118
|
+
bufApp(&buf, p, w, '/')
|
|
119
|
+
w++
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// If the original string was not modified (or only shortened at the end),
|
|
123
|
+
// return the respective substring of the original string.
|
|
124
|
+
// Otherwise return a new string from the buffer.
|
|
125
|
+
if len(buf) == 0 {
|
|
126
|
+
return p[:w]
|
|
127
|
+
}
|
|
128
|
+
return string(buf[:w])
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Internal helper to lazily create a buffer if necessary.
|
|
132
|
+
// Calls to this function get inlined.
|
|
133
|
+
func bufApp(buf *[]byte, s string, w int, c byte) {
|
|
134
|
+
b := *buf
|
|
135
|
+
if len(b) == 0 {
|
|
136
|
+
// No modification of the original string so far.
|
|
137
|
+
// If the next character is the same as in the original string, we do
|
|
138
|
+
// not yet have to allocate a buffer.
|
|
139
|
+
if s[w] == c {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Otherwise use either the stack buffer, if it is large enough, or
|
|
144
|
+
// allocate a new buffer on the heap, and copy all previous characters.
|
|
145
|
+
if l := len(s); l > cap(b) {
|
|
146
|
+
*buf = make([]byte, len(s))
|
|
147
|
+
} else {
|
|
148
|
+
*buf = (*buf)[:l]
|
|
149
|
+
}
|
|
150
|
+
b = *buf
|
|
151
|
+
|
|
152
|
+
copy(b, s[:w])
|
|
153
|
+
}
|
|
154
|
+
b[w] = c
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func min(a, b int) int {
|
|
158
|
+
if a <= b {
|
|
159
|
+
return a
|
|
160
|
+
}
|
|
161
|
+
return b
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func longestCommonPrefix(a, b string) int {
|
|
165
|
+
i := 0
|
|
166
|
+
max := min(len(a), len(b))
|
|
167
|
+
for i < max && a[i] == b[i] {
|
|
168
|
+
i++
|
|
169
|
+
}
|
|
170
|
+
return i
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Search for a wildcard segment and check the name for invalid characters.
|
|
174
|
+
// Returns -1 as index, if no wildcard was found.
|
|
175
|
+
func findWildcard(path string) (wilcard string, i int, valid bool) {
|
|
176
|
+
// Find start
|
|
177
|
+
for start, c := range []byte(path) {
|
|
178
|
+
// A wildcard starts with ':' (param) or '*' (catch-all)
|
|
179
|
+
if c != ':' && c != '*' {
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Find end and check for invalid characters
|
|
184
|
+
valid = true
|
|
185
|
+
for end, c := range []byte(path[start+1:]) {
|
|
186
|
+
switch c {
|
|
187
|
+
case '/':
|
|
188
|
+
return path[start : start+1+end], start, valid
|
|
189
|
+
case ':', '*':
|
|
190
|
+
valid = false
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return path[start:], start, valid
|
|
194
|
+
}
|
|
195
|
+
return "", -1, false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func countParams(path string) uint16 {
|
|
199
|
+
var n uint
|
|
200
|
+
for i := range []byte(path) {
|
|
201
|
+
switch path[i] {
|
|
202
|
+
case ':', '*':
|
|
203
|
+
n++
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return uint16(n)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
type nodeType uint8
|
|
210
|
+
|
|
211
|
+
const (
|
|
212
|
+
static nodeType = iota // default
|
|
213
|
+
root
|
|
214
|
+
param
|
|
215
|
+
catchAll
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
type node struct {
|
|
219
|
+
path string
|
|
220
|
+
indices string
|
|
221
|
+
wildChild bool
|
|
222
|
+
nType nodeType
|
|
223
|
+
priority uint32
|
|
224
|
+
children []*node
|
|
225
|
+
handle interface{}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Increments priority of the given child and reorders if necessary
|
|
229
|
+
func (n *node) incrementChildPrio(pos int) int {
|
|
230
|
+
cs := n.children
|
|
231
|
+
cs[pos].priority++
|
|
232
|
+
prio := cs[pos].priority
|
|
233
|
+
|
|
234
|
+
// Adjust position (move to front)
|
|
235
|
+
newPos := pos
|
|
236
|
+
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
|
|
237
|
+
// Swap node positions
|
|
238
|
+
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Build new index char string
|
|
242
|
+
if newPos != pos {
|
|
243
|
+
n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
|
|
244
|
+
n.indices[pos:pos+1] + // The index char we move
|
|
245
|
+
n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return newPos
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// addRoute adds a node with the given handle to the path.
|
|
252
|
+
// Not concurrency-safe!
|
|
253
|
+
func (n *node) addRoute(path string, handle interface{}) {
|
|
254
|
+
fullPath := path
|
|
255
|
+
n.priority++
|
|
256
|
+
|
|
257
|
+
// Empty tree
|
|
258
|
+
if n.path == "" && n.indices == "" {
|
|
259
|
+
n.insertChild(path, fullPath, handle)
|
|
260
|
+
n.nType = root
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
walk:
|
|
265
|
+
for {
|
|
266
|
+
// Find the longest common prefix.
|
|
267
|
+
// This also implies that the common prefix contains no ':' or '*'
|
|
268
|
+
// since the existing key can't contain those chars.
|
|
269
|
+
i := longestCommonPrefix(path, n.path)
|
|
270
|
+
|
|
271
|
+
// Split edge
|
|
272
|
+
if i < len(n.path) {
|
|
273
|
+
child := node{
|
|
274
|
+
path: n.path[i:],
|
|
275
|
+
wildChild: n.wildChild,
|
|
276
|
+
nType: static,
|
|
277
|
+
indices: n.indices,
|
|
278
|
+
children: n.children,
|
|
279
|
+
handle: n.handle,
|
|
280
|
+
priority: n.priority - 1,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
n.children = []*node{&child}
|
|
284
|
+
// []byte for proper unicode char conversion, see #65
|
|
285
|
+
n.indices = string([]byte{n.path[i]})
|
|
286
|
+
n.path = path[:i]
|
|
287
|
+
n.handle = nil
|
|
288
|
+
n.wildChild = false
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Make new node a child of this node
|
|
292
|
+
if i < len(path) {
|
|
293
|
+
path = path[i:]
|
|
294
|
+
|
|
295
|
+
if n.wildChild {
|
|
296
|
+
n = n.children[0]
|
|
297
|
+
n.priority++
|
|
298
|
+
|
|
299
|
+
// Check if the wildcard matches
|
|
300
|
+
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
|
|
301
|
+
// Adding a child to a catchAll is not possible
|
|
302
|
+
n.nType != catchAll &&
|
|
303
|
+
// Check for longer wildcard, e.g. :name and :names
|
|
304
|
+
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
|
|
305
|
+
continue walk
|
|
306
|
+
} else {
|
|
307
|
+
// Wildcard conflict
|
|
308
|
+
pathSeg := path
|
|
309
|
+
if n.nType != catchAll {
|
|
310
|
+
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
|
|
311
|
+
}
|
|
312
|
+
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
|
|
313
|
+
panic("'" + pathSeg +
|
|
314
|
+
"' in new path '" + fullPath +
|
|
315
|
+
"' conflicts with existing wildcard '" + n.path +
|
|
316
|
+
"' in existing prefix '" + prefix +
|
|
317
|
+
"'")
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
idxc := path[0]
|
|
322
|
+
|
|
323
|
+
// '/' after param
|
|
324
|
+
if n.nType == param && idxc == '/' && len(n.children) == 1 {
|
|
325
|
+
n = n.children[0]
|
|
326
|
+
n.priority++
|
|
327
|
+
continue walk
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check if a child with the next path byte exists
|
|
331
|
+
for i, c := range []byte(n.indices) {
|
|
332
|
+
if c == idxc {
|
|
333
|
+
i = n.incrementChildPrio(i)
|
|
334
|
+
n = n.children[i]
|
|
335
|
+
continue walk
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Otherwise insert it
|
|
340
|
+
if idxc != ':' && idxc != '*' {
|
|
341
|
+
// []byte for proper unicode char conversion, see #65
|
|
342
|
+
n.indices += string([]byte{idxc})
|
|
343
|
+
child := &node{}
|
|
344
|
+
n.children = append(n.children, child)
|
|
345
|
+
n.incrementChildPrio(len(n.indices) - 1)
|
|
346
|
+
n = child
|
|
347
|
+
}
|
|
348
|
+
n.insertChild(path, fullPath, handle)
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Otherwise add handle to current node
|
|
353
|
+
if n.handle != nil {
|
|
354
|
+
panic("a handle is already registered for path '" + fullPath + "'")
|
|
355
|
+
}
|
|
356
|
+
n.handle = handle
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
func (n *node) insertChild(path, fullPath string, handle interface{}) {
|
|
362
|
+
for {
|
|
363
|
+
// Find prefix until first wildcard
|
|
364
|
+
wildcard, i, valid := findWildcard(path)
|
|
365
|
+
if i < 0 { // No wilcard found
|
|
366
|
+
break
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// The wildcard name must not contain ':' and '*'
|
|
370
|
+
if !valid {
|
|
371
|
+
panic("only one wildcard per path segment is allowed, has: '" +
|
|
372
|
+
wildcard + "' in path '" + fullPath + "'")
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check if the wildcard has a name
|
|
376
|
+
if len(wildcard) < 2 {
|
|
377
|
+
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check if this node has existing children which would be
|
|
381
|
+
// unreachable if we insert the wildcard here
|
|
382
|
+
if len(n.children) > 0 {
|
|
383
|
+
panic("wildcard segment '" + wildcard +
|
|
384
|
+
"' conflicts with existing children in path '" + fullPath + "'")
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// param
|
|
388
|
+
if wildcard[0] == ':' {
|
|
389
|
+
if i > 0 {
|
|
390
|
+
// Insert prefix before the current wildcard
|
|
391
|
+
n.path = path[:i]
|
|
392
|
+
path = path[i:]
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
n.wildChild = true
|
|
396
|
+
child := &node{
|
|
397
|
+
nType: param,
|
|
398
|
+
path: wildcard,
|
|
399
|
+
}
|
|
400
|
+
n.children = []*node{child}
|
|
401
|
+
n = child
|
|
402
|
+
n.priority++
|
|
403
|
+
|
|
404
|
+
// If the path doesn't end with the wildcard, then there
|
|
405
|
+
// will be another non-wildcard subpath starting with '/'
|
|
406
|
+
if len(wildcard) < len(path) {
|
|
407
|
+
path = path[len(wildcard):]
|
|
408
|
+
child := &node{
|
|
409
|
+
priority: 1,
|
|
410
|
+
}
|
|
411
|
+
n.children = []*node{child}
|
|
412
|
+
n = child
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Otherwise we're done. Insert the handle in the new leaf
|
|
417
|
+
n.handle = handle
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// catchAll
|
|
422
|
+
if i+len(wildcard) != len(path) {
|
|
423
|
+
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
|
427
|
+
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Currently fixed width 1 for '/'
|
|
431
|
+
i--
|
|
432
|
+
if path[i] != '/' {
|
|
433
|
+
panic("no / before catch-all in path '" + fullPath + "'")
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
n.path = path[:i]
|
|
437
|
+
|
|
438
|
+
// First node: catchAll node with empty path
|
|
439
|
+
child := &node{
|
|
440
|
+
wildChild: true,
|
|
441
|
+
nType: catchAll,
|
|
442
|
+
}
|
|
443
|
+
n.children = []*node{child}
|
|
444
|
+
n.indices = string('/')
|
|
445
|
+
n = child
|
|
446
|
+
n.priority++
|
|
447
|
+
|
|
448
|
+
// Second node: node holding the variable
|
|
449
|
+
child = &node{
|
|
450
|
+
path: path[i:],
|
|
451
|
+
nType: catchAll,
|
|
452
|
+
handle: handle,
|
|
453
|
+
priority: 1,
|
|
454
|
+
}
|
|
455
|
+
n.children = []*node{child}
|
|
456
|
+
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// If no wildcard was found, simply insert the path and handle
|
|
461
|
+
n.path = path
|
|
462
|
+
n.handle = handle
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Returns the handle registered with the given path (key). The values of
|
|
466
|
+
// wildcards are saved to a map.
|
|
467
|
+
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
|
|
468
|
+
// made if a handle exists with an extra (without the) trailing slash for the
|
|
469
|
+
// given path.
|
|
470
|
+
func (n *node) getValue(path string, params func() *Params) (handle interface{}, ps *Params, tsr bool) {
|
|
471
|
+
walk: // Outer loop for walking the tree
|
|
472
|
+
for {
|
|
473
|
+
prefix := n.path
|
|
474
|
+
if len(path) > len(prefix) {
|
|
475
|
+
if path[:len(prefix)] == prefix {
|
|
476
|
+
path = path[len(prefix):]
|
|
477
|
+
|
|
478
|
+
// If this node does not have a wildcard (param or catchAll)
|
|
479
|
+
// child, we can just look up the next child node and continue
|
|
480
|
+
// to walk down the tree
|
|
481
|
+
if !n.wildChild {
|
|
482
|
+
idxc := path[0]
|
|
483
|
+
for i, c := range []byte(n.indices) {
|
|
484
|
+
if c == idxc {
|
|
485
|
+
n = n.children[i]
|
|
486
|
+
continue walk
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Nothing found.
|
|
491
|
+
// We can recommend to redirect to the same URL without a
|
|
492
|
+
// trailing slash if a leaf exists for that path.
|
|
493
|
+
tsr = (path == "/" && n.handle != nil)
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Handle wildcard child
|
|
498
|
+
n = n.children[0]
|
|
499
|
+
switch n.nType {
|
|
500
|
+
case param:
|
|
501
|
+
// Find param end (either '/' or path end)
|
|
502
|
+
end := 0
|
|
503
|
+
for end < len(path) && path[end] != '/' {
|
|
504
|
+
end++
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Save param value
|
|
508
|
+
if params != nil {
|
|
509
|
+
if ps == nil {
|
|
510
|
+
ps = params()
|
|
511
|
+
}
|
|
512
|
+
// Expand slice within preallocated capacity
|
|
513
|
+
i := len(*ps)
|
|
514
|
+
*ps = (*ps)[:i+1]
|
|
515
|
+
(*ps)[i] = Param{
|
|
516
|
+
Key: n.path[1:],
|
|
517
|
+
Value: path[:end],
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// We need to go deeper!
|
|
522
|
+
if end < len(path) {
|
|
523
|
+
if len(n.children) > 0 {
|
|
524
|
+
path = path[end:]
|
|
525
|
+
n = n.children[0]
|
|
526
|
+
continue walk
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ... but we can't
|
|
530
|
+
tsr = (len(path) == end+1)
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if handle = n.handle; handle != nil {
|
|
535
|
+
return
|
|
536
|
+
} else if len(n.children) == 1 {
|
|
537
|
+
// No handle found. Check if a handle for this path + a
|
|
538
|
+
// trailing slash exists for TSR recommendation
|
|
539
|
+
n = n.children[0]
|
|
540
|
+
tsr = (n.path == "/" && n.handle != nil) || (n.path == "" && n.indices == "/")
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
case catchAll:
|
|
546
|
+
// Save param value
|
|
547
|
+
if params != nil {
|
|
548
|
+
if ps == nil {
|
|
549
|
+
ps = params()
|
|
550
|
+
}
|
|
551
|
+
// Expand slice within preallocated capacity
|
|
552
|
+
i := len(*ps)
|
|
553
|
+
*ps = (*ps)[:i+1]
|
|
554
|
+
(*ps)[i] = Param{
|
|
555
|
+
Key: n.path[2:],
|
|
556
|
+
Value: path,
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
handle = n.handle
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
default:
|
|
564
|
+
panic("invalid node type")
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
} else if path == prefix {
|
|
568
|
+
// We should have reached the node containing the handle.
|
|
569
|
+
// Check if this node has a handle registered.
|
|
570
|
+
if handle = n.handle; handle != nil {
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// If there is no handle for this route, but this route has a
|
|
575
|
+
// wildcard child, there must be a handle for this path with an
|
|
576
|
+
// additional trailing slash
|
|
577
|
+
if path == "/" && n.wildChild && n.nType != root {
|
|
578
|
+
tsr = true
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// No handle found. Check if a handle for this path + a
|
|
583
|
+
// trailing slash exists for trailing slash recommendation
|
|
584
|
+
for i, c := range []byte(n.indices) {
|
|
585
|
+
if c == '/' {
|
|
586
|
+
n = n.children[i]
|
|
587
|
+
tsr = (len(n.path) == 1 && n.handle != nil) ||
|
|
588
|
+
(n.nType == catchAll && n.children[0].handle != nil)
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Nothing found. We can recommend to redirect to the same URL with an
|
|
596
|
+
// extra trailing slash if a leaf exists for that path
|
|
597
|
+
tsr = (path == "/") ||
|
|
598
|
+
(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
|
|
599
|
+
path == prefix[:len(prefix)-1] && n.handle != nil)
|
|
600
|
+
return
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Makes a case-insensitive lookup of the given path and tries to find a handler.
|
|
605
|
+
// It can optionally also fix trailing slashes.
|
|
606
|
+
// It returns the case-corrected path and a bool indicating whether the lookup
|
|
607
|
+
// was successful.
|
|
608
|
+
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (fixedPath string, found bool) {
|
|
609
|
+
const stackBufSize = 128
|
|
610
|
+
|
|
611
|
+
// Use a static sized buffer on the stack in the common case.
|
|
612
|
+
// If the path is too long, allocate a buffer on the heap instead.
|
|
613
|
+
buf := make([]byte, 0, stackBufSize)
|
|
614
|
+
if l := len(path) + 1; l > stackBufSize {
|
|
615
|
+
buf = make([]byte, 0, l)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
ciPath := n.findCaseInsensitivePathRec(
|
|
619
|
+
path,
|
|
620
|
+
buf, // Preallocate enough memory for new path
|
|
621
|
+
[4]byte{}, // Empty rune buffer
|
|
622
|
+
fixTrailingSlash,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
return string(ciPath), ciPath != nil
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Shift bytes in array by n bytes left
|
|
629
|
+
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
|
|
630
|
+
switch n {
|
|
631
|
+
case 0:
|
|
632
|
+
return rb
|
|
633
|
+
case 1:
|
|
634
|
+
return [4]byte{rb[1], rb[2], rb[3], 0}
|
|
635
|
+
case 2:
|
|
636
|
+
return [4]byte{rb[2], rb[3]}
|
|
637
|
+
case 3:
|
|
638
|
+
return [4]byte{rb[3]}
|
|
639
|
+
default:
|
|
640
|
+
return [4]byte{}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Recursive case-insensitive lookup function used by n.findCaseInsensitivePath
|
|
645
|
+
func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte {
|
|
646
|
+
npLen := len(n.path)
|
|
647
|
+
|
|
648
|
+
walk: // Outer loop for walking the tree
|
|
649
|
+
for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) {
|
|
650
|
+
// Add common prefix to result
|
|
651
|
+
oldPath := path
|
|
652
|
+
path = path[npLen:]
|
|
653
|
+
ciPath = append(ciPath, n.path...)
|
|
654
|
+
|
|
655
|
+
if len(path) > 0 {
|
|
656
|
+
// If this node does not have a wildcard (param or catchAll) child,
|
|
657
|
+
// we can just look up the next child node and continue to walk down
|
|
658
|
+
// the tree
|
|
659
|
+
if !n.wildChild {
|
|
660
|
+
// Skip rune bytes already processed
|
|
661
|
+
rb = shiftNRuneBytes(rb, npLen)
|
|
662
|
+
|
|
663
|
+
if rb[0] != 0 {
|
|
664
|
+
// Old rune not finished
|
|
665
|
+
idxc := rb[0]
|
|
666
|
+
for i, c := range []byte(n.indices) {
|
|
667
|
+
if c == idxc {
|
|
668
|
+
// continue with child node
|
|
669
|
+
n = n.children[i]
|
|
670
|
+
npLen = len(n.path)
|
|
671
|
+
continue walk
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
} else {
|
|
675
|
+
// Process a new rune
|
|
676
|
+
var rv rune
|
|
677
|
+
|
|
678
|
+
// Find rune start.
|
|
679
|
+
// Runes are up to 4 byte long,
|
|
680
|
+
// -4 would definitely be another rune.
|
|
681
|
+
var off int
|
|
682
|
+
for max := min(npLen, 3); off < max; off++ {
|
|
683
|
+
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
|
|
684
|
+
// read rune from cached path
|
|
685
|
+
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
|
|
686
|
+
break
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Calculate lowercase bytes of current rune
|
|
691
|
+
lo := unicode.ToLower(rv)
|
|
692
|
+
utf8.EncodeRune(rb[:], lo)
|
|
693
|
+
|
|
694
|
+
// Skip already processed bytes
|
|
695
|
+
rb = shiftNRuneBytes(rb, off)
|
|
696
|
+
|
|
697
|
+
idxc := rb[0]
|
|
698
|
+
for i, c := range []byte(n.indices) {
|
|
699
|
+
// Lowercase matches
|
|
700
|
+
if c == idxc {
|
|
701
|
+
// must use a recursive approach since both the
|
|
702
|
+
// uppercase byte and the lowercase byte might exist
|
|
703
|
+
// as an index
|
|
704
|
+
if out := n.children[i].findCaseInsensitivePathRec(
|
|
705
|
+
path, ciPath, rb, fixTrailingSlash,
|
|
706
|
+
); out != nil {
|
|
707
|
+
return out
|
|
708
|
+
}
|
|
709
|
+
break
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// If we found no match, the same for the uppercase rune,
|
|
714
|
+
// if it differs
|
|
715
|
+
if up := unicode.ToUpper(rv); up != lo {
|
|
716
|
+
utf8.EncodeRune(rb[:], up)
|
|
717
|
+
rb = shiftNRuneBytes(rb, off)
|
|
718
|
+
|
|
719
|
+
idxc := rb[0]
|
|
720
|
+
for i, c := range []byte(n.indices) {
|
|
721
|
+
// Uppercase matches
|
|
722
|
+
if c == idxc {
|
|
723
|
+
// Continue with child node
|
|
724
|
+
n = n.children[i]
|
|
725
|
+
npLen = len(n.path)
|
|
726
|
+
continue walk
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Nothing found. We can recommend to redirect to the same URL
|
|
733
|
+
// without a trailing slash if a leaf exists for that path
|
|
734
|
+
if fixTrailingSlash && path == "/" && n.handle != nil {
|
|
735
|
+
return ciPath
|
|
736
|
+
}
|
|
737
|
+
return nil
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
n = n.children[0]
|
|
741
|
+
switch n.nType {
|
|
742
|
+
case param:
|
|
743
|
+
// Find param end (either '/' or path end)
|
|
744
|
+
end := 0
|
|
745
|
+
for end < len(path) && path[end] != '/' {
|
|
746
|
+
end++
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Add param value to case insensitive path
|
|
750
|
+
ciPath = append(ciPath, path[:end]...)
|
|
751
|
+
|
|
752
|
+
// We need to go deeper!
|
|
753
|
+
if end < len(path) {
|
|
754
|
+
if len(n.children) > 0 {
|
|
755
|
+
// Continue with child node
|
|
756
|
+
n = n.children[0]
|
|
757
|
+
npLen = len(n.path)
|
|
758
|
+
path = path[end:]
|
|
759
|
+
continue
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ... but we can't
|
|
763
|
+
if fixTrailingSlash && len(path) == end+1 {
|
|
764
|
+
return ciPath
|
|
765
|
+
}
|
|
766
|
+
return nil
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if n.handle != nil {
|
|
770
|
+
return ciPath
|
|
771
|
+
} else if fixTrailingSlash && len(n.children) == 1 {
|
|
772
|
+
// No handle found. Check if a handle for this path + a
|
|
773
|
+
// trailing slash exists
|
|
774
|
+
n = n.children[0]
|
|
775
|
+
if n.path == "/" && n.handle != nil {
|
|
776
|
+
return append(ciPath, '/')
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return nil
|
|
780
|
+
|
|
781
|
+
case catchAll:
|
|
782
|
+
return append(ciPath, path...)
|
|
783
|
+
|
|
784
|
+
default:
|
|
785
|
+
panic("invalid node type")
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
788
|
+
// We should have reached the node containing the handle.
|
|
789
|
+
// Check if this node has a handle registered.
|
|
790
|
+
if n.handle != nil {
|
|
791
|
+
return ciPath
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// No handle found.
|
|
795
|
+
// Try to fix the path by adding a trailing slash
|
|
796
|
+
if fixTrailingSlash {
|
|
797
|
+
for i, c := range []byte(n.indices) {
|
|
798
|
+
if c == '/' {
|
|
799
|
+
n = n.children[i]
|
|
800
|
+
if (len(n.path) == 1 && n.handle != nil) ||
|
|
801
|
+
(n.nType == catchAll && n.children[0].handle != nil) {
|
|
802
|
+
return append(ciPath, '/')
|
|
803
|
+
}
|
|
804
|
+
return nil
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return nil
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Nothing found.
|
|
813
|
+
// Try to fix the path by adding / removing a trailing slash
|
|
814
|
+
if fixTrailingSlash {
|
|
815
|
+
if path == "/" {
|
|
816
|
+
return ciPath
|
|
817
|
+
}
|
|
818
|
+
if len(path)+1 == npLen && n.path[len(path)] == '/' &&
|
|
819
|
+
strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handle != nil {
|
|
820
|
+
return append(ciPath, n.path...)
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return nil
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Param is a single URL parameter, consisting of a key and a value.
|
|
827
|
+
type Param struct {
|
|
828
|
+
Key string
|
|
829
|
+
Value string
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Params is a Param-slice, as returned by the router.
|
|
833
|
+
// The slice is ordered, the first URL parameter is also the first slice value.
|
|
834
|
+
// It is therefore safe to read values by the index.
|
|
835
|
+
type Params []Param
|
|
836
|
+
|
|
837
|
+
// ByName returns the value of the first Param which key matches the given name.
|
|
838
|
+
// If no matching Param is found, an empty string is returned.
|
|
839
|
+
func (ps Params) ByName(name string) string {
|
|
840
|
+
for _, p := range ps {
|
|
841
|
+
if p.Key == name {
|
|
842
|
+
return p.Value
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return ""
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
type paramsKey struct{}
|
|
849
|
+
|
|
850
|
+
// ParamsKey is the request context key under which URL params are stored.
|
|
851
|
+
var ParamsKey = paramsKey{}
|
|
852
|
+
|
|
853
|
+
// ParamsFromContext pulls the URL parameters from a request context,
|
|
854
|
+
// or returns nil if none are present.
|
|
855
|
+
func ParamsFromContext(ctx context.Context) Params {
|
|
856
|
+
p, _ := ctx.Value(ParamsKey).(Params)
|
|
857
|
+
return p
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// MatchedRoutePathParam is the Param name under which the path of the matched
|
|
861
|
+
// route is stored, if Router.SaveMatchedRoutePath is set.
|
|
862
|
+
var MatchedRoutePathParam = "$matchedRoutePath"
|
|
863
|
+
|
|
864
|
+
// MatchedRoutePath retrieves the path of the matched route.
|
|
865
|
+
// Router.SaveMatchedRoutePath must have been enabled when the respective
|
|
866
|
+
// handler was added, otherwise this function always returns an empty string.
|
|
867
|
+
func (ps Params) MatchedRoutePath() string {
|
|
868
|
+
return ps.ByName(MatchedRoutePathParam)
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Router is a http.Handler which can be used to dispatch requests to different
|
|
872
|
+
// handler functions via configurable routes
|
|
873
|
+
type Router struct {
|
|
874
|
+
trees map[string]*node
|
|
875
|
+
|
|
876
|
+
paramsPool sync.Pool
|
|
877
|
+
maxParams uint16
|
|
878
|
+
|
|
879
|
+
// If enabled, adds the matched route path onto the http.Request context
|
|
880
|
+
// before invoking the handler.
|
|
881
|
+
// The matched route path is only added to handlers of routes that were
|
|
882
|
+
// registered when this option was enabled.
|
|
883
|
+
SaveMatchedRoutePath bool
|
|
884
|
+
|
|
885
|
+
// Enables automatic redirection if the current route can't be matched but a
|
|
886
|
+
// handler for the path with (without) the trailing slash exists.
|
|
887
|
+
// For example if /foo/ is requested but a route only exists for /foo, the
|
|
888
|
+
// client is redirected to /foo with http status code 301 for GET requests
|
|
889
|
+
// and 308 for all other request methods.
|
|
890
|
+
RedirectTrailingSlash bool
|
|
891
|
+
|
|
892
|
+
// If enabled, the router tries to fix the current request path, if no
|
|
893
|
+
// handle is registered for it.
|
|
894
|
+
// First superfluous path elements like ../ or // are removed.
|
|
895
|
+
// Afterwards the router does a case-insensitive lookup of the cleaned path.
|
|
896
|
+
// If a handle can be found for this route, the router makes a redirection
|
|
897
|
+
// to the corrected path with status code 301 for GET requests and 308 for
|
|
898
|
+
// all other request methods.
|
|
899
|
+
// For example /FOO and /..//Foo could be redirected to /foo.
|
|
900
|
+
// RedirectTrailingSlash is independent of this option.
|
|
901
|
+
RedirectFixedPath bool
|
|
902
|
+
|
|
903
|
+
// If enabled, the router checks if another method is allowed for the
|
|
904
|
+
// current route, if the current request can not be routed.
|
|
905
|
+
// If this is the case, the request is answered with 'Method Not Allowed'
|
|
906
|
+
// and HTTP status code 405.
|
|
907
|
+
// If no other Method is allowed, the request is delegated to the NotFound
|
|
908
|
+
// handler.
|
|
909
|
+
HandleMethodNotAllowed bool
|
|
910
|
+
|
|
911
|
+
// If enabled, the router automatically replies to OPTIONS requests.
|
|
912
|
+
// Custom OPTIONS handlers take priority over automatic replies.
|
|
913
|
+
HandleOPTIONS bool
|
|
914
|
+
|
|
915
|
+
// An optional http.Handler that is called on automatic OPTIONS requests.
|
|
916
|
+
// The handler is only called if HandleOPTIONS is true and no OPTIONS
|
|
917
|
+
// handler for the specific path was set.
|
|
918
|
+
// The "Allowed" header is set before calling the handler.
|
|
919
|
+
GlobalOPTIONS http.Handler
|
|
920
|
+
|
|
921
|
+
// Cached value of global (*) allowed methods
|
|
922
|
+
globalAllowed string
|
|
923
|
+
|
|
924
|
+
// Configurable http.Handler which is called when no matching route is
|
|
925
|
+
// found. If it is not set, http.NotFound is used.
|
|
926
|
+
NotFound RenderFunc
|
|
927
|
+
|
|
928
|
+
// Configurable http.Handler which is called when a request
|
|
929
|
+
// cannot be routed and HandleMethodNotAllowed is true.
|
|
930
|
+
// If it is not set, http.Error with http.StatusMethodNotAllowed is used.
|
|
931
|
+
// The "Allow" header with allowed request methods is set before the handler
|
|
932
|
+
// is called.
|
|
933
|
+
MethodNotAllowed http.Handler
|
|
934
|
+
|
|
935
|
+
// Function to handle panics recovered from http handlers.
|
|
936
|
+
// It should be used to generate a error page and return the http error code
|
|
937
|
+
// 500 (Internal Server Error).
|
|
938
|
+
// The handler can be used to keep your server from crashing because of
|
|
939
|
+
// unrecovered panics.
|
|
940
|
+
PanicHandler func(http.ResponseWriter, *http.Request, interface{})
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// func(http.ResponseWriter, *http.Request, Params)
|
|
944
|
+
// Make sure the Router conforms with the http.Handler interface
|
|
945
|
+
// var _ http.Handler = New()
|
|
946
|
+
|
|
947
|
+
var router = &Router{
|
|
948
|
+
RedirectTrailingSlash: true,
|
|
949
|
+
RedirectFixedPath: true,
|
|
950
|
+
HandleMethodNotAllowed: true,
|
|
951
|
+
HandleOPTIONS: true,
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
func (r *Router) getParams() *Params {
|
|
955
|
+
ps, _ := r.paramsPool.Get().(*Params)
|
|
956
|
+
*ps = (*ps)[0:0] // reset slice
|
|
957
|
+
return ps
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
func (r *Router) putParams(ps *Params) {
|
|
961
|
+
if ps != nil {
|
|
962
|
+
r.paramsPool.Put(ps)
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// func (r *Router) saveMatchedRoutePath(path string, handle interface{}) interface{} {
|
|
967
|
+
// return func(w http.ResponseWriter, req *http.Request, ps Params) {
|
|
968
|
+
// if ps == nil {
|
|
969
|
+
// psp := r.getParams()
|
|
970
|
+
// ps = (*psp)[0:1]
|
|
971
|
+
// ps[0] = Param{Key: MatchedRoutePathParam, Value: path}
|
|
972
|
+
// println("route: " + r.URL.Path)
|
|
973
|
+
// page := createPage(info, render(NewRenderContext()))
|
|
974
|
+
// w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
|
|
975
|
+
// w.Header().Set("Content-Type", "text/html")
|
|
976
|
+
// w.WriteHeader(http.StatusOK)
|
|
977
|
+
// w.Write(page.Bytes())
|
|
978
|
+
// handle(w, req, ps)
|
|
979
|
+
// r.putParams(psp)
|
|
980
|
+
// } else {
|
|
981
|
+
// ps = append(ps, Param{Key: MatchedRoutePathParam, Value: path})
|
|
982
|
+
// handle(w, req, ps)
|
|
983
|
+
// }
|
|
984
|
+
// }
|
|
985
|
+
// }
|
|
986
|
+
|
|
987
|
+
// GET is a shortcut for router.Handle(http.MethodGet, path, handle)
|
|
988
|
+
func (r *Router) GET(path string, handle interface{}) {
|
|
989
|
+
r.Handle(http.MethodGet, path, handle)
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// HEAD is a shortcut for router.Handle(http.MethodHead, path, handle)
|
|
993
|
+
func (r *Router) HEAD(path string, handle interface{}) {
|
|
994
|
+
r.Handle(http.MethodHead, path, handle)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// OPTIONS is a shortcut for router.Handle(http.MethodOptions, path, handle)
|
|
998
|
+
func (r *Router) OPTIONS(path string, handle interface{}) {
|
|
999
|
+
r.Handle(http.MethodOptions, path, handle)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// POST is a shortcut for router.Handle(http.MethodPost, path, handle)
|
|
1003
|
+
func (r *Router) POST(path string, handle interface{}) {
|
|
1004
|
+
r.Handle(http.MethodPost, path, handle)
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// PUT is a shortcut for router.Handle(http.MethodPut, path, handle)
|
|
1008
|
+
func (r *Router) PUT(path string, handle interface{}) {
|
|
1009
|
+
r.Handle(http.MethodPut, path, handle)
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// PATCH is a shortcut for router.Handle(http.MethodPatch, path, handle)
|
|
1013
|
+
func (r *Router) PATCH(path string, handle interface{}) {
|
|
1014
|
+
r.Handle(http.MethodPatch, path, handle)
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle)
|
|
1018
|
+
func (r *Router) DELETE(path string, handle interface{}) {
|
|
1019
|
+
r.Handle(http.MethodDelete, path, handle)
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Handle registers a new request handle with the given path and method.
|
|
1023
|
+
//
|
|
1024
|
+
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
|
|
1025
|
+
// functions can be used.
|
|
1026
|
+
//
|
|
1027
|
+
// This function is intended for bulk loading and to allow the usage of less
|
|
1028
|
+
// frequently used, non-standardized or custom methods (e.g. for internal
|
|
1029
|
+
// communication with a proxy).
|
|
1030
|
+
func (r *Router) Handle(method, path string, handle interface{}) {
|
|
1031
|
+
varsCount := uint16(0)
|
|
1032
|
+
|
|
1033
|
+
if method == "" {
|
|
1034
|
+
panic("method must not be empty")
|
|
1035
|
+
}
|
|
1036
|
+
if len(path) < 1 || path[0] != '/' {
|
|
1037
|
+
panic("path must begin with '/' in path '" + path + "'")
|
|
1038
|
+
}
|
|
1039
|
+
if handle == nil {
|
|
1040
|
+
panic("handle must not be nil")
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// if r.SaveMatchedRoutePath {
|
|
1044
|
+
// varsCount++
|
|
1045
|
+
// handle = r.saveMatchedRoutePath(path, handle)
|
|
1046
|
+
// }
|
|
1047
|
+
|
|
1048
|
+
if r.trees == nil {
|
|
1049
|
+
r.trees = make(map[string]*node)
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
root := r.trees[method]
|
|
1053
|
+
if root == nil {
|
|
1054
|
+
root = new(node)
|
|
1055
|
+
r.trees[method] = root
|
|
1056
|
+
|
|
1057
|
+
r.globalAllowed = r.allowed("*", "")
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
root.addRoute(path, handle)
|
|
1061
|
+
|
|
1062
|
+
// Update maxParams
|
|
1063
|
+
if paramsCount := countParams(path); paramsCount+varsCount > r.maxParams {
|
|
1064
|
+
r.maxParams = paramsCount + varsCount
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Lazy-init paramsPool alloc func
|
|
1068
|
+
if r.paramsPool.New == nil && r.maxParams > 0 {
|
|
1069
|
+
r.paramsPool.New = func() interface{} {
|
|
1070
|
+
ps := make(Params, 0, r.maxParams)
|
|
1071
|
+
return &ps
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Handler is an adapter which allows the usage of an http.Handler as a
|
|
1077
|
+
// request handle.
|
|
1078
|
+
// The Params are available in the request context under ParamsKey.
|
|
1079
|
+
// func (r *Router) Handler(method, path string, handler http.Handler) {
|
|
1080
|
+
// r.Handle(method, path,
|
|
1081
|
+
// func(w http.ResponseWriter, req *http.Request, p Params) {
|
|
1082
|
+
// if len(p) > 0 {
|
|
1083
|
+
// ctx := req.Context()
|
|
1084
|
+
// ctx = context.WithValue(ctx, ParamsKey, p)
|
|
1085
|
+
// req = req.WithContext(ctx)
|
|
1086
|
+
// }
|
|
1087
|
+
// handler.ServeHTTP(w, req)
|
|
1088
|
+
// },
|
|
1089
|
+
// )
|
|
1090
|
+
// }
|
|
1091
|
+
|
|
1092
|
+
// HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a
|
|
1093
|
+
// request handle.
|
|
1094
|
+
// func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) {
|
|
1095
|
+
// r.Handler(method, path, handler)
|
|
1096
|
+
// }
|
|
1097
|
+
|
|
1098
|
+
func (r *Router) recv(w http.ResponseWriter, req *http.Request) {
|
|
1099
|
+
if rcv := recover(); rcv != nil {
|
|
1100
|
+
r.PanicHandler(w, req, rcv)
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Lookup allows the manual lookup of a method + path combo.
|
|
1105
|
+
// This is e.g. useful to build a framework around this router.
|
|
1106
|
+
// If the path was found, it returns the handle function and the path parameter
|
|
1107
|
+
// values. Otherwise the third return value indicates whether a redirection to
|
|
1108
|
+
// the same path with an extra / without the trailing slash should be performed.
|
|
1109
|
+
func (r *Router) Lookup(method, path string) (interface{}, Params, bool) {
|
|
1110
|
+
if root := r.trees[method]; root != nil {
|
|
1111
|
+
handle, ps, tsr := root.getValue(path, r.getParams)
|
|
1112
|
+
if handle == nil {
|
|
1113
|
+
r.putParams(ps)
|
|
1114
|
+
return nil, nil, tsr
|
|
1115
|
+
}
|
|
1116
|
+
if ps == nil {
|
|
1117
|
+
return handle, nil, tsr
|
|
1118
|
+
}
|
|
1119
|
+
return handle, *ps, tsr
|
|
1120
|
+
}
|
|
1121
|
+
return nil, nil, false
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
func (r *Router) allowed(path, reqMethod string) (allow string) {
|
|
1125
|
+
allowed := make([]string, 0, 9)
|
|
1126
|
+
|
|
1127
|
+
if path == "*" { // server-wide
|
|
1128
|
+
// empty method is used for internal calls to refresh the cache
|
|
1129
|
+
if reqMethod == "" {
|
|
1130
|
+
for method := range r.trees {
|
|
1131
|
+
if method == http.MethodOptions {
|
|
1132
|
+
continue
|
|
1133
|
+
}
|
|
1134
|
+
// Add request method to list of allowed methods
|
|
1135
|
+
allowed = append(allowed, method)
|
|
1136
|
+
}
|
|
1137
|
+
} else {
|
|
1138
|
+
return r.globalAllowed
|
|
1139
|
+
}
|
|
1140
|
+
} else { // specific path
|
|
1141
|
+
for method := range r.trees {
|
|
1142
|
+
// Skip the requested method - we already tried this one
|
|
1143
|
+
if method == reqMethod || method == http.MethodOptions {
|
|
1144
|
+
continue
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
handle, _, _ := r.trees[method].getValue(path, nil)
|
|
1148
|
+
if handle != nil {
|
|
1149
|
+
// Add request method to list of allowed methods
|
|
1150
|
+
allowed = append(allowed, method)
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if len(allowed) > 0 {
|
|
1156
|
+
// Add request method to list of allowed methods
|
|
1157
|
+
allowed = append(allowed, http.MethodOptions)
|
|
1158
|
+
|
|
1159
|
+
// Sort allowed methods.
|
|
1160
|
+
// sort.Strings(allowed) unfortunately causes unnecessary allocations
|
|
1161
|
+
// due to allowed being moved to the heap and interface conversion
|
|
1162
|
+
for i, l := 1, len(allowed); i < l; i++ {
|
|
1163
|
+
for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- {
|
|
1164
|
+
allowed[j], allowed[j-1] = allowed[j-1], allowed[j]
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// return as comma separated list
|
|
1169
|
+
return strings.Join(allowed, ", ")
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
return allow
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// ServeHTTP makes the router implement the http.Handler interface.
|
|
1176
|
+
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
1177
|
+
if r.PanicHandler != nil {
|
|
1178
|
+
defer r.recv(w, req)
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
path := req.URL.Path
|
|
1182
|
+
|
|
1183
|
+
if root := r.trees[req.Method]; root != nil {
|
|
1184
|
+
// TODO: use _ ps save it to context for useParam()
|
|
1185
|
+
if handle, _, tsr := root.getValue(path, r.getParams); handle != nil {
|
|
1186
|
+
println("route: " + req.URL.Path)
|
|
1187
|
+
if render, ok := handle.(RenderFunc); ok {
|
|
1188
|
+
page := createPage(RouteInfo{}, render(NewRenderContext()))
|
|
1189
|
+
w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
|
|
1190
|
+
w.Header().Set("Content-Type", "text/html")
|
|
1191
|
+
w.WriteHeader(http.StatusOK)
|
|
1192
|
+
w.Write(page.Bytes())
|
|
1193
|
+
return
|
|
1194
|
+
} else {
|
|
1195
|
+
handle.(func(w http.ResponseWriter, r *http.Request))(w, req)
|
|
1196
|
+
return
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
} else if req.Method != http.MethodConnect && path != "/" {
|
|
1200
|
+
// Moved Permanently, request with GET method
|
|
1201
|
+
code := http.StatusMovedPermanently
|
|
1202
|
+
if req.Method != http.MethodGet {
|
|
1203
|
+
// Permanent Redirect, request with same method
|
|
1204
|
+
code = http.StatusPermanentRedirect
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if tsr && r.RedirectTrailingSlash {
|
|
1208
|
+
if len(path) > 1 && path[len(path)-1] == '/' {
|
|
1209
|
+
req.URL.Path = path[:len(path)-1]
|
|
1210
|
+
} else {
|
|
1211
|
+
req.URL.Path = path + "/"
|
|
1212
|
+
}
|
|
1213
|
+
http.Redirect(w, req, req.URL.String(), code)
|
|
1214
|
+
return
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Try to fix the request path
|
|
1218
|
+
if r.RedirectFixedPath {
|
|
1219
|
+
fixedPath, found := root.findCaseInsensitivePath(
|
|
1220
|
+
CleanPath(path),
|
|
1221
|
+
r.RedirectTrailingSlash,
|
|
1222
|
+
)
|
|
1223
|
+
if found {
|
|
1224
|
+
req.URL.Path = fixedPath
|
|
1225
|
+
http.Redirect(w, req, req.URL.String(), code)
|
|
1226
|
+
return
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if req.Method == http.MethodOptions && r.HandleOPTIONS {
|
|
1233
|
+
// Handle OPTIONS requests
|
|
1234
|
+
if allow := r.allowed(path, http.MethodOptions); allow != "" {
|
|
1235
|
+
w.Header().Set("Allow", allow)
|
|
1236
|
+
if r.GlobalOPTIONS != nil {
|
|
1237
|
+
r.GlobalOPTIONS.ServeHTTP(w, req)
|
|
1238
|
+
}
|
|
1239
|
+
return
|
|
1240
|
+
}
|
|
1241
|
+
} else if r.HandleMethodNotAllowed { // Handle 405
|
|
1242
|
+
if allow := r.allowed(path, req.Method); allow != "" {
|
|
1243
|
+
w.Header().Set("Allow", allow)
|
|
1244
|
+
if r.MethodNotAllowed != nil {
|
|
1245
|
+
r.MethodNotAllowed.ServeHTTP(w, req)
|
|
1246
|
+
} else {
|
|
1247
|
+
http.Error(w,
|
|
1248
|
+
http.StatusText(http.StatusMethodNotAllowed),
|
|
1249
|
+
http.StatusMethodNotAllowed,
|
|
1250
|
+
)
|
|
1251
|
+
}
|
|
1252
|
+
return
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Handle 404
|
|
1257
|
+
if r.NotFound != nil {
|
|
1258
|
+
page := createPage(RouteInfo{}, r.NotFound(NewRenderContext()))
|
|
1259
|
+
w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
|
|
1260
|
+
w.Header().Set("Content-Type", "text/html")
|
|
1261
|
+
w.WriteHeader(http.StatusOK)
|
|
1262
|
+
w.Write(page.Bytes())
|
|
1263
|
+
} else {
|
|
1264
|
+
page := createPage(RouteInfo{}, DefaultNotFound(NewRenderContext()))
|
|
1265
|
+
w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
|
|
1266
|
+
w.Header().Set("Content-Type", "text/html")
|
|
1267
|
+
w.WriteHeader(http.StatusOK)
|
|
1268
|
+
w.Write(page.Bytes())
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
func DefaultNotFound(c *RenderContext) UI {
|
|
1273
|
+
return Col(
|
|
1274
|
+
Row(
|
|
1275
|
+
"This is the default 404 - Not Found Route handler",
|
|
1276
|
+
),
|
|
1277
|
+
Row(
|
|
1278
|
+
"Create a notfound.go file and add a func NotFound(c *RenderContext) UI {} to override it",
|
|
1279
|
+
),
|
|
1280
|
+
)
|
|
1281
|
+
}
|
utils.go
CHANGED
|
@@ -2,7 +2,6 @@ package app
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"io"
|
|
5
|
-
"regexp"
|
|
6
5
|
"unsafe"
|
|
7
6
|
|
|
8
7
|
"github.com/pyros2097/wapp/js"
|
|
@@ -50,32 +49,9 @@ func stob(s string) []byte {
|
|
|
50
49
|
return *(*[]byte)(unsafe.Pointer(&s))
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
func matchPath(k, p string) bool {
|
|
54
|
-
validRoute := regexp.MustCompile(k)
|
|
55
|
-
|
|
52
|
+
type RouteInfo struct {
|
|
56
|
-
|
|
53
|
+
Title string
|
|
57
|
-
}
|
|
58
|
-
return false
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
func MatchRoute(routes map[string]RenderFunc, path string) RenderFunc {
|
|
62
|
-
for key, renderFn := range routes {
|
|
63
|
-
if matchPath(key, path) {
|
|
64
|
-
|
|
54
|
+
Description string
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
notFound, ok := routes["/notfound"]
|
|
68
|
-
if ok {
|
|
69
|
-
|
|
55
|
+
Author string
|
|
70
|
-
}
|
|
71
|
-
return func(c *RenderContext) UI {
|
|
72
|
-
|
|
56
|
+
Keywords string
|
|
73
|
-
Row(
|
|
74
|
-
"This is the default 404 - Not Found Route handler",
|
|
75
|
-
),
|
|
76
|
-
Row(
|
|
77
|
-
"Create a notfound.go file and add a func NotFound(c *RenderContext) UI {} to override it",
|
|
78
|
-
),
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
57
|
}
|