From 18c347001a86b06ef55dc8ed3b4072eefa044261 Mon Sep 17 00:00:00 2001 From: John Coene Date: Wed, 9 Oct 2024 21:15:10 +0200 Subject: [PATCH] feat: nested routers initial pass --- .lintr | 16 ++++ R/ambiorix.R | 5 +- R/routing.R | 239 +++++++++++++++++++++++++++++++-------------------- 3 files changed, 164 insertions(+), 96 deletions(-) create mode 100644 .lintr diff --git a/.lintr b/.lintr new file mode 100644 index 0000000..a5c1ffa --- /dev/null +++ b/.lintr @@ -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" diff --git a/R/ambiorix.R b/R/ambiorix.R index cff4444..3bad152 100644 --- a/R/ambiorix.R +++ b/R/ambiorix.R @@ -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, diff --git a/R/routing.R b/R/routing.R index 33c7fca..6cd7041 100644 --- a/R/routing.R +++ b/R/routing.R @@ -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)) @@ -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)) @@ -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)) @@ -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)) @@ -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)) @@ -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)) @@ -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, @@ -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()}}") @@ -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)) @@ -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){ @@ -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) @@ -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 @@ -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( @@ -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]]] } @@ -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)