generated from quinnj/Example.jl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhandlers.jl
398 lines (353 loc) · 14.3 KB
/
handlers.jl
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
module Handlers
import ..Request
"""
Handler
Abstract type for the handler interface that exists for documentation purposes.
A `Handler` is any function of the form `f(req::HTTP.Request) -> HTTP.Response`.
There is no requirement to subtype `Handler` and users should not rely on or dispatch
on `Handler`. A `Handler` function `f` can be passed to [`HTTP.serve`](@ref)
wherein a server will pass each incoming request to `f` to be handled and a response
to be returned. Handler functions are also the inputs to [`Middleware`](@ref) functions
which are functions of the form `f(::Handler) -> Handler`, i.e. they take a `Handler`
function as input, and return a "modified" or enhanced `Handler` function.
For advanced cases, a `Handler` function can also be of the form `f(stream::HTTP.Stream) -> Nothing`.
In this case, the server would be run like `HTTP.serve(f, ...; stream=true)`. For this use-case,
the handler function reads the request and writes the response to the stream directly. Note that
any middleware used with a stream handler also needs to be of the form `f(stream_handler) -> stream_handler`,
i.e. it needs to accept a stream `Handler` function and return a stream `Handler` function.
"""
abstract type Handler end
"""
Middleware
Abstract type for the middleware interface that exists for documentation purposes.
A `Middleware` is any function of the form `f(::Handler) -> Handler` (ref: [`Handler`](@ref)).
There is no requirement to subtype `Middleware` and users should not rely on or dispatch
on the `Middleware` type. While `HTTP.serve(f, ...)` requires a _handler_ function `f` to be
passed, middleware can be "stacked" to create a chain of functions that are called in sequence,
like `HTTP.serve(base_handler |> cookie_middleware |> auth_middlware, ...)`, where the
`base_handler` `Handler` function is passed to `cookie_middleware`, which takes the handler
and returns a "modified" handler (that parses and stores cookies). This "modified" handler is
then an input to the `auth_middlware`, which further enhances/modifies the handler.
"""
abstract type Middleware end
# tree-based router handler
mutable struct Variable
name::String
pattern::Union{Nothing, Regex}
end
const VARREGEX = r"^{([^:{}]+)(?::(.*))?}$"
function Variable(pattern)
re = Base.match(VARREGEX, pattern)
if re === nothing
error("problem parsing path variable for route: `$pattern`")
end
pat = re.captures[2]
return Variable(re.captures[1], pat === nothing ? nothing : Regex(pat))
end
struct Leaf
method::String
variables::Vector{Tuple{Int, String}}
path::String
handler::Any
end
Base.show(io::IO, x::Leaf) = print(io, "Leaf($(x.method))")
mutable struct Node
segment::Union{String, Variable}
exact::Vector{Node} # sorted alphabetically, all x.segment are String
conditional::Vector{Node} # unsorted; will be applied in source-order; all x.segment are Regex
wildcard::Union{Node, Nothing} # unconditional variable or wildcard
doublestar::Union{Node, Nothing} # /** to match any length of path; must be final segment
methods::Vector{Leaf}
end
Base.show(io::IO, x::Node) = print(io, "Node($(x.segment))")
isvariable(x) = startswith(x, "{") && endswith(x, "}")
segment(x) = isvariable(x) ? Variable(x) : String(x)
Node(x) = Node(x, Node[], Node[], nothing, nothing, Leaf[])
Node() = Node("*")
function find(y, itr; by=identity, eq=(==))
for (i, x) in enumerate(itr)
eq(by(x), y) && return i
end
return nothing
end
function insert!(node::Node, leaf, segments, i)
if i > length(segments)
# time to insert leaf method match node
j = find(leaf.method, node.methods; by=x->x.method, eq=(x, y) -> x == "*" || x == y)
if j === nothing
push!(node.methods, leaf)
else
# hmmm, we've seen this route before, warn that we're replacing
@warn "replacing existing registered route; $(node.methods[j].method) => \"$(node.methods[j].path)\" route with new path = \"$(leaf.path)\""
node.methods[j] = leaf
end
return
end
segment = segments[i]
# @show segment, segment isa Variable
if segment isa Variable
# if we're inserting a variable segment, add variable name to leaf vars array
push!(leaf.variables, (i, segment.name))
end
# figure out which kind of node this segment is
if segment == "*" || (segment isa Variable && segment.pattern === nothing)
# wildcard node
if node.wildcard === nothing
node.wildcard = Node(segment)
end
return insert!(node.wildcard, leaf, segments, i + 1)
elseif segment == "**"
# double-star node
if node.doublestar === nothing
node.doublestar = Node(segment)
end
if i < length(segments)
error("/** double wildcard must be last segment in path")
end
return insert!(node.doublestar, leaf, segments, i + 1)
elseif segment isa Variable
# conditional node
# check if we've seen this exact conditional segment before
j = find(segment.pattern, node.conditional; by=x->x.segment.pattern)
if j === nothing
# new pattern
n = Node(segment)
push!(node.conditional, n)
else
n = node.conditional[j]
end
return insert!(n, leaf, segments, i + 1)
else
# exact node
@assert segment isa String
j = find(segment, node.exact; by=x->x.segment)
if j === nothing
# new exact match segment
n = Node(segment)
push!(node.exact, n)
sort!(node.exact; by=x->x.segment)
return insert!(n, leaf, segments, i + 1)
else
# existing exact match segment
return insert!(node.exact[j], leaf, segments, i + 1)
end
end
end
function match(node::Node, method, segments, i)
# @show node.segment, i, segments
if i > length(segments)
if isempty(node.methods)
return nothing
end
j = find(method, node.methods; by=x->x.method, eq=(x, y) -> x == "*" || x == y)
if j === nothing
# we return missing here so we can return a 405 instead of 404
# i.e. we matched the route, but there wasn't a matching method
return missing
else
# return matched leaf node
return node.methods[j]
end
end
segment = segments[i]
anymissing = false
# first check for exact matches
j = find(segment, node.exact; by=x->x.segment)
if j !== nothing
# found an exact match, recurse
m = match(node.exact[j], method, segments, i + 1)
anymissing = m === missing
m = coalesce(m, nothing)
# @show :exact, m
if m !== nothing
return m
end
end
# check for conditional matches
for node in node.conditional
# @show node.segment.pattern, segment
if Base.match(node.segment.pattern, segment) !== nothing
# matched a conditional node, recurse
m = match(node, method, segments, i + 1)
anymissing = m === missing
m = coalesce(m, nothing)
if m !== nothing
return m
end
end
end
if node.wildcard !== nothing
m = match(node.wildcard, method, segments, i + 1)
anymissing = m === missing
m = coalesce(m, nothing)
if m !== nothing
return m
end
end
if node.doublestar !== nothing
m = match(node.doublestar, method, segments, length(segments) + 1)
anymissing = m === missing
m = coalesce(m, nothing)
if m !== nothing
return m
end
end
return anymissing ? missing : nothing
end
"""
HTTP.Router(_404, _405, middleware=nothing)
Define a router object that maps incoming requests by path to registered routes and
associated handlers. Paths can be registered using [`HTTP.register!`](@ref). The router
object itself is a "request handler" that can be called like:
```
r = HTTP.Router()
resp = r(request)
```
Which will inspect the `request`, find the matching, registered handler from the url,
and pass the request on to be handled further.
See [`HTTP.register!`](@ref) for additional information on registering handlers based on routes.
If a request doesn't have a matching, registered handler, the `_404` handler is called which,
by default, returns a `HTTP.Response(404)`. If a route matches the path, but not the method/verb
(e.g. there's a registerd route for "GET /api", but the request is "POST /api"), then the `_405`
handler is called, which by default returns `HTTP.Response(405)` (method not allowed).
A `middleware` ([`Middleware`](@ref)) can optionally be provided as well, which will be called
after the router has matched the request to a route, but before the route's handler is called.
This provides a "hook" for matched routes that can be helpful for metric tracking, logging, etc.
Note that the middleware is only called if the route is matched; for the 404 and 405 cases,
users should wrap those handlers in the `middleware` manually.
"""
struct Router{T, S, F}
_404::T
_405::S
routes::Node
middleware::F
end
default404(::Request) = Response(404)
default405(::Request) = Response(405)
# default404(s::Stream) = setstatus(s, 404)
# default405(s::Stream) = setstatus(s, 405)
Router(_404=default404, _405=default405, middleware=nothing) = Router(_404, _405, Node(), middleware)
"""
HTTP.register!(r::Router, method, path, handler)
HTTP.register!(r::Router, path, handler)
Register a handler function that should be called when an incoming request matches `path`
and the optionally provided `method` (if not provided, any method is allowed). Can be used
to dynamically register routes. When a registered route is matched, the original route string
is stored in the `request.context[:route]` variable.
The following path types are allowed for matching:
* `/api/widgets`: exact match of static strings
* `/api/*/owner`: single `*` to wildcard match anything for a single segment
* `/api/widget/{id}`: Define a path variable `id` that matches any valued provided for this segment; path variables are available in the request context like `HTTP.getparams(req)["id"]`
* `/api/widget/{id:[0-9]+}`: Define a path variable `id` that does a regex match for integers for this segment
* `/api/**`: double wildcard matches any number of trailing segments in the request path; the double wildcard must be the last segment in the path
"""
function register! end
function register!(r::Router, method, path, handler)
segments = map(segment, split(path, '/'; keepempty=false))
if r.middleware !== nothing
handler = r.middleware(handler)
end
insert!(r.routes, Leaf(method, Tuple{Int, String}[], path, handler), segments, 1)
return
end
register!(r::Router, path, handler) = register!(r, "*", path, handler)
const Params = Dict{String, String}
function gethandler(r::Router, req::Request)
url = req.uri
segments = split(url.path, '/'; keepempty=false)
leaf = match(r.routes, req.method, segments, 1)
params = Params()
if leaf isa Leaf
# @show leaf.variables, segments
if !isempty(leaf.variables)
# we have variables to fill in
for (i, v) in leaf.variables
params[v] = segments[i]
end
end
return leaf.handler, leaf.path, params
end
return leaf, "", params
end
# function (r::Router)(stream::Stream{<:Request})
# req = stream.message
# handler, route, params = gethandler(r, req)
# if handler === nothing
# # didn't match a registered route
# return r._404(stream)
# elseif handler === missing
# # matched the path, but method not supported
# return r._405(stream)
# else
# req.context[:route] = route
# if !isempty(params)
# req.context[:params] = params
# end
# return handler(stream)
# end
# end
function (r::Router)(req::Request)
handler, route, params = gethandler(r, req)
if handler === nothing
# didn't match a registered route
return r._404(req)
elseif handler === missing
# matched the path, but method not supported
return r._405(req)
else
req.context[:route] = route
if !isempty(params)
req.context[:params] = params
end
return handler(req)
end
end
"""
HTTP.getroute(req) -> String
Retrieve the original route registration string for a request after its url has been
matched against a router. Helpful for metric logging to ignore matched variables in
a path and only see the registered routes.
"""
getroute(req) = Base.get(req.context, :route, nothing)
"""
HTTP.getparams(req) -> Dict{String, String}
Retrieve any matched path parameters from the request context.
If a path was registered with a router via `HTTP.register!` like
"/api/widget/{id}", then the path parameters are available in the request context
and can be retrieved like `id = HTTP.getparams(req)["id"]`.
"""
getparams(req) = Base.get(req.context, :params, nothing)
"""
HTTP.getparam(req, name, default=nothing) -> String
Retrieve a matched path parameter with name `name` from request context.
If a path was registered with a router via `HTTP.register!` like
"/api/widget/{id}", then the path parameter can be retrieved like `id = HTTP.getparam(req, "id").
"""
function getparam(req, name, default=nothing)
params = getparams(req)
params === nothing && return default
return Base.get(params, name, default)
end
"""
HTTP.Handlers.cookie_middleware(handler) -> handler
Middleware that parses and stores any cookies in the incoming
request in the request context. Cookies can then be retrieved by calling
[`HTTP.getcookies(req)`](@ref) in subsequent middlewares/handlers.
"""
function cookie_middleware(handler)
function (req)
if !haskey(req.context, :cookies)
req.context[:cookies] = Cookies.cookies(req)
end
return handler(req)
end
end
"""
HTTP.getcookies(req) -> Vector{Cookie}
Retrieve any parsed cookies from a request context. Cookies
are expected to be stored in the `req.context[:cookies]` of the
request context as implemented in the [`HTTP.Handlers.cookie_middleware`](@ref)
middleware.
"""
getcookies(req) = Base.get(() -> Cookie[], req.context, :cookies)
end # module Handlers