lph-11/odin-http/routing.odin
2025-03-13 18:14:21 +13:00

295 lines
6.5 KiB
Odin

package http
import "base:runtime"
import "core:log"
import "core:net"
import "core:strconv"
import "core:strings"
import "core:text/match"
URL :: struct {
raw: string, // All other fields are views/slices into this string.
scheme: string,
host: string,
path: string,
query: string,
}
url_parse :: proc(raw: string) -> (url: URL) {
url.raw = raw
s := raw
i := strings.index(s, "://")
if i >= 0 {
url.scheme = s[:i]
s = s[i+3:]
}
i = strings.index(s, "?")
if i != -1 {
url.query = s[i+1:]
s = s[:i]
}
i = strings.index(s, "/")
if i == -1 {
url.host = s
} else {
url.host = s[:i]
url.path = s[i:]
}
return
}
Query_Entry :: struct {
key, value: string,
}
query_iter :: proc(query: ^string) -> (entry: Query_Entry, ok: bool) {
if len(query) == 0 do return
ok = true
i := strings.index(query^, "=")
if i < 0 {
entry.key = query^
query^ = ""
return
}
entry.key = query[:i]
query^ = query[i+1:]
i = strings.index(query^, "&")
if i < 0 {
entry.value = query^
query^ = ""
return
}
entry.value = query[:i]
query^ = query[i+1:]
return
}
query_get :: proc(url: URL, key: string) -> (val: string, ok: bool) #optional_ok {
q := url.query
for entry in #force_inline query_iter(&q) {
if entry.key == key {
return entry.value, true
}
}
return
}
query_get_percent_decoded :: proc(url: URL, key: string, allocator := context.temp_allocator) -> (val: string, ok: bool) {
str := query_get(url, key) or_return
return net.percent_decode(str, allocator)
}
query_get_bool :: proc(url: URL, key: string) -> (result, set: bool) #optional_ok {
str := query_get(url, key) or_return
set = true
switch str {
case "", "false", "0", "no":
case:
result = true
}
return
}
query_get_int :: proc(url: URL, key: string, base := 0) -> (result: int, ok: bool, set: bool) {
str := query_get(url, key) or_return
set = true
result, ok = strconv.parse_int(str, base)
return
}
query_get_uint :: proc(url: URL, key: string, base := 0) -> (result: uint, ok: bool, set: bool) {
str := query_get(url, key) or_return
set = true
result, ok = strconv.parse_uint(str, base)
return
}
Route :: struct {
handler: Handler,
pattern: string,
}
Router :: struct {
allocator: runtime.Allocator,
routes: map[Method][dynamic]Route,
all: [dynamic]Route,
}
router_init :: proc(router: ^Router, allocator := context.allocator) {
router.allocator = allocator
router.routes = make(map[Method][dynamic]Route, len(Method), allocator)
}
router_destroy :: proc(router: ^Router) {
context.allocator = router.allocator
for route in router.all {
delete(route.pattern)
}
delete(router.all)
for _, routes in router.routes {
for route in routes {
delete(route.pattern)
}
delete(routes)
}
delete(router.routes)
}
// Returns a handler that matches against the given routes.
router_handler :: proc(router: ^Router) -> Handler {
h: Handler
h.user_data = router
h.handle = proc(handler: ^Handler, req: ^Request, res: ^Response) {
router := (^Router)(handler.user_data)
rline := req.line.(Requestline)
if routes_try(router.routes[rline.method], req, res) {
return
}
if routes_try(router.all, req, res) {
return
}
log.infof("no route matched %s %s", method_string(rline.method), rline.target)
res.status = .Not_Found
respond(res)
}
return h
}
route_get :: proc(router: ^Router, pattern: string, handler: Handler) {
route_add(
router,
.Get,
Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)},
)
}
route_post :: proc(router: ^Router, pattern: string, handler: Handler) {
route_add(
router,
.Post,
Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)},
)
}
// NOTE: this does not get called when `Server_Opts.redirect_head_to_get` is set to true.
route_head :: proc(router: ^Router, pattern: string, handler: Handler) {
route_add(
router,
.Head,
Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)},
)
}
route_put :: proc(router: ^Router, pattern: string, handler: Handler) {
route_add(
router,
.Put,
Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)},
)
}
route_patch :: proc(router: ^Router, pattern: string, handler: Handler) {
route_add(
router,
.Patch,
Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)},
)
}
route_trace :: proc(router: ^Router, pattern: string, handler: Handler) {
route_add(
router,
.Trace,
Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)},
)
}
route_delete :: proc(router: ^Router, pattern: string, handler: Handler) {
route_add(
router,
.Delete,
Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)},
)
}
route_connect :: proc(router: ^Router, pattern: string, handler: Handler) {
route_add(
router,
.Connect,
Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)},
)
}
route_options :: proc(router: ^Router, pattern: string, handler: Handler) {
route_add(
router,
.Options,
Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)},
)
}
// Adds a catch-all fallback route (all methods, ran if no other routes match).
route_all :: proc(router: ^Router, pattern: string, handler: Handler) {
if router.all == nil {
router.all = make([dynamic]Route, 0, 1, router.allocator)
}
append(
&router.all,
Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)},
)
}
@(private)
route_add :: proc(router: ^Router, method: Method, route: Route) {
if method not_in router.routes {
router.routes[method] = make([dynamic]Route, router.allocator)
}
append(&router.routes[method], route)
}
@(private)
routes_try :: proc(routes: [dynamic]Route, req: ^Request, res: ^Response) -> bool {
try_captures: [match.MAX_CAPTURES]match.Match = ---
for route in routes {
n, err := match.find_aux(req.url.path, route.pattern, 0, true, &try_captures)
if err != .OK {
log.errorf("Error matching route: %v", err)
continue
}
if n > 0 {
captures := make([]string, n - 1, context.temp_allocator)
for cap, i in try_captures[1:n] {
captures[i] = req.url.path[cap.byte_start:cap.byte_end]
}
req.url_params = captures
rh := route.handler
rh.handle(&rh, req, res)
return true
}
}
return false
}