~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.
f3604a4a
—
pyros2097 5 years ago
handle panics
- app_nowasm.go +7 -7
- app_wasm.go +5 -10
- example/main.go +1 -0
- example/panic.go +9 -0
- router.go +28 -406
app_nowasm.go
CHANGED
|
@@ -5,6 +5,7 @@ package app
|
|
|
5
5
|
import (
|
|
6
6
|
"bytes"
|
|
7
7
|
"fmt"
|
|
8
|
+
"io"
|
|
8
9
|
"net/http"
|
|
9
10
|
"os"
|
|
10
11
|
"path/filepath"
|
|
@@ -46,19 +47,19 @@ func Run() {
|
|
|
46
47
|
return
|
|
47
48
|
}
|
|
48
49
|
assetsFS := http.FileServer(pkger.Dir(filepath.Join(wd, "assets")))
|
|
49
|
-
|
|
50
|
+
AppRouter.GET("/assets/*filepath", func(w http.ResponseWriter, r *http.Request) {
|
|
50
51
|
r.URL.Path = strings.Replace(r.URL.Path, "/assets", "", 1)
|
|
51
52
|
assetsFS.ServeHTTP(w, r)
|
|
52
53
|
})
|
|
53
54
|
}
|
|
54
55
|
if isLambda {
|
|
55
56
|
println("running in lambda mode")
|
|
56
|
-
algnhsa.ListenAndServe(
|
|
57
|
+
algnhsa.ListenAndServe(AppRouter, &algnhsa.Options{
|
|
57
58
|
BinaryContentTypes: []string{"application/wasm", "image/png"},
|
|
58
59
|
})
|
|
59
60
|
} else {
|
|
60
61
|
println("Serving on HTTP port: 1234")
|
|
61
|
-
http.ListenAndServe(":1234",
|
|
62
|
+
http.ListenAndServe(":1234", AppRouter)
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -68,10 +69,10 @@ func Reload() {
|
|
|
68
69
|
|
|
69
70
|
func Route(path string, render RenderFunc) {
|
|
70
71
|
println("registering route: " + path)
|
|
71
|
-
|
|
72
|
+
AppRouter.GET(path, render)
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
func
|
|
75
|
+
func writePage(ui UI, w io.Writer) {
|
|
75
76
|
isLambda := os.Getenv("_LAMBDA_SERVER_PORT") != ""
|
|
76
77
|
page := bytes.NewBuffer(nil)
|
|
77
78
|
page.WriteString("<!DOCTYPE html>\n")
|
|
@@ -92,6 +93,5 @@ func createPage(ui UI) *bytes.Buffer {
|
|
|
92
93
|
Script(`const enosys = () => { const a = new Error("not implemented"); return a.code = "ENOSYS", a }; let outputBuf = ""; window.fs = { constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, writeSync(a, b) { outputBuf += decoder.decode(b); const c = outputBuf.lastIndexOf("\n"); return -1 != c && (console.log(outputBuf.substr(0, c)), outputBuf = outputBuf.substr(c + 1)), b.length }, write(a, b, c, d, e, f) { if (0 !== c || d !== b.length || null !== e) return void f(enosys()); const g = this.writeSync(a, b); f(null, g) } }; const encoder = new TextEncoder("utf-8"), decoder = new TextDecoder("utf-8"); class Go { constructor() { this.argv = ["js"], this.env = {}, this.exit = a => { 0 !== a && console.warn("exit code:", a) }, this._exitPromise = new Promise(a => { this._resolveExitPromise = a }), this._pendingEvent = null, this._scheduledTimeouts = new Map, this._nextCallbackTimeoutID = 1; const a = (a, b) => { this.mem.setUint32(a + 0, b, !0), this.mem.setUint32(a + 4, Math.floor(b / 4294967296), !0) }, b = a => { const b = this.mem.getUint32(a + 0, !0), c = this.mem.getInt32(a + 4, !0); return b + 4294967296 * c }, c = a => { const b = this.mem.getFloat64(a, !0); if (0 !== b) { if (!isNaN(b)) return b; const c = this.mem.getUint32(a, !0); return this._values[c] } }, d = (a, b) => { const c = 2146959360; if ("number" == typeof b) return isNaN(b) ? (this.mem.setUint32(a + 4, 2146959360, !0), void this.mem.setUint32(a, 0, !0)) : 0 === b ? (this.mem.setUint32(a + 4, 2146959360, !0), void this.mem.setUint32(a, 1, !0)) : void this.mem.setFloat64(a, b, !0); switch (b) { case void 0: return void this.mem.setFloat64(a, 0, !0); case null: return this.mem.setUint32(a + 4, c, !0), void this.mem.setUint32(a, 2, !0); case !0: return this.mem.setUint32(a + 4, c, !0), void this.mem.setUint32(a, 3, !0); case !1: return this.mem.setUint32(a + 4, c, !0), void this.mem.setUint32(a, 4, !0); }let d = this._ids.get(b); d === void 0 && (d = this._idPool.pop(), d === void 0 && (d = this._values.length), this._values[d] = b, this._goRefCounts[d] = 0, this._ids.set(b, d)), this._goRefCounts[d]++; let e = 1; switch (typeof b) { case "string": e = 2; break; case "symbol": e = 3; break; case "function": e = 4; }this.mem.setUint32(a + 4, 2146959360 | e, !0), this.mem.setUint32(a, d, !0) }, e = a => { const c = b(a + 0), d = b(a + 8); return new Uint8Array(this._inst.exports.mem.buffer, c, d) }, f = d => { const e = b(d + 0), f = b(d + 8), g = Array(f); for (let a = 0; a < f; a++)g[a] = c(e + 8 * a); return g }, g = a => { const c = b(a + 0), d = b(a + 8); return decoder.decode(new DataView(this._inst.exports.mem.buffer, c, d)) }, h = Date.now() - performance.now(); this.importObject = { go: { "runtime.wasmExit": a => { const b = this.mem.getInt32(a + 8, !0); this.exited = !0, delete this._inst, delete this._values, delete this._goRefCounts, delete this._ids, delete this._idPool, this.exit(b) }, "runtime.wasmWrite": a => { const c = b(a + 8), d = b(a + 16), e = this.mem.getInt32(a + 24, !0); fs.writeSync(c, new Uint8Array(this._inst.exports.mem.buffer, d, e)) }, "runtime.resetMemoryDataView": () => { this.mem = new DataView(this._inst.exports.mem.buffer) }, "runtime.nanotime1": b => { a(b + 8, 1e6 * (h + performance.now())) }, "runtime.walltime1": b => { const c = new Date().getTime(); a(b + 8, c / 1e3), this.mem.setInt32(b + 16, 1e6 * (c % 1e3), !0) }, "runtime.scheduleTimeoutEvent": a => { const c = this._nextCallbackTimeoutID; this._nextCallbackTimeoutID++, this._scheduledTimeouts.set(c, setTimeout(() => { for (this._resume(); this._scheduledTimeouts.has(c);)console.warn("scheduleTimeoutEvent: missed timeout event"), this._resume() }, b(a + 8) + 1)), this.mem.setInt32(a + 16, c, !0) }, "runtime.clearTimeoutEvent": a => { const b = this.mem.getInt32(a + 8, !0); clearTimeout(this._scheduledTimeouts.get(b)), this._scheduledTimeouts.delete(b) }, "runtime.getRandomData": a => { crypto.getRandomValues(e(a + 8)) }, "syscall/js.finalizeRef": a => { const b = this.mem.getUint32(a + 8, !0); if (this._goRefCounts[b]--, 0 === this._goRefCounts[b]) { const a = this._values[b]; this._values[b] = null, this._ids.delete(a), this._idPool.push(b) } }, "syscall/js.stringVal": a => { d(a + 24, g(a + 8)) }, "syscall/js.valueGet": a => { const b = Reflect.get(c(a + 8), g(a + 16)); a = this._inst.exports.getsp(), d(a + 32, b) }, "syscall/js.valueSet": a => { Reflect.set(c(a + 8), g(a + 16), c(a + 32)) }, "syscall/js.valueDelete": a => { Reflect.deleteProperty(c(a + 8), g(a + 16)) }, "syscall/js.valueIndex": a => { d(a + 24, Reflect.get(c(a + 8), b(a + 16))) }, "syscall/js.valueSetIndex": a => { Reflect.set(c(a + 8), b(a + 16), c(a + 24)) }, "syscall/js.valueCall": a => { try { const b = c(a + 8), e = Reflect.get(b, g(a + 16)), h = f(a + 32), i = Reflect.apply(e, b, h); a = this._inst.exports.getsp(), d(a + 56, i), this.mem.setUint8(a + 64, 1) } catch (b) { d(a + 56, b), this.mem.setUint8(a + 64, 0) } }, "syscall/js.valueInvoke": a => { try { const b = c(a + 8), e = f(a + 16), g = Reflect.apply(b, void 0, e); a = this._inst.exports.getsp(), d(a + 40, g), this.mem.setUint8(a + 48, 1) } catch (b) { d(a + 40, b), this.mem.setUint8(a + 48, 0) } }, "syscall/js.valueNew": a => { try { const b = c(a + 8), e = f(a + 16), g = Reflect.construct(b, e); a = this._inst.exports.getsp(), d(a + 40, g), this.mem.setUint8(a + 48, 1) } catch (b) { d(a + 40, b), this.mem.setUint8(a + 48, 0) } }, "syscall/js.valueLength": b => { a(b + 16, parseInt(c(b + 8).length)) }, "syscall/js.valuePrepareString": b => { const e = encoder.encode(c(b + 8) + ""); d(b + 16, e), a(b + 24, e.length) }, "syscall/js.valueLoadString": a => { const b = c(a + 8); e(a + 16).set(b) }, "syscall/js.valueInstanceOf": a => { this.mem.setUint8(a + 24, c(a + 8) instanceof c(a + 16)) }, "syscall/js.copyBytesToGo": b => { const d = e(b + 8), f = c(b + 32); if (!(f instanceof Uint8Array)) return void this.mem.setUint8(b + 48, 0); const g = f.subarray(0, d.length); d.set(g), a(b + 40, g.length), this.mem.setUint8(b + 48, 1) }, "syscall/js.copyBytesToJS": b => { const d = c(b + 8), f = e(b + 16); if (!(d instanceof Uint8Array)) return void this.mem.setUint8(b + 48, 0); const g = f.subarray(0, d.length); d.set(g), a(b + 40, g.length), this.mem.setUint8(b + 48, 1) }, debug: a => { console.log(a) } } } } async run(a) { this._inst = a, this.mem = new DataView(this._inst.exports.mem.buffer), this._values = [NaN, 0, null, !0, !1, window, this], this._goRefCounts = [], this._ids = new Map, this._idPool = [], this.exited = !1; let b = 4096; const c = a => { const c = b, d = encoder.encode(a + "\0"); return new Uint8Array(this.mem.buffer, b, d.length).set(d), b += d.length, 0 != b % 8 && (b += 8 - b % 8), c }, d = this.argv.length, e = []; this.argv.forEach(a => { e.push(c(a)) }), e.push(0); const f = Object.keys(this.env).sort(); f.forEach(a => { e.push(c(a + "=" + this.env[a])) }), e.push(0); const g = b; e.forEach(a => { this.mem.setUint32(b, a, !0), this.mem.setUint32(b + 4, 0, !0), b += 8 }), this._inst.exports.run(d, g), this.exited && this._resolveExitPromise(), await this._exitPromise } _resume() { if (this.exited) throw new Error("Go program has already exited"); this._inst.exports.resume(), this.exited && this._resolveExitPromise() } _makeFuncWrapper(a) { const b = this; return function () { const c = { id: a, this: this, args: arguments }; return b._pendingEvent = c, b._resume(), c.result } } } const go = new Go; WebAssembly.instantiateStreaming(fetch("`+basePath+`main.wasm`+`"), go.importObject).then(a => go.run(a.instance)).catch(a => console.error("could not load wasm", a));`),
|
|
93
94
|
),
|
|
94
95
|
Body(ui),
|
|
95
|
-
).Html(
|
|
96
|
+
).Html(w)
|
|
96
|
-
return page
|
|
97
97
|
}
|
app_wasm.go
CHANGED
|
@@ -2,19 +2,15 @@
|
|
|
2
2
|
package app
|
|
3
3
|
|
|
4
4
|
import (
|
|
5
|
-
"
|
|
5
|
+
"io"
|
|
6
6
|
|
|
7
7
|
"github.com/pyros2097/wapp/js"
|
|
8
8
|
)
|
|
9
9
|
|
|
10
10
|
func Run() {
|
|
11
|
-
handle, _, _ :=
|
|
11
|
+
handle, _, _ := AppRouter.Lookup("GET", js.Window.URL().Path)
|
|
12
12
|
if handle == nil {
|
|
13
|
-
if router.NotFound != nil {
|
|
14
|
-
|
|
13
|
+
renderFunc = AppRouter.NotFound
|
|
15
|
-
} else {
|
|
16
|
-
renderFunc = DefaultNotFound
|
|
17
|
-
}
|
|
18
14
|
} else {
|
|
19
15
|
renderFunc, _ = handle.(RenderFunc)
|
|
20
16
|
}
|
|
@@ -46,7 +42,7 @@ func Reload() {
|
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
func Route(path string, render RenderFunc) {
|
|
49
|
-
|
|
45
|
+
AppRouter.GET(path, render)
|
|
50
46
|
}
|
|
51
47
|
|
|
52
48
|
func initBody() {
|
|
@@ -81,6 +77,5 @@ func initContent() {
|
|
|
81
77
|
// return u.Fragment != ""
|
|
82
78
|
// }
|
|
83
79
|
|
|
84
|
-
func
|
|
80
|
+
func writePage(ui UI, w io.Writer) {
|
|
85
|
-
return &bytes.Buffer{}
|
|
86
81
|
}
|
example/main.go
CHANGED
|
@@ -5,6 +5,7 @@ import (
|
|
|
5
5
|
)
|
|
6
6
|
|
|
7
7
|
func main() {
|
|
8
|
+
Route("/panic", Panic)
|
|
8
9
|
Route("/about", About)
|
|
9
10
|
Route("/clock", Clock)
|
|
10
11
|
Route("/container", Container)
|
example/panic.go
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
. "github.com/pyros2097/wapp"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
func Panic(c *RenderContext) UI {
|
|
8
|
+
panic("Hello")
|
|
9
|
+
}
|
router.go
CHANGED
|
@@ -3,157 +3,12 @@ package app
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
"net/http"
|
|
6
|
-
"strconv"
|
|
7
6
|
"strings"
|
|
8
7
|
"sync"
|
|
9
8
|
"unicode"
|
|
10
9
|
"unicode/utf8"
|
|
11
10
|
)
|
|
12
11
|
|
|
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
12
|
func min(a, b int) int {
|
|
158
13
|
if a <= b {
|
|
159
14
|
return a
|
|
@@ -857,31 +712,12 @@ func ParamsFromContext(ctx context.Context) Params {
|
|
|
857
712
|
return p
|
|
858
713
|
}
|
|
859
714
|
|
|
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
715
|
// Router is a http.Handler which can be used to dispatch requests to different
|
|
872
716
|
// handler functions via configurable routes
|
|
873
717
|
type Router struct {
|
|
874
|
-
trees
|
|
718
|
+
trees map[string]*node
|
|
875
|
-
|
|
876
719
|
paramsPool sync.Pool
|
|
877
720
|
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
721
|
// Enables automatic redirection if the current route can't be matched but a
|
|
886
722
|
// handler for the path with (without) the trailing slash exists.
|
|
887
723
|
// For example if /foo/ is requested but a route only exists for /foo, the
|
|
@@ -889,66 +725,25 @@ type Router struct {
|
|
|
889
725
|
// and 308 for all other request methods.
|
|
890
726
|
RedirectTrailingSlash bool
|
|
891
727
|
|
|
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
|
|
728
|
+
// Configurable handler which is called when no matching route is found.
|
|
925
|
-
// found. If it is not set, http.NotFound is used.
|
|
926
729
|
NotFound RenderFunc
|
|
927
730
|
|
|
928
|
-
// Configurable
|
|
731
|
+
// Configurable handler which is called when an error occurs.
|
|
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
|
-
|
|
732
|
+
Error RenderFunc
|
|
940
|
-
PanicHandler func(http.ResponseWriter, *http.Request, interface{})
|
|
941
733
|
}
|
|
942
734
|
|
|
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
|
|
735
|
+
var AppRouter = &Router{
|
|
948
|
-
RedirectTrailingSlash:
|
|
736
|
+
RedirectTrailingSlash: true,
|
|
949
|
-
|
|
737
|
+
NotFound: func(c *RenderContext) UI {
|
|
950
|
-
|
|
738
|
+
return Col(
|
|
739
|
+
Row(
|
|
951
|
-
|
|
740
|
+
Text("This is the default 404 - Not Found Route handler"),
|
|
741
|
+
),
|
|
742
|
+
Row(
|
|
743
|
+
Text("Create a notfound.go file and add a func NotFound(c *RenderContext) UI {} to override it"),
|
|
744
|
+
),
|
|
745
|
+
)
|
|
746
|
+
},
|
|
952
747
|
}
|
|
953
748
|
|
|
954
749
|
func (r *Router) getParams() *Params {
|
|
@@ -963,43 +758,11 @@ func (r *Router) putParams(ps *Params) {
|
|
|
963
758
|
}
|
|
964
759
|
}
|
|
965
760
|
|
|
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
761
|
// GET is a shortcut for router.Handle(http.MethodGet, path, handle)
|
|
988
762
|
func (r *Router) GET(path string, handle interface{}) {
|
|
989
763
|
r.Handle(http.MethodGet, path, handle)
|
|
990
764
|
}
|
|
991
765
|
|
|
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
766
|
func (r *Router) POST(path string, handle interface{}) {
|
|
1004
767
|
r.Handle(http.MethodPost, path, handle)
|
|
1005
768
|
}
|
|
@@ -1009,11 +772,6 @@ func (r *Router) PUT(path string, handle interface{}) {
|
|
|
1009
772
|
r.Handle(http.MethodPut, path, handle)
|
|
1010
773
|
}
|
|
1011
774
|
|
|
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
775
|
// DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle)
|
|
1018
776
|
func (r *Router) DELETE(path string, handle interface{}) {
|
|
1019
777
|
r.Handle(http.MethodDelete, path, handle)
|
|
@@ -1053,8 +811,6 @@ func (r *Router) Handle(method, path string, handle interface{}) {
|
|
|
1053
811
|
if root == nil {
|
|
1054
812
|
root = new(node)
|
|
1055
813
|
r.trees[method] = root
|
|
1056
|
-
|
|
1057
|
-
r.globalAllowed = r.allowed("*", "")
|
|
1058
814
|
}
|
|
1059
815
|
|
|
1060
816
|
root.addRoute(path, handle)
|
|
@@ -1073,34 +829,6 @@ func (r *Router) Handle(method, path string, handle interface{}) {
|
|
|
1073
829
|
}
|
|
1074
830
|
}
|
|
1075
831
|
|
|
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
832
|
// Lookup allows the manual lookup of a method + path combo.
|
|
1105
833
|
// This is e.g. useful to build a framework around this router.
|
|
1106
834
|
// If the path was found, it returns the handle function and the path parameter
|
|
@@ -1121,62 +849,16 @@ func (r *Router) Lookup(method, path string) (interface{}, Params, bool) {
|
|
|
1121
849
|
return nil, nil, false
|
|
1122
850
|
}
|
|
1123
851
|
|
|
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
852
|
// ServeHTTP makes the router implement the http.Handler interface.
|
|
1176
853
|
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
854
|
+
// Handle errors
|
|
855
|
+
defer func() {
|
|
1177
|
-
|
|
856
|
+
if rcv := recover(); rcv != nil {
|
|
1178
|
-
|
|
857
|
+
writePage(r.Error(NewRenderContext()), w)
|
|
858
|
+
w.Header().Set("Content-Type", "text/html")
|
|
859
|
+
w.WriteHeader(http.StatusInternalServerError)
|
|
1179
|
-
|
|
860
|
+
}
|
|
861
|
+
}()
|
|
1180
862
|
|
|
1181
863
|
path := req.URL.Path
|
|
1182
864
|
|
|
@@ -1185,11 +867,9 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
|
1185
867
|
if handle, _, tsr := root.getValue(path, r.getParams); handle != nil {
|
|
1186
868
|
println("route: " + req.URL.Path)
|
|
1187
869
|
if render, ok := handle.(RenderFunc); ok {
|
|
1188
|
-
|
|
870
|
+
writePage(render(NewRenderContext()), w)
|
|
1189
|
-
w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
|
|
1190
871
|
w.Header().Set("Content-Type", "text/html")
|
|
1191
872
|
w.WriteHeader(http.StatusOK)
|
|
1192
|
-
w.Write(page.Bytes())
|
|
1193
873
|
return
|
|
1194
874
|
} else {
|
|
1195
875
|
handle.(func(w http.ResponseWriter, r *http.Request))(w, req)
|
|
@@ -1213,69 +893,11 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
|
1213
893
|
http.Redirect(w, req, req.URL.String(), code)
|
|
1214
894
|
return
|
|
1215
895
|
}
|
|
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
896
|
}
|
|
1254
897
|
}
|
|
1255
898
|
|
|
1256
899
|
// Handle 404
|
|
1257
|
-
if r.NotFound != nil {
|
|
1258
|
-
|
|
900
|
+
writePage(r.NotFound(NewRenderContext()), w)
|
|
1259
|
-
w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
|
|
1260
|
-
|
|
901
|
+
w.Header().Set("Content-Type", "text/html")
|
|
1261
|
-
|
|
902
|
+
w.WriteHeader(http.StatusNotFound)
|
|
1262
|
-
w.Write(page.Bytes())
|
|
1263
|
-
} else {
|
|
1264
|
-
page := createPage(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
|
-
Text("This is the default 404 - Not Found Route handler"),
|
|
1276
|
-
),
|
|
1277
|
-
Row(
|
|
1278
|
-
Text("Create a notfound.go file and add a func NotFound(c *RenderContext) UI {} to override it"),
|
|
1279
|
-
),
|
|
1280
|
-
)
|
|
1281
903
|
}
|