🍯 Glaze

package router

import "core:fmt"
import "core:os"
import "core:slice"
import "core:strings"

import http "../"

Route_Kind :: enum {
	Exact,
	Prefix,
	Pattern,
}

Route :: struct {
	method:      http.Method,
	kind:        Route_Kind,
	path:        string,
	handler:     Route_Handler,
	user_data:   rawptr,
	middlewares: []Middleware, // owned by the router, runs after the global middlewares
}

Middleware :: struct {
	handler:   Middleware_Handler,
	user_data: rawptr,
}

Chain_Node :: struct {
	middleware:     Middleware_Handler,
	middleware_ctx: rawptr,
	next:           Route_Handler,
	next_ctx:       rawptr,
}

Router :: struct {
	routes:      [dynamic]Route,
	middlewares: [dynamic]Middleware,
	statics:     [dynamic]^Static_Context,
	mime_map:    map[string]string,
}

Request :: struct {
	using _http_req: http.Request,
	rest_path:       string,
	params:          map[string]string,
}

Response :: http.Response

Route_Handler :: proc(req: Request, res: ^Response, ctx: rawptr)

// A pattern is a path of segments: `name` matches itself, `:name` matches any one
// non-empty segment and a trailing `*name` matches whatever is left, which can be
// nothing at all (`/files/*path` matches `/files` and `/files/` as well as `/files/a/b`)
@(private)
Pattern_Kind :: enum {
	Static,
	Param,
	Wildcard,
}

@(private)
segment_kind :: proc(segment: string) -> Pattern_Kind {
	if len(segment) > 0 {
		switch segment[0] {
		case ':':
			return .Param
		case '*':
			return .Wildcard
		}
	}
	return .Static
}

// Empty when the pattern is fine, otherwise what is wrong with it
@(private)
pattern_error :: proc(path: string) -> string {
	if len(path) == 0 || path[0] != '/' {
		return "pattern must start with a slash"
	}

	segments := strings.split(path[1:], "/")
	defer delete(segments)
	for segment, i in segments {
		kind := segment_kind(segment)
		if kind != .Static && len(segment) == 1 {
			return "params and wildcards need a name"
		}
		if kind == .Wildcard && i != len(segments) - 1 {
			return "wildcard must be last"
		}
	}
	return ""
}

Middleware_Handler :: proc(req: Request, res: ^Response, ctx: rawptr, next: Route_Handler, next_ctx: rawptr)

create :: proc() -> Router {
	r: Router
	r.routes = make([dynamic]Route, 0, 8)
	r.middlewares = make([dynamic]Middleware, 0, 8)
	r.statics = make([dynamic]^Static_Context, 0, 2)

	// TODO: break out into dedicated file to enable clean imports
	// Might aswell expose mime handling
	r.mime_map = make(map[string]string)
	r.mime_map[".html"] = "text/html; charset=utf-8"
	r.mime_map[".css"] = "text/css; charset=utf-8"
	r.mime_map[".js"] = "application/javascript; charset=utf-8"
	r.mime_map[".json"] = "application/json; charset=utf-8"
	r.mime_map[".svg"] = "image/svg+xml"
	r.mime_map[".png"] = "image/png"
	r.mime_map[".jpg"] = "image/jpeg"
	r.mime_map[".jpeg"] = "image/jpeg"
	r.mime_map[".webp"] = "image/webp"
	r.mime_map[".gif"] = "image/gif"
	r.mime_map[".ico"] = "image/x-icon"
	r.mime_map[".txt"] = "text/plain; charset=utf-8"

	return r
}

destroy :: proc(r: ^Router) {
	for s in r.statics {
		delete(s.root)
		delete(s.base_path)
		free(s)
	}
	delete(r.statics)
	for rt in r.routes {
		delete(rt.middlewares)
	}
	delete(r.mime_map)
	delete(r.middlewares)
	delete(r.routes)
}

// Returns false, and doesn't add the route, if `path` is not a valid pattern
add :: proc(
	r: ^Router,
	method: http.Method,
	path: string,
	h: Route_Handler,
	kind: Route_Kind = .Exact,
	user_data: rawptr,
	middlewares: []Middleware = nil,
) -> bool {
	if kind == .Pattern {
		if msg := pattern_error(path); msg != "" {
			fmt.eprintf("router: %s -> %s\n", msg, path)
			return false
		}
	}

	rt := Route {
		method    = method,
		kind      = kind,
		path      = path,
		handler   = h,
		user_data = user_data,
	}
	if len(middlewares) > 0 {
		rt.middlewares = slice.clone(middlewares)
	}
	append(&r.routes, rt)
	return true
}

get :: proc(
	r: ^Router,
	path: string,
	h: Route_Handler,
	kind: Route_Kind = .Exact,
	user_data: rawptr = nil,
	middlewares: []Middleware = nil,
) -> bool {
	return add(r, .Get, path, h, kind, user_data, middlewares)
}

