Add routing
This commit is contained in:
parent
1368b7b69b
commit
4316fb2a73
7 changed files with 534 additions and 109 deletions
4
.gitmodules
vendored
4
.gitmodules
vendored
|
|
@ -1,4 +0,0 @@
|
||||||
[submodule "radix_tree"]
|
|
||||||
path = radix_tree
|
|
||||||
url = https://codeberg.org/hugomardbrink/odin-radixtree
|
|
||||||
update = none
|
|
||||||
100
README.md
100
README.md
|
|
@ -1 +1,99 @@
|
||||||
I'll get back to this when Odin is more mature, I've ran into countless compiler seg faults and can't be bothered to find out why anymore.
|
# HTTP 1/1 server library
|
||||||
|
|
||||||
|
## Routing rules
|
||||||
|
- Static first routing
|
||||||
|
- Path variables defined with `:foo`
|
||||||
|
- Example: `/odin/:version/download`
|
||||||
|
- Wild card routing not implemented
|
||||||
|
|
||||||
|
## Compile time user errors included
|
||||||
|
When defining the server you can pass a custom error type for the server.
|
||||||
|
All the handler function will, of course, need to return these in as a pair with a response.
|
||||||
|
|
||||||
|
## Memory allocation
|
||||||
|
The server will free the context temp allocators memory after the handler is run, thus, you are free
|
||||||
|
to use `context.temp_allocator` within your handlers and expect the memory to be freed after.
|
||||||
|
This becomes useful when you want to pass the response back to the server but is also ergonomic
|
||||||
|
if you find yourself in need of a temp allocator in your handlers.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```odin
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "core:log"
|
||||||
|
import "core:net"
|
||||||
|
import "core:strings"
|
||||||
|
import fjord "fjord"
|
||||||
|
import http "fjord/http"
|
||||||
|
|
||||||
|
// Handler for GET /hello/:name
|
||||||
|
hello_handler :: proc(request: ^http.Request) -> (http.Response, Error) {
|
||||||
|
name := request.path_variables["name"]
|
||||||
|
|
||||||
|
body := strings.concatenate(
|
||||||
|
{"<h1>Hello, ", name, "!</h1>"},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
response := http.make_response(
|
||||||
|
.Ok,
|
||||||
|
transmute([]byte)body,
|
||||||
|
.Html,
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler for GET /users/:id/posts/:post_id
|
||||||
|
user_post_handler :: proc(request: ^http.Request) -> (http.Response, Error) {
|
||||||
|
user_id := request.path_variables["id"]
|
||||||
|
post_id := request.path_variables["post_id"]
|
||||||
|
|
||||||
|
body := strings.concatenate(
|
||||||
|
{"<p>User ", user_id, " - Post ", post_id, "</p>"},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
response := http.make_response(
|
||||||
|
.Ok,
|
||||||
|
transmute([]byte)body,
|
||||||
|
.Html,
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
UserError :: enum {
|
||||||
|
DatabaseError,
|
||||||
|
ValidationError,
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: proc() {
|
||||||
|
context.logger = log.create_console_logger(.Info)
|
||||||
|
defer log.destroy_console_logger(context.logger)
|
||||||
|
|
||||||
|
endpoint := net.Endpoint {
|
||||||
|
address = net.IP4_Address{127, 0, 0, 1},
|
||||||
|
port = 8080,
|
||||||
|
}
|
||||||
|
|
||||||
|
server: fjord.Server(UserError)
|
||||||
|
fjord.server_init(&server, endpoint, context.allocator)
|
||||||
|
defer fjord.server_destroy(&server)
|
||||||
|
|
||||||
|
fjord.server_add_route(&server, .GET, {"hello", ":name"}, hello_handler)
|
||||||
|
fjord.server_add_route(&server, .GET, {"users", ":id", "posts", ":post_id"}, user_post_handler)
|
||||||
|
|
||||||
|
log.infof("Server listening on http://127.0.0.1:%d", endpoint.port)
|
||||||
|
fjord.listen_and_serve(&server)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dev
|
||||||
|
Run the test suite to check functionality, curl is a required dependancy for the test suite to work.
|
||||||
|
```bash
|
||||||
|
odin test .
|
||||||
|
```
|
||||||
|
|
|
||||||
230
fjord_test.odin
230
fjord_test.odin
|
|
@ -1,30 +1,75 @@
|
||||||
package fjord
|
package fjord
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:net"
|
import "core:net"
|
||||||
|
import "core:os/os2"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:thread"
|
||||||
|
import "core:time"
|
||||||
|
|
||||||
import "core:testing"
|
import "core:testing"
|
||||||
|
|
||||||
import http "http"
|
import http "http"
|
||||||
|
|
||||||
Error :: enum {}
|
Error :: enum {
|
||||||
|
TestError,
|
||||||
|
}
|
||||||
|
|
||||||
handler :: proc(request: ^http.Request) -> (http.Response, Error) {
|
first_handler :: proc(request: ^http.Request) -> (http.Response, Error) {
|
||||||
body := "<div>Hello</div>"
|
entity := request.path_variables["entity"]
|
||||||
response := http.make_response(.Ok, transmute([]byte)body, .Html)
|
|
||||||
|
body := strings.concatenate(
|
||||||
|
{"<div>Hello first", entity, "</div>"},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
response := http.make_response(
|
||||||
|
.Ok,
|
||||||
|
transmute([]byte)body,
|
||||||
|
.Html,
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
get_path_var_handler :: proc(request: ^http.Request) -> (http.Response, Error) {
|
second_handler :: proc(request: ^http.Request) -> (http.Response, Error) {
|
||||||
body := "<div>Hello</div>"
|
entity := request.path_variables["entity"]
|
||||||
response := http.make_response(.Ok, transmute([]byte)body, .Html)
|
|
||||||
|
body := strings.concatenate(
|
||||||
|
{"<div>Hello second", entity, "</div>"},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
response := http.make_response(
|
||||||
|
.Ok,
|
||||||
|
transmute([]byte)body,
|
||||||
|
.Html,
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
third_handler :: proc(request: ^http.Request) -> (http.Response, Error) {
|
||||||
|
entity := request.path_variables["entity"]
|
||||||
|
other_entity := request.path_variables["other_entity"]
|
||||||
|
|
||||||
|
body := strings.concatenate(
|
||||||
|
{"<div>Hello third", entity, "and", other_entity, "</div>"},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
response := http.make_response(
|
||||||
|
.Ok,
|
||||||
|
transmute([]byte)body,
|
||||||
|
.Html,
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_basic_ok :: proc(t: ^testing.T) {
|
test_router_ok :: proc(t: ^testing.T) {
|
||||||
context.logger = log.create_console_logger(.Info)
|
context.logger = log.create_console_logger(.Info)
|
||||||
defer log.destroy_console_logger(context.logger)
|
defer log.destroy_console_logger(context.logger)
|
||||||
|
|
||||||
|
|
@ -37,7 +82,170 @@ test_basic_ok :: proc(t: ^testing.T) {
|
||||||
server_init(&server, endpoint, context.allocator)
|
server_init(&server, endpoint, context.allocator)
|
||||||
defer server_destroy(&server)
|
defer server_destroy(&server)
|
||||||
|
|
||||||
server_add_handler(&server, .GET, {}, handler)
|
server_add_route(&server, .GET, {"hello", ":entity"}, first_handler)
|
||||||
server_add_handler(&server, .GET, {"hello", "{name-thing}"}, handler)
|
server_add_route(
|
||||||
listen_and_serve(&server)
|
&server,
|
||||||
|
.GET,
|
||||||
|
{"hello", ":entity", "only"},
|
||||||
|
second_handler,
|
||||||
|
)
|
||||||
|
server_add_route(
|
||||||
|
&server,
|
||||||
|
.GET,
|
||||||
|
{"hello", ":entity", "and", ":other_entity"},
|
||||||
|
third_handler,
|
||||||
|
)
|
||||||
|
|
||||||
|
handler, path_vars, ok := router_lookup(
|
||||||
|
&server.router,
|
||||||
|
.GET,
|
||||||
|
{"hello", "world"},
|
||||||
|
context.temp_allocator,
|
||||||
|
); assert(ok)
|
||||||
|
assert(handler == first_handler)
|
||||||
|
assert(path_vars["entity"] == "world")
|
||||||
|
|
||||||
|
handler, path_vars, ok = router_lookup(
|
||||||
|
&server.router,
|
||||||
|
.GET,
|
||||||
|
{"hello", "lonely world", "only"},
|
||||||
|
context.temp_allocator,
|
||||||
|
); assert(ok)
|
||||||
|
assert(handler == second_handler)
|
||||||
|
assert(path_vars["entity"] == "lonely world")
|
||||||
|
|
||||||
|
handler, path_vars, ok = router_lookup(
|
||||||
|
&server.router,
|
||||||
|
.GET,
|
||||||
|
{"hello", "world", "and", "worlds friend"},
|
||||||
|
context.temp_allocator,
|
||||||
|
); assert(ok)
|
||||||
|
assert(handler == third_handler)
|
||||||
|
assert(path_vars["entity"] == "world")
|
||||||
|
assert(path_vars["other_entity"] == "worlds friend")
|
||||||
|
|
||||||
|
ok = server_remove_route(&server, .GET, {"hello", ":entity"}); assert(ok)
|
||||||
|
ok = server_remove_route(&server, .GET, {"hello", ":entity"}); assert(!ok)
|
||||||
|
|
||||||
|
free_all(context.temp_allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
start_concurrent_server :: proc(
|
||||||
|
server: ^Server($Error_Type),
|
||||||
|
) -> ^thread.Thread {
|
||||||
|
ServerThreadData :: struct {
|
||||||
|
server: ^Server(Error_Type),
|
||||||
|
thread: ^thread.Thread,
|
||||||
|
}
|
||||||
|
|
||||||
|
server_thread_proc :: proc(t: ^thread.Thread) {
|
||||||
|
d := (^ServerThreadData)(t.data)
|
||||||
|
listen_and_serve(d.server)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := new(ServerThreadData, context.temp_allocator)
|
||||||
|
d.server = server
|
||||||
|
|
||||||
|
if d.thread = thread.create(server_thread_proc); d.thread != nil {
|
||||||
|
d.thread.init_context = context
|
||||||
|
d.thread.data = rawptr(d)
|
||||||
|
thread.start(d.thread)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give server some time to start
|
||||||
|
time.sleep(100 * time.Millisecond)
|
||||||
|
return d.thread
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown_concurrent_server :: proc(
|
||||||
|
server: ^Server($Error_Type),
|
||||||
|
t: ^thread.Thread,
|
||||||
|
) {
|
||||||
|
server_shutdown(server)
|
||||||
|
|
||||||
|
// Send a dummy request to unblock accept_tcp, todo: fix this immidiate server shutdown
|
||||||
|
_, _, _, err := os2.process_exec(
|
||||||
|
{command = []string{"curl", "-s", "http://127.0.0.1:8080/dummy"}},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
assert(err == nil)
|
||||||
|
|
||||||
|
thread.join(t)
|
||||||
|
thread.destroy(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_endpoint :: proc(url: string, expected_response: string) {
|
||||||
|
state, stdout, stderr, err := os2.process_exec(
|
||||||
|
{command = []string{"curl", "-s", url}},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.errorf("Failed to execute curl: %v", err)
|
||||||
|
assert(false, "curl execution failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.exit_code != 0 {
|
||||||
|
log.errorf("curl failed with exit code: %d", state.exit_code)
|
||||||
|
assert(false, "curl returned non-zero exit code")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := string(stdout)
|
||||||
|
log.assertf(
|
||||||
|
response == expected_response,
|
||||||
|
"Expected: %s, but got: %s",
|
||||||
|
response,
|
||||||
|
expected_response,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_server_general_ok :: proc(t: ^testing.T) {
|
||||||
|
context.logger = log.create_console_logger(.Info)
|
||||||
|
defer log.destroy_console_logger(context.logger)
|
||||||
|
|
||||||
|
endpoint := net.Endpoint {
|
||||||
|
address = net.IP4_Address{127, 0, 0, 1},
|
||||||
|
port = 8080,
|
||||||
|
}
|
||||||
|
|
||||||
|
server: Server(Error)
|
||||||
|
server_init(&server, endpoint, context.allocator)
|
||||||
|
defer server_destroy(&server)
|
||||||
|
|
||||||
|
server_add_route(&server, .GET, {"hello", ":entity"}, first_handler)
|
||||||
|
server_add_route(
|
||||||
|
&server,
|
||||||
|
.GET,
|
||||||
|
{"hello", ":entity", "only"},
|
||||||
|
second_handler,
|
||||||
|
)
|
||||||
|
server_add_route(
|
||||||
|
&server,
|
||||||
|
.GET,
|
||||||
|
{"hello", ":entity", "and", ":other_entity"},
|
||||||
|
third_handler,
|
||||||
|
)
|
||||||
|
|
||||||
|
t := start_concurrent_server(&server)
|
||||||
|
|
||||||
|
assert_endpoint(
|
||||||
|
"http://127.0.0.1:8080/hello/world",
|
||||||
|
"<div>Hello firstworld</div>",
|
||||||
|
)
|
||||||
|
assert_endpoint(
|
||||||
|
"http://127.0.0.1:8080/hello/lonely%20world/only",
|
||||||
|
"<div>Hello secondlonely%20world</div>",
|
||||||
|
)
|
||||||
|
assert_endpoint(
|
||||||
|
"http://127.0.0.1:8080/hello/world/and/worlds%20friend",
|
||||||
|
"<div>Hello thirdworldandworlds%20friend</div>",
|
||||||
|
)
|
||||||
|
|
||||||
|
shutdown_concurrent_server(&server, t)
|
||||||
|
|
||||||
|
free_all(context.temp_allocator)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,14 @@ status_text :: proc(status: Status) -> string {
|
||||||
unmarshall_request_line :: proc(
|
unmarshall_request_line :: proc(
|
||||||
request: ^Request,
|
request: ^Request,
|
||||||
reader: ^bufio.Reader,
|
reader: ^bufio.Reader,
|
||||||
|
allocator := context.temp_allocator
|
||||||
) -> RequestError {
|
) -> RequestError {
|
||||||
request_line, io_err := bufio.reader_read_slice(reader, '\n')
|
request_line, io_err := bufio.reader_read_slice(reader, '\n')
|
||||||
if io_err != nil do return .InvalidRequestLine
|
if io_err != nil do return .InvalidRequestLine
|
||||||
|
|
||||||
request_line = sanitize_byte_slice(request_line)
|
request_line = sanitize_byte_slice(request_line)
|
||||||
|
|
||||||
parts := bytes.split(request_line, {' '})
|
parts := bytes.split(request_line, {' '}, allocator)
|
||||||
if len(parts) != 3 do return .InvalidRequestLine
|
if len(parts) != 3 do return .InvalidRequestLine
|
||||||
|
|
||||||
request.method = method_from_str(string(parts[0]))
|
request.method = method_from_str(string(parts[0]))
|
||||||
|
|
@ -86,7 +87,7 @@ unmarshall_request_headers :: proc(
|
||||||
header_line = sanitize_byte_slice(header_line)
|
header_line = sanitize_byte_slice(header_line)
|
||||||
if len(header_line) == 0 do break
|
if len(header_line) == 0 do break
|
||||||
|
|
||||||
parts := bytes.split_n(header_line, {':'}, 2)
|
parts := bytes.split_n(header_line, {':'}, 2, allocator)
|
||||||
if len(parts) != 2 do return .InvalidHeaderFormat
|
if len(parts) != 2 do return .InvalidHeaderFormat
|
||||||
|
|
||||||
key := string(bytes.trim_space(parts[0]))
|
key := string(bytes.trim_space(parts[0]))
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 6fe8dc79ee3aa57f3a8b9a427bedf7bb24cfd809
|
|
||||||
184
router.odin
184
router.odin
|
|
@ -1,42 +1,188 @@
|
||||||
|
#+private package
|
||||||
package fjord
|
package fjord
|
||||||
|
|
||||||
|
import "base:runtime"
|
||||||
import "core:path/slashpath"
|
import "core:path/slashpath"
|
||||||
|
import "core:strings"
|
||||||
import http "http"
|
import http "http"
|
||||||
import rxt "radix_tree"
|
|
||||||
|
|
||||||
EndpointHandler :: struct($Error_Type: typeid) {
|
PathVariable :: struct {
|
||||||
method_handlers: map[http.Method]#type proc(
|
name: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
Static :: struct {}
|
||||||
|
|
||||||
|
RouterNodeType :: union {
|
||||||
|
Static,
|
||||||
|
PathVariable,
|
||||||
|
}
|
||||||
|
|
||||||
|
RouterNode :: struct($Error_Type: typeid) {
|
||||||
|
type: RouterNodeType,
|
||||||
|
handlers: map[http.Method]#type proc(request: ^http.Request) -> (http.Response, Error_Type),
|
||||||
|
children: map[string]^RouterNode(Error_Type),
|
||||||
|
}
|
||||||
|
|
||||||
|
Router :: struct($Error_Type: typeid) {
|
||||||
|
root_node: ^RouterNode(Error_Type),
|
||||||
|
allocator: runtime.Allocator,
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
router_node_init :: proc(
|
||||||
|
router: ^Router($Error_Type),
|
||||||
|
router_node: ^RouterNode(Error_Type),
|
||||||
|
maybe_path_var_name: Maybe(string),
|
||||||
|
) {
|
||||||
|
router_node.children = make(
|
||||||
|
map[string]^RouterNode(Error_Type),
|
||||||
|
router.allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
router_node.handlers = make(
|
||||||
|
map[http.Method]#type proc(
|
||||||
request: ^http.Request,
|
request: ^http.Request,
|
||||||
) -> (
|
) -> (
|
||||||
http.Response,
|
http.Response,
|
||||||
Error_Type,
|
Error_Type,
|
||||||
),
|
),
|
||||||
path_variables: []string,
|
router.allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
path_var_name, ok := maybe_path_var_name.?
|
||||||
|
if ok {
|
||||||
|
router_node.type = PathVariable {
|
||||||
|
name = path_var_name,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
router_node.type = Static{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Router :: struct {
|
router_init :: proc(
|
||||||
radix_tree: rxt.RadixTree(EndpointHandler),
|
router: ^Router($Error_Type),
|
||||||
|
allocator: runtime.Allocator,
|
||||||
|
) {
|
||||||
|
router^ = Router(Error_Type) {
|
||||||
|
root_node = new(RouterNode(Error_Type), allocator),
|
||||||
|
allocator = allocator,
|
||||||
|
}
|
||||||
|
|
||||||
|
router_node_init(router, router.root_node, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
router_init :: proc(router: ^Router, allocator := context.allocator) {
|
router_destroy :: proc(router: ^Router($Error_Type)) {
|
||||||
rxt.init(&router.radix_tree, allocator)
|
stack := make([dynamic]^RouterNode(Error_Type), router.allocator)
|
||||||
|
defer delete(stack)
|
||||||
|
|
||||||
|
append(&stack, router.root_node)
|
||||||
|
|
||||||
|
for len(stack) > 0 {
|
||||||
|
node := pop(&stack)
|
||||||
|
for _, child in node.children {
|
||||||
|
append(&stack, child)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(node.handlers)
|
||||||
|
delete(node.children)
|
||||||
|
free(node)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
router_destroy :: proc(router : ^Router) {
|
router_lookup :: proc(
|
||||||
rxt.destroy(&router.radix_tree)
|
router: ^Router($Error_Type),
|
||||||
|
method: http.Method,
|
||||||
|
path: []string,
|
||||||
|
allocator := context.temp_allocator,
|
||||||
|
) -> (
|
||||||
|
handler: #type proc(request: ^http.Request) -> (http.Response, Error_Type),
|
||||||
|
path_variables: map[string]string,
|
||||||
|
ok: bool,
|
||||||
|
) {
|
||||||
|
path_variables = make(map[string]string, allocator)
|
||||||
|
current_node := router.root_node
|
||||||
|
|
||||||
|
for segment_value in path {
|
||||||
|
if child, found := current_node.children[segment_value]; found {
|
||||||
|
current_node = child
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_path_child: Maybe(^RouterNode(Error_Type)) = nil
|
||||||
|
for name, child in current_node.children {
|
||||||
|
switch type in child.type {
|
||||||
|
case PathVariable:
|
||||||
|
maybe_path_child = child
|
||||||
|
path_variables[type.name] = segment_value
|
||||||
|
break
|
||||||
|
case Static:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if path_child, ok := maybe_path_child.?; ok {
|
||||||
|
current_node = path_child
|
||||||
|
} else {
|
||||||
|
return handler, path_variables, false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if handler, ok := current_node.handlers[method]; ok {
|
||||||
|
return handler, path_variables, true
|
||||||
|
} else {
|
||||||
|
return handler, path_variables, false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
router_lookup :: proc(router: ^Router, key: string) -> (endpoint_handler: EndpointHandler, ok: bool) {
|
router_add_route :: proc(
|
||||||
return rxt.lookup(&router.radix_tree, key)
|
router: ^Router($Error_Type),
|
||||||
|
method: http.Method,
|
||||||
|
path: []string,
|
||||||
|
handler: #type proc(request: ^http.Request) -> (http.Response, Error_Type),
|
||||||
|
) {
|
||||||
|
current_node := router.root_node
|
||||||
|
|
||||||
|
for segment_name in path {
|
||||||
|
node, ok := current_node.children[segment_name]
|
||||||
|
if !ok {
|
||||||
|
new_node := new(RouterNode(Error_Type))
|
||||||
|
|
||||||
|
if strings.has_prefix(segment_name, ":") {
|
||||||
|
router_node_init(router, new_node, segment_name[1:])
|
||||||
|
} else {
|
||||||
|
router_node_init(router, new_node, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
current_node.children[segment_name] = new_node
|
||||||
|
current_node = new_node
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
current_node = node
|
||||||
|
}
|
||||||
|
|
||||||
|
current_node.handlers[method] = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
router_add_route :: proc(router: ^Router, key: []string, value: EndpointHandler) {
|
router_remove_route :: proc(
|
||||||
joined_path := slashpath.join(key, context.allocator)
|
router: ^Router($Error_Type),
|
||||||
defer delete(joined_path)
|
method: http.Method,
|
||||||
|
path: []string,
|
||||||
|
) -> (
|
||||||
|
ok: bool,
|
||||||
|
) {
|
||||||
|
current_node := router.root_node
|
||||||
|
|
||||||
rxt.insert(&router.radix_tree, joined_path[:], value)
|
for segment_name in path {
|
||||||
}
|
node, ok := current_node.children[segment_name]
|
||||||
|
if !ok do return false
|
||||||
|
current_node = node
|
||||||
|
}
|
||||||
|
|
||||||
router_remove_route :: proc(router: ^Router, key: string) -> (ok: bool){
|
if method not_in current_node.handlers {
|
||||||
return rxt.remove(&router.radix_tree, key)
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_key(¤t_node.handlers, method)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
111
server.odin
111
server.odin
|
|
@ -8,19 +8,21 @@ import "core:net"
|
||||||
import "core:path/slashpath"
|
import "core:path/slashpath"
|
||||||
import "core:strconv"
|
import "core:strconv"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
import "core:sync"
|
||||||
import "core:unicode"
|
import "core:unicode"
|
||||||
|
|
||||||
import http "http"
|
import http "http"
|
||||||
|
|
||||||
Server :: struct($Error_Type: typeid) {
|
Server :: struct($Error_Type: typeid) {
|
||||||
endpoint: net.Endpoint,
|
endpoint: net.Endpoint,
|
||||||
router: Router(EndpointHandler(Error_Type)),
|
|
||||||
not_found_handler: #type proc(
|
not_found_handler: #type proc(
|
||||||
request: ^http.Request,
|
request: ^http.Request,
|
||||||
) -> (
|
) -> (
|
||||||
http.Response,
|
http.Response,
|
||||||
Error_Type,
|
Error_Type,
|
||||||
),
|
),
|
||||||
|
router: Router(Error_Type),
|
||||||
|
shutdown: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private)
|
@(private)
|
||||||
|
|
@ -75,62 +77,16 @@ server_init :: proc(
|
||||||
)
|
)
|
||||||
return http_response, nil
|
return http_response, nil
|
||||||
},
|
},
|
||||||
|
shutdown = false,
|
||||||
}
|
}
|
||||||
|
router_init(&server.router, allocator)
|
||||||
router_init(&server.router)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server_destroy :: proc(server: ^Server($Error_Type)) {
|
server_destroy :: proc(server: ^Server($Error_Type)) {
|
||||||
router_destroy(&server.router)
|
router_destroy(&server.router)
|
||||||
}
|
}
|
||||||
|
|
||||||
server_add_handler :: proc(
|
// This is used instead of default server_make param until compiler bug is fixed: https://github.com/odin-lang/Odin/issues/5792
|
||||||
server: ^Server($Error_Type),
|
|
||||||
method: http.Method,
|
|
||||||
path: []string,
|
|
||||||
handle_proc: #type proc(
|
|
||||||
request: ^http.Request,
|
|
||||||
) -> (
|
|
||||||
http.Response,
|
|
||||||
Error_Type,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
path_variables := make([dynamic]string, context.allocator)
|
|
||||||
defer delete(path_variables)
|
|
||||||
|
|
||||||
for seg in path {
|
|
||||||
is_path_var :=
|
|
||||||
strings.has_prefix(seg, "{") && strings.has_suffix(seg, "}")
|
|
||||||
|
|
||||||
if is_path_var {
|
|
||||||
append(&path_variables, seg[1:len(seg) - 1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
joined_path := slashpath.join(path, context.allocator)
|
|
||||||
defer delete(joined_path)
|
|
||||||
|
|
||||||
if joined_path == "" {
|
|
||||||
router_add_route(
|
|
||||||
&server.router,
|
|
||||||
"/",
|
|
||||||
EndpointHandler(Error_Type){method},
|
|
||||||
)
|
|
||||||
server.handlers["/"] = EndpointHandler(Error_Type) {
|
|
||||||
method,
|
|
||||||
handle_proc,
|
|
||||||
path_variables[:],
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
server.handlers[joined_path] = Handler(Error_Type) {
|
|
||||||
method,
|
|
||||||
handle_proc,
|
|
||||||
path_variables[:],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is used instead of server_make param until compiler bug is fixed: https://github.com/odin-lang/Odin/issues/5792
|
|
||||||
server_set_not_found_handler :: proc(
|
server_set_not_found_handler :: proc(
|
||||||
server: ^Server($Error_Type),
|
server: ^Server($Error_Type),
|
||||||
not_found_handler: #type proc(
|
not_found_handler: #type proc(
|
||||||
|
|
@ -143,22 +99,27 @@ server_set_not_found_handler :: proc(
|
||||||
server.not_found_handler = not_found_handler
|
server.not_found_handler = not_found_handler
|
||||||
}
|
}
|
||||||
|
|
||||||
match_handler_pattern :: proc(
|
server_add_route :: proc(
|
||||||
server: ^Server($Error_Type),
|
server: ^Server($Error_Type),
|
||||||
method: http.Method,
|
method: http.Method,
|
||||||
path: string,
|
path: []string,
|
||||||
allocator := context.allocator,
|
|
||||||
) -> (
|
|
||||||
handler: #type proc(request: ^http.Request) -> (http.Response, Error_Type),
|
handler: #type proc(request: ^http.Request) -> (http.Response, Error_Type),
|
||||||
path_variables: map[string]string,
|
|
||||||
) {
|
) {
|
||||||
path_variables = make(map[string]string, allocator)
|
router_add_route(&server.router, method, path, handler)
|
||||||
segments := strings.split(path, "/", context.allocator)
|
}
|
||||||
defer delete(segments)
|
|
||||||
|
|
||||||
handler = server.handlers[identifier].procedure
|
server_remove_route :: proc(
|
||||||
|
server: ^Server($Error_Type),
|
||||||
|
method: http.Method,
|
||||||
|
path: []string,
|
||||||
|
) -> (
|
||||||
|
ok: bool,
|
||||||
|
) {
|
||||||
|
return router_remove_route(&server.router, method, path)
|
||||||
|
}
|
||||||
|
|
||||||
return handler, path_variables
|
server_shutdown :: proc(server: ^Server($Error_Type)) {
|
||||||
|
sync.atomic_store(&server.shutdown, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
listen_and_serve :: proc(server: ^Server($Error_Type)) {
|
listen_and_serve :: proc(server: ^Server($Error_Type)) {
|
||||||
|
|
@ -166,7 +127,7 @@ listen_and_serve :: proc(server: ^Server($Error_Type)) {
|
||||||
log.assert(net_err == nil, "Couldn't create TCP socket")
|
log.assert(net_err == nil, "Couldn't create TCP socket")
|
||||||
defer net.close(server_socket)
|
defer net.close(server_socket)
|
||||||
|
|
||||||
for {
|
for !sync.atomic_load(&server.shutdown) {
|
||||||
client_socket, source, net_err := net.accept_tcp(server_socket)
|
client_socket, source, net_err := net.accept_tcp(server_socket)
|
||||||
if net_err != nil {
|
if net_err != nil {
|
||||||
log.warnf("Failed to accept TCP connection, reason: %s", net_err)
|
log.warnf("Failed to accept TCP connection, reason: %s", net_err)
|
||||||
|
|
@ -201,13 +162,29 @@ listen_and_serve :: proc(server: ^Server($Error_Type)) {
|
||||||
http.Response,
|
http.Response,
|
||||||
Error_Type,
|
Error_Type,
|
||||||
)
|
)
|
||||||
handler, http_request.path_variables = match_handler_pattern(
|
ok: bool
|
||||||
server,
|
|
||||||
http_request.method,
|
|
||||||
http_request.path,
|
|
||||||
)
|
|
||||||
|
|
||||||
http_response, err := handler(&http_request)
|
// todo: should sanitize better
|
||||||
|
path := strings.split(
|
||||||
|
http_request.path,
|
||||||
|
"/",
|
||||||
|
context.temp_allocator,
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
handler, http_request.path_variables, ok = router_lookup(
|
||||||
|
&server.router,
|
||||||
|
http_request.method,
|
||||||
|
path,
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
if !ok do handler = server.not_found_handler
|
||||||
|
|
||||||
|
http_response, handler_err := handler(&http_request)
|
||||||
|
|
||||||
|
if handler_err != nil {
|
||||||
|
log.warnf("Handler failed with error: %s", handler_err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
marshalled_response := http.marshall_response(
|
marshalled_response := http.marshall_response(
|
||||||
&http_response,
|
&http_response,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue