-
Notifications
You must be signed in to change notification settings - Fork 0
/
router.go
147 lines (132 loc) · 4.33 KB
/
router.go
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package rest
//
//Copyright 2018 Telenor Digital AS
//
//Licensed under the Apache License, Version 2.0 (the "License");
//you may not use this file except in compliance with the License.
//You may obtain a copy of the License at
//
//http://www.apache.org/licenses/LICENSE-2.0
//
//Unless required by applicable law or agreed to in writing, software
//distributed under the License is distributed on an "AS IS" BASIS,
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//See the License for the specific language governing permissions and
//limitations under the License.
//
import (
"context"
"fmt"
"log"
"net/http"
"regexp"
"strings"
)
// PathParameter is the type used when storing parameters in the context
type PathParameter string
// The elementwalker function is used when walking through the different parts of the path.
type elementwalker func(pattern string, path string, param map[string]string) bool
// Pull a value (aka parameter) from the path
func getparameter(pattern string, path string, param map[string]string) bool {
param[pattern] = path
return true
}
// Do a simple comparison between path elements
func compare(pattern string, path string, param map[string]string) bool {
return pattern == path
}
// A single route
type route struct {
elements []string
process []elementwalker
handler http.HandlerFunc
}
// Match an URI. The URI is split into an array of strings (ie elements of path). Returns
// a list of parameters and a bool
func (r *route) matches(uriElements []string) (map[string]string, bool) {
if len(uriElements) != len(r.elements) {
return nil, false
}
params := make(map[string]string)
for i, pathElement := range r.elements {
if !r.process[i](pathElement, uriElements[i], params) {
return nil, false
}
}
return params, true
}
// ParameterRouter implements a request URI router that allows for path parameters in
// URIs, ie requests like https://example.com/api/thing/0424242/subthing/ - ie presenting
// resources with IDs in the request URI rather than having to rely on query parameters.
// The router can be plugged in in the standard http package.
type ParameterRouter struct {
routes []route
}
// NewParameterRouter creates a new router instance
func NewParameterRouter() ParameterRouter {
return ParameterRouter{routes: make([]route, 0)}
}
// AddRoute adds a new route described by the specified pattern, handled by the supplied ParameterHandler. This method isn't thread safe.
func (r *ParameterRouter) AddRoute(pattern string, handler http.HandlerFunc) {
patternElements := strings.Split(pattern, "/")
newRoute := route{
elements: patternElements,
handler: handler,
}
const parameterPattern string = "{(.*)}"
re, err := regexp.Compile(parameterPattern)
if err != nil {
panic(fmt.Sprintf("Unable to compile regexp for pattern: %s", err))
}
for i, element := range patternElements {
match := re.FindStringSubmatch(element)
var procFunc elementwalker
if len(match) > 0 {
// Matching element - add to parameter http.ListenAndServe(":8080", nil)
newRoute.elements[i] = match[1]
procFunc = getparameter
} else {
newRoute.elements[i] = element
procFunc = compare
}
newRoute.process = append(newRoute.process, procFunc)
}
r.routes = append(r.routes, newRoute)
}
// GetHandler returns a matching handler (as a closure) for the matching uri. If no matching route
// is found it will return the default http.NotFound handler.
func (r *ParameterRouter) GetHandler(uri string) http.HandlerFunc {
params := strings.Split(uri, "?")
pathElements := strings.Split(params[0], "/")
for _, route := range r.routes {
params, matches := route.matches(pathElements)
if matches {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
for key, value := range params {
ctx = context.WithValue(ctx, PathParameter(key), value)
}
route.handler(w, r.WithContext(ctx))
}
}
}
log.Printf("No matching handler for %s", uri)
return http.NotFound
}
// GetPathKey is a simple utility function that will return the (string) value
// for the specified key in the path. If there's an error it will return an
// empty string.
func GetPathKey(name string, r *http.Request) string {
if r == nil {
return ""
}
v := r.Context().Value(PathParameter(name))
if v == nil {
return ""
}
stringVal, ok := v.(string)
if !ok {
return ""
}
return stringVal
}