post :: proc(
	r: ^Router,
	path: string,
	h: Route_Handler,
	kind: Route_Kind = .Exact,
	user_data: rawptr = nil,
	middlewares: []Middleware = nil,
) -> bool {
	return add(r, .Post, path, h, kind, user_data, middlewares)
}

// Global middleware, runs for every route regardless of when the route was added.
// Middlewares run in the order they are added, first added is outermost:
// global (in `use` order) -> route (in `middlewares` order) -> handler
use :: proc(r: ^Router, h: Middleware_Handler, user_data: rawptr = nil) {
	append(&r.middlewares, Middleware{handler = h, user_data = user_data})
}

@(private)
Static_Context :: struct {
	root:      string,
	base_path: string,
	mime_map:  ^map[string]string, // the router's map, so later additions are seen
}

// Returns false, and serves nothing, if `filepath` isn't a directory that can be resolved
// NOTE: `r` must not be moved after this, the route keeps a pointer into it
static :: proc(r: ^Router, path, filepath: string, middlewares: []Middleware = nil) -> bool {
	root, err := os.get_absolute_path(filepath, context.allocator)
	if err != nil || !os.is_dir(root) {
		// Files are checked against the root, an empty one would let requests reach anywhere
		fmt.eprintf("router: static root isn't a directory -> %s\n", filepath)
		delete(root)
		return false
	}

	ctx := new(Static_Context)
	ctx.root = root
	ctx.base_path = strings.clone(path)
	ctx.mime_map = &r.mime_map
	append(&r.statics, ctx) // freed by `destroy`

	return add(r, .Get, path, static_handler, .Prefix, ctx, middlewares)
}

handle :: proc(r: ^Router) -> (http.Handler, rawptr) {
	return proc(req: http.Request, res: ^http.Response, ctx: rawptr) {
			router_handler(Request{_http_req = req}, res, ctx)
		}, r
}

// Returns a path the caller has to free, which is a file below `root`
verify_and_get_absolute_filepath :: proc(root, target: string) -> (string, bool) {
	// Would cut the path short once it reaches the OS
	if strings.contains_rune(target, 0) {
		return "", false
	}

	raw_file_path, requested_file: string
	err: os.Error

	raw_file_path, err = os.join_path({root, target}, context.allocator)
	if err != nil {
		return "", false
	}
	defer delete(raw_file_path)

	requested_file, err = os.get_absolute_path(raw_file_path, context.allocator)
	if err != nil {
		return "", false
	}

	if !is_within(root, requested_file) || !os.is_file(requested_file) {
		delete(requested_file)
		return "", false
	}

	return requested_file, true
}

// Below `root` as a path, sharing its leading characters isn't enough (/srv/www vs /srv/www-secret)
@(private)
is_within :: proc(root, path: string) -> bool {
	if !strings.has_prefix(path, root) || len(path) == len(root) {
		return false
	}
	return strings.has_suffix(root, "/") || path[len(root)] == '/'
}

@(private)
static_handler :: proc(req: Request, res: ^Response, ctx: rawptr) {
	static_context := (^Static_Context)(ctx)

	if len(req.target) <= len(static_context.base_path) + 1 {
		http.respond(res, .NotFound, nil)
		return
	}

	target := string(req.target[len(static_context.base_path) + 1:])
	requested_file, ok := verify_and_get_absolute_filepath(static_context.root, target)
	if !ok {
		http.respond(res, .NotFound, nil)
		return
	}
	defer delete(requested_file)

	ext := os.ext(requested_file)
	mime: string
	mime, ok = static_context.mime_map[ext]
	if !ok {
		mime = "application/octet-stream"
	}

	http.headers_add(&res.headers, "content-type", mime)
	http.headers_add(&res.headers, "cache-control", "public, max-age=3600")

	// TODO: don't read complete file into memory, stream/chunk it
	buf, err := os.read_entire_file_from_path(requested_file, context.allocator)
	if err != nil {
		fmt.eprintf("couldn't read file: %v\n", err)
		http.respond(res, .InternalServerError, nil)
		return
	}

	http.respond_owned(res, .Ok, buf)
}

// A chain is one `Chain_Node` per middleware, linked outermost first and ending in the
// route handler. The nodes live in storage owned by `dispatch` and are never freed one
// by one, so a middleware that returns without calling `next` can't leak the rest.
@(private)
chain_node_handler :: proc(req: Request, res: ^Response, ctx: rawptr) {
	n := (^Chain_Node)(ctx)
	n.middleware(req, res, n.middleware_ctx, n.next, n.next_ctx)
}

