Skip to content

Commit

Permalink
feat: nested routers initial pass
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnCoene committed Oct 9, 2024
1 parent e4fdfd2 commit 18c3470
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 96 deletions.
16 changes: 16 additions & 0 deletions .lintr
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
linters: linters_with_defaults(
line_length_linter(120),
trailing_whitespace_linter = NULL,
commented_code_linter = NULL,
function_left_parentheses_linter = NULL,
spaces_left_parentheses_linter = NULL,
paren_body_linter = NULL,
brace_linter = NULL,
indentation_linter(
indent = 2L,
hanging_indent_style = "never"
),
object_usage_linter = NULL, # this uses eval()
object_name_linter = NULL
)
encoding: "UTF-8"
5 changes: 4 additions & 1 deletion R/ambiorix.R
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,10 @@ Ambiorix <- R6::R6Class(
if(is.null(host))
host <- private$.host

super$reorder_routes()
super$prepare()
private$.routes <- super$get_routes()
private$.receivers <- super$get_receivers()
private$.middleware <- super$get_middleware()

private$.server <- httpuv::startServer(
host = host,
Expand Down
239 changes: 144 additions & 95 deletions R/routing.R
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ Routing <- R6::R6Class(

invisible(self)
},
#' @details PUT Method
#'
#' Add routes to listen to.
#'
#' @param path Route to listen to, `:` defines a parameter.
#' @param handler Function that accepts the request and returns an object
#' describing an httpuv response, e.g.: [response()].
#' @param error Handler function to run on error.
#' @details PUT Method
#'
#' Add routes to listen to.
#'
#' @param path Route to listen to, `:` defines a parameter.
#' @param handler Function that accepts the request and returns an object
#' describing an httpuv response, e.g.: [response()].
#' @param error Handler function to run on error.
put = function(path, handler, error = NULL){
assert_that(valid_path(path))
assert_that(not_missing(handler))
Expand All @@ -74,14 +74,14 @@ Routing <- R6::R6Class(

invisible(self)
},
#' @details PATCH Method
#'
#' Add routes to listen to.
#'
#' @param path Route to listen to, `:` defines a parameter.
#' @param handler Function that accepts the request and returns an object
#' describing an httpuv response, e.g.: [response()].
#' @param error Handler function to run on error.
#' @details PATCH Method
#'
#' Add routes to listen to.
#'
#' @param path Route to listen to, `:` defines a parameter.
#' @param handler Function that accepts the request and returns an object
#' describing an httpuv response, e.g.: [response()].
#' @param error Handler function to run on error.
patch = function(path, handler, error = NULL){
assert_that(valid_path(path))
assert_that(not_missing(handler))
Expand All @@ -98,14 +98,14 @@ Routing <- R6::R6Class(

invisible(self)
},
#' @details DELETE Method
#'
#' Add routes to listen to.
#'
#' @param path Route to listen to, `:` defines a parameter.
#' @param handler Function that accepts the request and returns an object
#' describing an httpuv response, e.g.: [response()].
#' @param error Handler function to run on error.
#' @details DELETE Method
#'
#' Add routes to listen to.
#'
#' @param path Route to listen to, `:` defines a parameter.
#' @param handler Function that accepts the request and returns an object
#' describing an httpuv response, e.g.: [response()].
#' @param error Handler function to run on error.
delete = function(path, handler, error = NULL){
assert_that(valid_path(path))
assert_that(not_missing(handler))
Expand All @@ -122,14 +122,14 @@ Routing <- R6::R6Class(

invisible(self)
},
#' @details POST Method
#'
#' Add routes to listen to.
#'
#' @param path Route to listen to.
#' @param handler Function that accepts the request and returns an object
#' describing an httpuv response, e.g.: [response()].
#' @param error Handler function to run on error.
#' @details POST Method
#'
#' Add routes to listen to.
#'
#' @param path Route to listen to.
#' @param handler Function that accepts the request and returns an object
#' describing an httpuv response, e.g.: [response()].
#' @param error Handler function to run on error.
post = function(path, handler, error = NULL){
assert_that(valid_path(path))
assert_that(not_missing(handler))
Expand All @@ -146,14 +146,14 @@ Routing <- R6::R6Class(

invisible(self)
},
#' @details OPTIONS Method
#'
#' Add routes to listen to.
#'
#' @param path Route to listen to.
#' @param handler Function that accepts the request and returns an object
#' describing an httpuv response, e.g.: [response()].
#' @param error Handler function to run on error.
#' @details OPTIONS Method
#'
#' Add routes to listen to.
#'
#' @param path Route to listen to.
#' @param handler Function that accepts the request and returns an object
#' describing an httpuv response, e.g.: [response()].
#' @param error Handler function to run on error.
options = function(path, handler, error = NULL){
assert_that(valid_path(path))
assert_that(not_missing(handler))
Expand All @@ -170,14 +170,14 @@ Routing <- R6::R6Class(

invisible(self)
},
#' @details All Methods
#'
#' Add routes to listen to for all methods `GET`, `POST`, `PUT`, `DELETE`, and `PATCH`.
#'
#' @param path Route to listen to.
#' @param handler Function that accepts the request and returns an object
#' describing an httpuv response, e.g.: [response()].
#' @param error Handler function to run on error.
#' @details All Methods
#'
#' Add routes to listen to for all methods `GET`, `POST`, `PUT`, `DELETE`, and `PATCH`.
#'
#' @param path Route to listen to.
#' @param handler Function that accepts the request and returns an object
#' describing an httpuv response, e.g.: [response()].
#' @param error Handler function to run on error.
all = function(path, handler, error = NULL){
assert_that(valid_path(path))
assert_that(not_missing(handler))
Expand All @@ -194,26 +194,26 @@ Routing <- R6::R6Class(

invisible(self)
},
#' @details Receive Websocket Message
#' @param name Name of message.
#' @param handler Function to run when message is received.
#'
#' @examples
#' app <- Ambiorix$new()
#'
#' app$get("/", function(req, res){
#' res$send("Using {ambiorix}!")
#' })
#'
#' app$receive("hello", function(msg, ws){
#' print(msg) # print msg received
#'
#' # send a message back
#' ws$send("hello", "Hello back! (sent from R)")
#' })
#'
#' if(interactive())
#' app$start()
#' @details Receive Websocket Message
#' @param name Name of message.
#' @param handler Function to run when message is received.
#'
#' @examples
#' app <- Ambiorix$new()
#'
#' app$get("/", function(req, res){
#' res$send("Using {ambiorix}!")
#' })
#'
#' app$receive("hello", function(msg, ws){
#' print(msg) # print msg received
#'
#' # send a message back
#' ws$send("hello", "Hello back! (sent from R)")
#' })
#'
#' if(interactive())
#' app$start()
receive = function(name, handler){
private$.receivers <- append(
private$.receivers,
Expand All @@ -222,7 +222,7 @@ Routing <- R6::R6Class(

invisible(self)
},
#' @details Print
#' @details Print
print = function(){
cli::cli_rule("Ambiorix", right = "web server")
cli::cli_li("routes: {.val {private$n_routes()}}")
Expand All @@ -235,13 +235,13 @@ Routing <- R6::R6Class(
self$use(engine)
invisible(self)
},
#' @details Use a router or middleware
#' @param use Either a router as returned by [Router], a function to use as middleware,
#' or a `list` of functions.
#' If a function is passed, it must accept two arguments (the request, and the response):
#' this function will be executed every time the server receives a request.
#' _Middleware may but does not have to return a response, unlike other methods such as `get`_
#' Note that multiple routers and middlewares can be used.
#' @details Use a router or middleware
#' @param use Either a router as returned by [Router], a function to use as middleware,
#' or a `list` of functions.
#' If a function is passed, it must accept two arguments (the request, and the response):
#' this function will be executed every time the server receives a request.
#' _Middleware may but does not have to return a response, unlike other methods such as `get`_
#' Note that multiple routers and middlewares can be used.
use = function(use){
assert_that(not_missing(use))

Expand All @@ -254,9 +254,8 @@ Routing <- R6::R6Class(

# mount router
if(inherits(use, "Router")){
private$.routes <- append(private$.routes, use$get_routes())
private$.receivers <- append(private$.receivers, use$get_receivers())
private$.middleware <- append(private$.middleware, use$get_middleware())
use$add_basepath(private$.basepath)
private$.routers <- append(private$.routers, use)
}

if(is_renderer_obj(use) && private$.is_router){
Expand Down Expand Up @@ -325,20 +324,71 @@ Routing <- R6::R6Class(

invisible(self)
},
#' @details Get the routes
get_routes = function(){
return(private$.routes)
#' @details Get the routes
get_routes = function(routes = list()){
routes <- append(routes, private$.routes)

if(!length(private$.routers)) return(routes)

for(router in private$.routers) {
routes <- router$get_routes(routes)
}

return(routes)
},
#' @details Get the receivers
get_receivers = function(receivers = list()){
receivers <- append(receivers, private$.receivers)

if(!length(private$.receivers)) return(receivers)

for(receiver in private$.receivers) {
receivers <- router$get_receivers(receivers)
}

return(receivers)
},
#' @details Get the receivers
get_receivers = function(){
return(private$.receivers)
#' @details Get the middleware
get_middleware = function(middlewares = list()){
middlewares <- append(middlewares, private$.middlewares)

if(!length(private$.middleware)) return(middlewares)

for(middleware in private$.middleware) {
middlewares <- router$get_middleware(middlewares)
}

return(middlewares)
},
#' @details Get the middleware
get_middleware = function(){
return(private$.middleware)
add_basepath = function(parent) {
if(missing(parent))
stop("missing parent")

private$.basepath <- paste0(parent, private$.basepath)
invisible(self)
},
prepare = function() {
for(route in private$.routes) {
route$route$as_pattern()
}

private$reorder_routes()
if(!length(private$.routers)) return()

for(route in private$.routers) {
route$prepare()
}
}
),
active = list(
basepath = function(path) {
if(!missing(path)){
private$.basepath <- path
return(path)
}

invisible(private$.basepath)
},
websocket = function(ws){
if(missing(ws) && !is.null(private$.wss_custom))
return(private$.wss_custom)
Expand All @@ -359,6 +409,7 @@ Routing <- R6::R6Class(
.middleware = list(),
.is_running = FALSE,
.wss_custom = NULL,
.routers = list(),
# we reorder the routes before launching the app
# we make sure the longest patterns are checked first
# this makes sure /:id/x matches BEFORE /:id does
Expand All @@ -367,9 +418,6 @@ Routing <- R6::R6Class(
# e.g. /hello should be matched before /:id
# TODO https://github.com/devOpifex/ambiorix/issues/47
reorder_routes = function() {
if(length(private$.routes) < 3L)
return()

indices <- seq_along(private$.routes)
pats <- lapply(private$.routes, \(route) {
data.frame(
Expand All @@ -378,12 +426,12 @@ Routing <- R6::R6Class(
)
})
df <- do.call(rbind, pats)
df$order <- 1:nrow(df)
df$order <- seq_len(nrow(df))
df$nchar <- nchar(df$pattern)
df <- df[order(df$dynamic, -df$nchar), ]

new_routes <- as.list(c(1:nrow(df)))
for(i in 1:nrow(df)) {
new_routes <- as.list(c(seq_len(nrow(df))))
for(i in seq_len(nrow(df))) {
new_routes[[i]] <- private$.routes[[df$order[i]]]
}

Expand All @@ -410,7 +458,8 @@ Routing <- R6::R6Class(
# loop over routes
for(i in seq_along(private$.routes)){
# if path matches pattern and method
if(grepl(private$.routes[[i]]$route$pattern, req$PATH_INFO) && req$REQUEST_METHOD %in% private$.routes[[i]]$method){
if(grepl(private$.routes[[i]]$route$pattern, req$PATH_INFO) &&
req$REQUEST_METHOD %in% private$.routes[[i]]$method){

.globals$infoLog$log(req$REQUEST_METHOD, "on", req$PATH_INFO)

Expand Down

0 comments on commit 18c3470

Please sign in to comment.