// Fills `nodes` (one per middleware, global ones first) and returns the chain's entry point
@(private)
build_chain :: proc(nodes: []Chain_Node, rt: Route, global: []Middleware) -> (Route_Handler, rawptr) {
	assert(len(nodes) == len(global) + len(rt.middlewares))

	curr_handler := rt.handler
	curr_ctx := rt.user_data
	i := len(nodes)
	// Innermost first, so every node can point at the one below it
	for layer in ([2][]Middleware{rt.middlewares, global}) {
		#reverse for m in layer {
			i -= 1
			nodes[i] = Chain_Node {
				middleware     = m.handler,
				middleware_ctx = m.user_data,
				next           = curr_handler,
				next_ctx       = curr_ctx,
			}
			curr_handler = chain_node_handler
			curr_ctx = &nodes[i]
		}
	}
	return curr_handler, curr_ctx
}

@(private)
dispatch :: proc(r: ^Router, rt: Route, req: Request, res: ^Response) {
	count := len(r.middlewares) + len(rt.middlewares)

	// Typical chains fit on the stack, only unusually long ones touch the heap
	stack: [8]Chain_Node
	nodes := stack[:count] if count <= len(stack) else make([]Chain_Node, count)
	defer if count > len(stack) {
		delete(nodes)
	}

	handler, ctx := build_chain(nodes, rt, r.middlewares[:])
	handler(req, res, ctx)
}

@(private)
router_handler :: proc(req: Request, res: ^Response, user_data: rawptr) {
	r := (^Router)(user_data)

	req := req
	req.params = make(map[string]string)
	defer delete(req.params)

	for rt in r.routes {
		if rt.kind != .Exact {
			continue
		}

		if rt.method == req.method && rt.path == req.target {
			dispatch(r, rt, req, res)
			return
		}
	}

	best_i := -1
	best_len := -1
	for rt, i in r.routes {
		if rt.kind != .Prefix {
			continue
		}
		if rt.method != req.method {
			// TODO: Support 405 in the future
			continue
		}
		if strings.has_prefix(req.target, rt.path) {
			path_len := len(rt.path)
			if path_len > best_len {
				best_len = path_len
				best_i = i
				req.rest_path = req.target[len(rt.path):]
			}
		}
	}
	if best_i >= 0 {
		dispatch(r, r.routes[best_i], req, res)
		return
	}

	// Patterns match whole segments, so the target has to be a path
	if len(req.target) == 0 || req.target[0] != '/' {
		http.respond(res, .NotFound, nil)
		return
	}

	// TODO: rewrite logic to parse over windows of values instead of splitting
	target_parts := strings.split(req.target[1:], "/")
	defer delete(target_parts)
	Pattern_Elem :: struct {
		kind:  Pattern_Kind,
		name:  string,
		value: string,
	}
	best_pattern: []Pattern_Elem
	for rt, i in r.routes {
		if rt.kind != .Pattern {
			continue
		}
		if rt.method != req.method {
			// TODO: Support 405 in the future
			continue
		}

		// TODO: Do all this work at route setup

		// NOTE: Skip initial slash, `add` made sure there is one
		pattern_parts := strings.split(rt.path[1:], "/")
		defer delete(pattern_parts)

		// Only a trailing wildcard can match a different number of segments than the pattern has
		if segment_kind(pattern_parts[len(pattern_parts) - 1]) == .Wildcard {
			if len(target_parts) < len(pattern_parts) - 1 {
				continue
			}
		} else if len(target_parts) != len(pattern_parts) {
			continue
		}

		pattern := make([]Pattern_Elem, len(pattern_parts))

		for p, j in pattern_parts {
			pattern[j].kind = segment_kind(p)
			pattern[j].name = p if pattern[j].kind == .Static else p[1:]
		}

		skip: bool
		for p, j in pattern {
			switch p.kind {
			case .Static:
				skip ||= target_parts[j] != p.name
			case .Param:
				// A param has to match something
				skip ||= target_parts[j] == ""
			case .Wildcard:
			}
		}
		if skip {
			delete(pattern)
			continue
		}

		if len(pattern) > len(best_pattern) {
			best_i = i
			if best_pattern != nil {
				delete(best_pattern)
			}
			best_pattern = pattern
		} else {
			delete(pattern)
		}
	}
	if best_i >= 0 {
		for p, i in best_pattern {
			#partial switch p.kind {
			case .Param:
				req.params[p.name] = target_parts[i]
			case .Wildcard:
				// Everything from this segment on, if the target even gets that far. The parts
				// are slices of the target, so their lengths (and the slashes between them)
				// say where it starts.
				offset := 1
				for part in target_parts[:min(i, len(target_parts))] {
					offset += len(part) + 1
				}
				req.params[p.name] = req.target[min(offset, len(req.target)):]
			}
		}
		dispatch(r, r.routes[best_i], req, res)
		delete(best_pattern)
		return
	}

	// TODO: Have a 404 handler
	http.respond(res, .NotFound, nil)
}