diff --git a/.Rbuildignore b/.Rbuildignore index 3c0e666..a4052f5 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -19,3 +19,4 @@ docker/ ^makefile$ ^test\.html$ ^\.vscode$ +^\.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/DESCRIPTION b/DESCRIPTION index ccb706d..e195175 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,7 +17,7 @@ License: GPL (>= 3) Encoding: UTF-8 LazyData: false Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.2 Depends: R (>= 4.1.0) Imports: fs, diff --git a/R/ambiorix.R b/R/ambiorix.R index cff4444..fe60a9a 100644 --- a/R/ambiorix.R +++ b/R/ambiorix.R @@ -38,11 +38,11 @@ Ambiorix <- R6::R6Class( not_found = NULL, error = NULL, on_stop = NULL, -#' @details Define the webserver. -#' -#' @param host A string defining the host. -#' @param port Integer defining the port, defaults to `ambiorix.port` option: uses a random port if `NULL`. -#' @param log Whether to generate a log of events. + #' @details Define the webserver. + #' + #' @param host A string defining the host. + #' @param port Integer defining the port, defaults to `ambiorix.port` option: uses a random port if `NULL`. + #' @param log Whether to generate a log of events. initialize = function( host = getOption("ambiorix.host", "0.0.0.0"), port = getOption("ambiorix.port", NULL), @@ -68,91 +68,91 @@ Ambiorix <- R6::R6Class( .globals$cache_tmpls <- TRUE invisible(self) }, -#' @details Specifies the port to listen on. -#' @param port Port number. -#' -#' @examples -#' app <- Ambiorix$new() -#' -#' app$listen(3000L) -#' -#' app$get("/", function(req, res){ -#' res$send("Using {ambiorix}!") -#' }) -#' -#' if(interactive()) -#' app$start() + #' @details Specifies the port to listen on. + #' @param port Port number. + #' + #' @examples + #' app <- Ambiorix$new() + #' + #' app$listen(3000L) + #' + #' app$get("/", function(req, res){ + #' res$send("Using {ambiorix}!") + #' }) + #' + #' if(interactive()) + #' app$start() listen = function(port){ assert_that(not_missing(port)) private$.port <- as.integer(port) invisible(self) }, -#' @details Sets the 404 page. -#' @param handler Function that accepts the request and returns an object -#' describing an httpuv response, e.g.: [response()]. -#' -#' @examples -#' app <- Ambiorix$new() -#' -#' app$set_404(function(req, res){ -#' res$send("Nothing found here") -#' }) -#' -#' app$get("/", function(req, res){ -#' res$send("Using {ambiorix}!") -#' }) -#' -#' if(interactive()) -#' app$start() + #' @details Sets the 404 page. + #' @param handler Function that accepts the request and returns an object + #' describing an httpuv response, e.g.: [response()]. + #' + #' @examples + #' app <- Ambiorix$new() + #' + #' app$set_404(function(req, res){ + #' res$send("Nothing found here") + #' }) + #' + #' app$get("/", function(req, res){ + #' res$send("Using {ambiorix}!") + #' }) + #' + #' if(interactive()) + #' app$start() set_404 = function(handler){ assert_that(not_missing(handler)) assert_that(is_handler(handler)) self$not_found <- handler invisible(self) }, -#' @details Sets the error handler. -#' @param handler Function that accepts a request, response and an error object. -#' -#' @examples -#' # my custom error handler: -#' error_handler <- \(req, res, error) { -#' if (!is.null(error)) { -#' error_msg <- conditionMessage(error) -#' cli::cli_alert_danger("Error: {error_msg}") -#' } -#' response <- list( -#' code = 500L, -#' msg = "Uhhmmm... Looks like there's an error from our side :(" -#' ) -#' res$ -#' set_status(500L)$ -#' json(response) -#' } -#' -#' # handler for GET at /whoami: -#' whoami <- \(req, res) { -#' # simulate error (object 'Pikachu' is not defined) -#' print(Pikachu) -#' } -#' -#' app <- Ambiorix$ -#' new()$ -#' set_error(error_handler)$ -#' get("/whoami", whoami) -#' -#' if (interactive()) { -#' app$start(open = FALSE) -#' } + #' @details Sets the error handler. + #' @param handler Function that accepts a request, response and an error object. + #' + #' @examples + #' # my custom error handler: + #' error_handler <- \(req, res, error) { + #' if (!is.null(error)) { + #' error_msg <- conditionMessage(error) + #' cli::cli_alert_danger("Error: {error_msg}") + #' } + #' response <- list( + #' code = 500L, + #' msg = "Uhhmmm... Looks like there's an error from our side :(" + #' ) + #' res$ + #' set_status(500L)$ + #' json(response) + #' } + #' + #' # handler for GET at /whoami: + #' whoami <- \(req, res) { + #' # simulate error (object 'Pikachu' is not defined) + #' print(Pikachu) + #' } + #' + #' app <- Ambiorix$ + #' new()$ + #' set_error(error_handler)$ + #' get("/whoami", whoami) + #' + #' if (interactive()) { + #' app$start(open = FALSE) + #' } set_error = function(handler) { assert_that(not_missing(handler)) assert_that(is_error_handler(handler)) self$error <- handler invisible(self) }, -#' @details Static directories -#' -#' @param path Local path to directory of assets. -#' @param uri URL path where the directory will be available. + #' @details Static directories + #' + #' @param path Local path to directory of assets. + #' @param uri URL path where the directory will be available. static = function(path, uri = "www"){ assert_that(not_missing(uri)) assert_that(not_missing(path)) @@ -162,21 +162,21 @@ Ambiorix <- R6::R6Class( private$.static <- append(private$.static, lst) invisible(self) }, -#' @details Start -#' Start the webserver. -#' @param host A string defining the host. -#' @param port Integer defining the port, defaults to `ambiorix.port` option: uses a random port if `NULL`. -#' @param open Whether to open the app the browser. -#' -#' @examples -#' app <- Ambiorix$new() -#' -#' app$get("/", function(req, res){ -#' res$send("Using {ambiorix}!") -#' }) -#' -#' if(interactive()) -#' app$start(port = 3000L) + #' @details Start + #' Start the webserver. + #' @param host A string defining the host. + #' @param port Integer defining the port, defaults to `ambiorix.port` option: uses a random port if `NULL`. + #' @param open Whether to open the app the browser. + #' + #' @examples + #' app <- Ambiorix$new() + #' + #' app$get("/", function(req, res){ + #' res$send("Using {ambiorix}!") + #' }) + #' + #' if(interactive()) + #' app$start(port = 3000L) start = function( port = NULL, host = NULL, @@ -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, @@ -259,30 +262,30 @@ Ambiorix <- R6::R6Class( invisible(self) }, -#' @details Define Serialiser -#' @param handler Function to use to serialise. -#' This function should accept two arguments: the object to serialise and `...`. -#' -#' @examples -#' app <- Ambiorix$new() -#' -#' app$serialiser(function(data, ...){ -#' jsonlite::toJSON(x, ..., pretty = TRUE) -#' }) -#' -#' app$get("/", function(req, res){ -#' res$send("Using {ambiorix}!") -#' }) -#' -#' if(interactive()) -#' app$start() + #' @details Define Serialiser + #' @param handler Function to use to serialise. + #' This function should accept two arguments: the object to serialise and `...`. + #' + #' @examples + #' app <- Ambiorix$new() + #' + #' app$serialiser(function(data, ...){ + #' jsonlite::toJSON(x, ..., pretty = TRUE) + #' }) + #' + #' app$get("/", function(req, res){ + #' res$send("Using {ambiorix}!") + #' }) + #' + #' if(interactive()) + #' app$start() serialiser = function(handler){ assert_that(is_function(handler)) options(AMBIORIX_SERIALISER = handler) invisible(self) }, -#' @details Stop -#' Stop the webserver. + #' @details Stop + #' Stop the webserver. stop = function(){ if(!private$.is_running){ @@ -301,7 +304,7 @@ Ambiorix <- 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()}}") diff --git a/R/log.R b/R/log.R index 0680f9d..f6809c0 100644 --- a/R/log.R +++ b/R/log.R @@ -39,8 +39,8 @@ new_log <- function( file = file, sep = sep )$ - date()$ - time() + date()$ + time() } #' Customise logs diff --git a/R/request.R b/R/request.R index 1d014d2..aabd2dc 100644 --- a/R/request.R +++ b/R/request.R @@ -16,6 +16,7 @@ #' @field HTTP_SEC_FETCH_USER Only sent for requests initiated by user activation, and its value will always be `?1`. #' @field HTTP_UPGRADE_INSECURE_REQUESTS Signals that server supports upgrade. #' @field HTTP_USER_AGENT User agent. +#' @field SERVER_NAME Name of the server. #' @field httpuv.version Version of httpuv. #' @field PATH_INFO Path of the request. #' @field QUERY_STRING Query string of the request. @@ -26,8 +27,7 @@ #' @field rook.input Rook inputs. #' @field rook.url_scheme Rook url scheme. #' @field rook.version Rook version. -#' @field SCRIPT_NAME The initial portion of the request URL's "path" that corresponds to the application object, so that the application knows its virtual "location". -#' @field SERVER_NAME Server name. +#' @field SCRIPT_NAME The initial portion of the request URL's "path" that corresponds to the application object, so that the application knows its virtual "location". #' @field SERVER_NAME Server name. #' @field SERVER_PORT Server port #' @field CONTENT_LENGTH Size of the message body. #' @field CONTENT_TYPE Type of content of the request. @@ -160,37 +160,6 @@ Request <- R6::R6Class( cli::cli_end() }, - #' @details Set Data - #' @param name Name of the variable. - #' @param value Value of the variable. - #' @return Invisible returns self. - set = function(name, value){ - assert_that(not_missing(name)) - assert_that(not_missing(value)) - .Deprecated( - "", - package = "ambiorix", - "Deprecated. The environment is no longer locked, you may simply `res$name <- value`" - ) - - name <- as_label(name) - self[[name]] <- value - - invisible(self) - }, - #' @details Get data - #' @param name Name of the variable to get. - get = function(name){ - assert_that(not_missing(name)) - .Deprecated( - "", - package = "ambiorix", - "Deprecated. The environment is no longer locked, you may simply `res$value" - ) - - name <- as_label(name) - self[[name]] - }, #' @details Get Header #' @param name Name of the header get_header = function(name){ @@ -256,7 +225,7 @@ set_params <- function(path, route = NULL){ nms <- c() pms <- list() - for(i in 1:length(path_split)){ + for(i in seq_along(path_split)){ if(route$components[[i]]$dynamic){ nms <- c(nms, route$components[[i]]$name) pms <- append(pms, utils::URLdecode(path_split[i])) @@ -332,4 +301,4 @@ mockRequest <- function( ) Request$new(req) -} \ No newline at end of file +} diff --git a/R/response.R b/R/response.R index aee3a6e..e6c092d 100644 --- a/R/response.R +++ b/R/response.R @@ -98,28 +98,28 @@ Response <- R6::R6Class( "Response", lock_objects = FALSE, public = list( -#' @details Set the status of the response. -#' @param status An integer defining the status. + #' @details Set the status of the response. + #' @param status An integer defining the status. set_status = function(status){ assert_that(not_missing(status)) private$.status <- status invisible(self) }, -#' @details Send a plain HTML response. -#' @param body Body of the response. -#' @param headers HTTP headers to set. -#' @param status Status of the response, if `NULL` uses `self$status`. + #' @details Send a plain HTML response. + #' @param body Body of the response. + #' @param headers HTTP headers to set. + #' @param status Status of the response, if `NULL` uses `self$status`. send = function(body, headers = NULL, status = NULL){ deprecated_headers(headers) deprecated_status(status) headers <- private$.get_headers(headers) response(status = private$.get_status(status), headers = headers, body = convert_body(body)) }, -#' @details Send a plain HTML response, pre-processed with sprintf. -#' @param body Body of the response. -#' @param ... Passed to `...` of `sprintf`. -#' @param headers HTTP headers to set. -#' @param status Status of the response, if `NULL` uses `self$status`. + #' @details Send a plain HTML response, pre-processed with sprintf. + #' @param body Body of the response. + #' @param ... Passed to `...` of `sprintf`. + #' @param headers HTTP headers to set. + #' @param status Status of the response, if `NULL` uses `self$status`. sendf = function(body, ..., headers = NULL, status = NULL){ deprecated_headers(headers) deprecated_status(status) @@ -127,10 +127,10 @@ Response <- R6::R6Class( headers <- private$.get_headers(headers) response(status = private$.get_status(status), headers = headers, body = convert_body(body)) }, -#' @details Send a plain text response. -#' @param body Body of the response. -#' @param headers HTTP headers to set. -#' @param status Status of the response, if `NULL` uses `self$status`. + #' @details Send a plain text response. + #' @param body Body of the response. + #' @param headers HTTP headers to set. + #' @param status Status of the response, if `NULL` uses `self$status`. text = function(body, headers = NULL, status = NULL){ deprecated_headers(headers) deprecated_status(status) @@ -138,30 +138,30 @@ Response <- R6::R6Class( headers[["Content-Type"]] <- content_plain() response(status = private$.get_status(status), headers = headers, body = convert_body(body)) }, -#' @details Send a file. -#' @param file File to send. -#' @param headers HTTP headers to set. -#' @param status Status of the response. + #' @details Send a file. + #' @param file File to send. + #' @param headers HTTP headers to set. + #' @param status Status of the response. send_file = function(file, headers = NULL, status = NULL){ deprecated_headers(headers) deprecated_status(status) assert_that(not_missing(file)) self$render(file, data = list(), status = status, headers = headers) }, -#' @details Redirect to a path or URL. -#' @param path Path or URL to redirect to. -#' @param status Status of the response, if `NULL` uses `self$status`. + #' @details Redirect to a path or URL. + #' @param path Path or URL to redirect to. + #' @param status Status of the response, if `NULL` uses `self$status`. redirect = function(path, status = NULL){ deprecated_status(status) status <- private$.get_status(status) headers <- private$.get_headers(list(Location = path)) response(status = status, headers = headers, body = "") }, -#' @details Render a template file. -#' @param file Template file. -#' @param data List to fill `[% tags %]`. -#' @param headers HTTP headers to set. -#' @param status Status of the response, if `NULL` uses `self$status`. + #' @details Render a template file. + #' @param file Template file. + #' @param data List to fill `[% tags %]`. + #' @param headers HTTP headers to set. + #' @param status Status of the response, if `NULL` uses `self$status`. render = function(file, data = list(), headers = NULL, status = NULL){ assert_that(not_missing(file)) assert_that(has_file(file)) @@ -175,11 +175,11 @@ Response <- R6::R6Class( response(file_content, status = private$.get_status(status), headers = headers) }, -#' @details Render an object as JSON. -#' @param body Body of the response. -#' @param headers HTTP headers to set. -#' @param status Status of the response, if `NULL` uses `self$status`. -#' @param ... Additional arguments passed to the serialiser. + #' @details Render an object as JSON. + #' @param body Body of the response. + #' @param headers HTTP headers to set. + #' @param status Status of the response, if `NULL` uses `self$status`. + #' @param ... Additional arguments passed to the serialiser. json = function(body, headers = NULL, status = NULL, ...){ self$header_content_json() deprecated_headers(headers) @@ -187,11 +187,11 @@ Response <- R6::R6Class( headers <- private$.get_headers(headers) response(serialise(body), headers = headers, status = private$.get_status(status)) }, -#' @details Sends a comma separated value file -#' @param data Data to convert to CSV. -#' @param name Name of the file. -#' @param status Status of the response, if `NULL` uses `self$status`. -#' @param ... Additional arguments passed to [readr::format_csv()]. + #' @details Sends a comma separated value file + #' @param data Data to convert to CSV. + #' @param name Name of the file. + #' @param status Status of the response, if `NULL` uses `self$status`. + #' @param ... Additional arguments passed to [readr::format_csv()]. csv = function(data, name = "data", status = NULL, ...){ assert_that(not_missing(data)) check_installed("readr") @@ -208,11 +208,11 @@ Response <- R6::R6Class( data <- readr::format_csv(data, ...) response(data, header = headers, status = private$.get_status(status)) }, -#' @details Sends a tab separated value file -#' @param data Data to convert to CSV. -#' @param name Name of the file. -#' @param status Status of the response, if `NULL` uses `self$status`. -#' @param ... Additional arguments passed to [readr::format_tsv()]. + #' @details Sends a tab separated value file + #' @param data Data to convert to CSV. + #' @param name Name of the file. + #' @param status Status of the response, if `NULL` uses `self$status`. + #' @param ... Additional arguments passed to [readr::format_tsv()]. tsv = function(data, name = "data", status = NULL, ...){ assert_that(not_missing(data)) check_installed("readr") @@ -229,10 +229,10 @@ Response <- R6::R6Class( data <- readr::format_tsv(data, ...) response(data, header = headers, status = private$.get_status(status)) }, -#' @details Sends an htmlwidget. -#' @param widget The widget to use. -#' @param status Status of the response, if `NULL` uses `self$status`. -#' @param ... Additional arguments passed to [htmlwidgets::saveWidget()]. + #' @details Sends an htmlwidget. + #' @param widget The widget to use. + #' @param status Status of the response, if `NULL` uses `self$status`. + #' @param ... Additional arguments passed to [htmlwidgets::saveWidget()]. htmlwidget = function(widget, status = NULL, ...){ check_installed("htmlwidgets") if(!inherits(widget, "htmlwidget")) @@ -249,41 +249,41 @@ Response <- R6::R6Class( response(body = paste0(read_lines(tmp), "\n", collapse = ""), status = private$.get_status(status), headers = headers) }, -#' @details Render a markdown file. -#' @param file Template file. -#' @param data List to fill `[% tags %]`. -#' @param headers HTTP headers to set. -#' @param status Status of the response, if `NULL` uses `self$status`. + #' @details Render a markdown file. + #' @param file Template file. + #' @param data List to fill `[% tags %]`. + #' @param headers HTTP headers to set. + #' @param status Status of the response, if `NULL` uses `self$status`. md = function(file, data = list(), headers = NULL, status = NULL) { check_installed("commonmark") deprecated_headers(headers) deprecated_status(status) self$render(file, data, headers, status) }, -#' @details Send a png file -#' @param file Path to local file. + #' @details Send a png file + #' @param file Path to local file. png = function(file){ private$.send_image(file, "png") }, -#' @details Send a jpeg file -#' @param file Path to local file. + #' @details Send a jpeg file + #' @param file Path to local file. jpeg = function(file) { private$.send_image(file, "jpeg") }, -#' @details Send an image -#' Similar to `png` and `jpeg` methods but guesses correct method -#' based on file extension. -#' @param file Path to local file. + #' @details Send an image + #' Similar to `png` and `jpeg` methods but guesses correct method + #' based on file extension. + #' @param file Path to local file. image = function(file) { type <- tools::file_ext(file) if(!type %in% c("png", "jpeg")) stop("Only accepts .png and .jpeg files") private$.send_image(file, type) }, -#' @details Ggplot2 -#' @param plot Ggplot2 plot object. -#' @param type Type of image to save. -#' @param ... Passed to [ggplot2::ggsave()] + #' @details Ggplot2 + #' @param plot Ggplot2 plot object. + #' @param type Type of image to save. + #' @param ... Passed to [ggplot2::ggsave()] ggplot2 = function(plot, ..., type = c("png", "jpeg")) { assert_that(not_missing(plot)) check_installed("ggplot2") @@ -298,7 +298,7 @@ Response <- R6::R6Class( ) private$.send_image(temp, type, clean = TRUE) }, -#' @details Print + #' @details Print print = function(){ cli::cli_h3("A Response") @@ -315,10 +315,10 @@ Response <- R6::R6Class( cli::cli_end() }, -#' @details Set Data -#' @param name Name of the variable. -#' @param value Value of the variable. -#' @return Invisible returns self. + #' @details Set Data + #' @param name Name of the variable. + #' @param value Value of the variable. + #' @return Invisible returns self. set = function(name, value){ assert_that(not_missing(name)) assert_that(not_missing(value)) @@ -333,8 +333,8 @@ Response <- R6::R6Class( invisible(self) }, -#' @details Get data -#' @param name Name of the variable to get. + #' @details Get data + #' @param name Name of the variable to get. get = function(name){ assert_that(not_missing(name)) .Deprecated( @@ -346,9 +346,9 @@ Response <- R6::R6Class( name <- as_label(name) self[[name]] }, -#' @details Add headers to the response. -#' @param name,value Name and value of the header. -#' @return Invisibly returns self. + #' @details Add headers to the response. + #' @param name,value Name and value of the header. + #' @return Invisibly returns self. header = function(name, value){ assert_that(not_missing(name)) assert_that(not_missing(value)) @@ -356,50 +356,50 @@ Response <- R6::R6Class( private$.headers[[name]] <- value invisible(self) }, -#' @details Set Content Type to JSON -#' @return Invisibly returns self. + #' @details Set Content Type to JSON + #' @return Invisibly returns self. header_content_json = function(){ self$header("Content-Type", content_json()) invisible(self) }, - #' @details Set Content Type to HTML - #' @return Invisibly returns self. + #' @details Set Content Type to HTML + #' @return Invisibly returns self. header_content_html = function(){ self$header("Content-Type", content_html()) invisible(self) }, - #' @details Set Content Type to Plain Text - #' @return Invisibly returns self. + #' @details Set Content Type to Plain Text + #' @return Invisibly returns self. header_content_plain = function(){ self$header("Content-Type", content_plain()) invisible(self) }, - #' @details Set Content Type to CSV - #' @return Invisibly returns self. + #' @details Set Content Type to CSV + #' @return Invisibly returns self. header_content_csv = function(){ self$header("Content-Type", content_csv()) invisible(self) }, - #' @details Set Content Type to TSV - #' @return Invisibly returns self. + #' @details Set Content Type to TSV + #' @return Invisibly returns self. header_content_tsv = function(){ self$header("Content-Type", content_tsv()) invisible(self) }, -#' @details Get headers -#' Returns the list of headers currently set. + #' @details Get headers + #' Returns the list of headers currently set. get_headers = function() { return(private$.headers) }, -#' @details Get a header -#' Returns a single header currently, `NULL` if not set. -#' @param name Name of the header to return. + #' @details Get a header + #' Returns a single header currently, `NULL` if not set. + #' @param name Name of the header to return. get_header = function(name) { assert_that(not_missing(name)) return(private$.headers[[name]]) }, -#' @details Set headers -#' @param headers A named list of headers to set. + #' @details Set headers + #' @param headers A named list of headers to set. set_headers = function(headers) { assert_that(not_missing(headers)) if(!is.list(headers)) @@ -425,26 +425,26 @@ Response <- R6::R6Class( private$.headers[[name]] <- value invisible(self) }, -#' @details Add a pre render hook. -#' Runs before the `render` and `send_file` method. -#' -#' @param hook A function that accepts at least 4 arguments: -#' - `self`: The `Request` class instance. -#' - `content`: File content a vector of character string, -#' content of the template. -#' - `data`: `list` passed from `render` method. -#' - `ext`: File extension of the template file. -#' -#' This function is used to add pre-render hooks to the `render` -#' method. The function should return an object of class -#' `responsePreHook` as obtained by [pre_hook()]. -#' This is meant to be used by middlewares to, if necessary, -#' pre-process rendered data. -#' -#' Include `...` in your `hook` to ensure it will handle -#' potential updates to hooks in the future. -#' -#' @return Invisible returns self. + #' @details Add a pre render hook. + #' Runs before the `render` and `send_file` method. + #' + #' @param hook A function that accepts at least 4 arguments: + #' - `self`: The `Request` class instance. + #' - `content`: File content a vector of character string, + #' content of the template. + #' - `data`: `list` passed from `render` method. + #' - `ext`: File extension of the template file. + #' + #' This function is used to add pre-render hooks to the `render` + #' method. The function should return an object of class + #' `responsePreHook` as obtained by [pre_hook()]. + #' This is meant to be used by middlewares to, if necessary, + #' pre-process rendered data. + #' + #' Include `...` in your `hook` to ensure it will handle + #' potential updates to hooks in the future. + #' + #' @return Invisible returns self. pre_render_hook = function(hook) { assert_that(not_missing(hook)) assert_that( @@ -460,19 +460,19 @@ Response <- R6::R6Class( private$.preHooks <- append(private$.preHooks, hook) invisible(self) }, -#' @details Post render hook. -#' -#' @param hook A function to run after the rendering of HTML. -#' It should accept at least 3 arguments: -#' - `self`: The `Request` class instance. -#' - `content`: File content a vector of character string, -#' content of the template. -#' - `ext`: File extension of the template file. -#' -#' Include `...` in your `hook` to ensure it will handle -#' potential updates to hooks in the future. -#' -#' @return Invisible returns self. + #' @details Post render hook. + #' + #' @param hook A function to run after the rendering of HTML. + #' It should accept at least 3 arguments: + #' - `self`: The `Request` class instance. + #' - `content`: File content a vector of character string, + #' content of the template. + #' - `ext`: File extension of the template file. + #' + #' Include `...` in your `hook` to ensure it will handle + #' potential updates to hooks in the future. + #' + #' @return Invisible returns self. post_render_hook = function(hook) { assert_that(not_missing(hook)) assert_that( @@ -488,32 +488,32 @@ Response <- R6::R6Class( private$.postHooks <- append(private$.postHooks, hook) invisible(self) }, -#' @details Set a cookie -#' Overwrites existing cookie of the same `name`. -#' @param name Name of the cookie. -#' @param value value of the cookie. -#' @param expires Expiry, if an integer assumes it's the number of seconds -#' from now. Otherwise accepts an object of class `POSIXct` or `Date`. -#' If a `character` string then it is set as-is and not pre-processed. -#' If unspecified, the cookie becomes a session cookie. A session finishes -#' when the client shuts down, after which the session cookie is removed. -#' @param max_age Indicates the number of seconds until the cookie expires. -#' A zero or negative number will expire the cookie immediately. -#' If both `expires` and `max_age` are set, the latter has precedence. -#' @param domain Defines the host to which the cookie will be sent. -#' If omitted, this attribute defaults to the host of the current document URL, -#' not including subdomains. -#' @param path Indicates the path that must exist in the requested URL for the -#' browser to send the Cookie header. -#' @param secure Indicates that the cookie is sent to the server only when a -#' request is made with the https: scheme (except on localhost), and therefore, -#' is more resistant to man-in-the-middle attacks. -#' @param http_only Forbids JavaScript from accessing the cookie, for example, -#' through the document.cookie property. -#' @param same_site Controls whether or not a cookie is sent with cross-origin -#' requests, providing some protection against cross-site request forgery -#' attacks (CSRF). Accepts `Strict`, `Lax`, or `None`. -#' @return Invisibly returns self. + #' @details Set a cookie + #' Overwrites existing cookie of the same `name`. + #' @param name Name of the cookie. + #' @param value value of the cookie. + #' @param expires Expiry, if an integer assumes it's the number of seconds + #' from now. Otherwise accepts an object of class `POSIXct` or `Date`. + #' If a `character` string then it is set as-is and not pre-processed. + #' If unspecified, the cookie becomes a session cookie. A session finishes + #' when the client shuts down, after which the session cookie is removed. + #' @param max_age Indicates the number of seconds until the cookie expires. + #' A zero or negative number will expire the cookie immediately. + #' If both `expires` and `max_age` are set, the latter has precedence. + #' @param domain Defines the host to which the cookie will be sent. + #' If omitted, this attribute defaults to the host of the current document URL, + #' not including subdomains. + #' @param path Indicates the path that must exist in the requested URL for the + #' browser to send the Cookie header. + #' @param secure Indicates that the cookie is sent to the server only when a + #' request is made with the https: scheme (except on localhost), and therefore, + #' is more resistant to man-in-the-middle attacks. + #' @param http_only Forbids JavaScript from accessing the cookie, for example, + #' through the document.cookie property. + #' @param same_site Controls whether or not a cookie is sent with cross-origin + #' requests, providing some protection against cross-site request forgery + #' attacks (CSRF). Accepts `Strict`, `Lax`, or `None`. + #' @return Invisibly returns self. cookie = function( name, value, @@ -559,10 +559,10 @@ Response <- R6::R6Class( invisible(self) }, -#' @details Clear a cookie -#' Clears the value of a cookie. -#' @param name Name of the cookie to clear. -#' @return Invisibly returns self. + #' @details Clear a cookie + #' Clears the value of a cookie. + #' @param name Name of the cookie to clear. + #' @return Invisibly returns self. clear_cookie = function(name) { # cookies with date in the past are removed from the browser self$cookie( diff --git a/R/route.R b/R/route.R index ed465c7..3622695 100644 --- a/R/route.R +++ b/R/route.R @@ -9,12 +9,10 @@ Route <- R6::R6Class( assert_that(not_missing(path)) self$path <- gsub("\\?.*$", "", path) # remove query self$dynamic <- grepl(":", path) - self$decompose() - self$as_pattern() }, - as_pattern = function(){ + as_pattern = function(parent = ""){ if(!is.null(.globals$pathToPattern)) { - self$pattern <-.globals$pathToPattern(self$path) + self$pattern <- .globals$pathToPattern(self$path) return( invisible(self) ) @@ -28,12 +26,13 @@ Route <- R6::R6Class( }) pattern <- paste0(pattern, collapse = "/") - self$pattern <- paste0("^/", pattern, "$") + self$pattern <- paste0("^", parent, "/", pattern, "$") invisible(self) }, - decompose = function(){ + decompose = function(parent = ""){ + path <- paste0(parent, self$path) # split - components <- strsplit(self$path, "(?<=.)(?=[:/])", perl = TRUE)[[1]] + components <- strsplit(path, "(?<=.)(?=[:/])", perl = TRUE)[[1]] # remove lonely / components <- components[components != "/"] @@ -54,7 +53,7 @@ Route <- R6::R6Class( components <- as.list(components) comp <- list() - for(i in 1:length(components)){ + for(i in seq_along(components)){ c <- list( index = i, dynamic = grepl(":", components[[i]]), diff --git a/R/router.R b/R/router.R index 26b31f3..5ba5fa4 100644 --- a/R/router.R +++ b/R/router.R @@ -47,13 +47,13 @@ Router <- R6::R6Class( inherit = Routing, public = list( error = NULL, -#' @details Define the base route. -#' @param path The base path of the router. + #' @details Define the base route. + #' @param path The base path of the router. initialize = function(path){ assert_that(not_missing(path)) super$initialize(path) }, -#' @details Print + #' @details Print print = function(){ cli::cli_rule("Ambiorix", right = "router") cli::cli_li("routes: {.val {super$n_routes()}}") diff --git a/R/routing.R b/R/routing.R index 33c7fca..87e42ee 100644 --- a/R/routing.R +++ b/R/routing.R @@ -4,6 +4,8 @@ #' Do not use directly, see [Ambiorix], and [Router]. #' #' @field error Error handler. +#' @field basepath Basepath, read-only. +#' @field websocket Websocket handler. #' #' @keywords export Routing <- R6::R6Class( @@ -50,14 +52,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 +76,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 +100,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 +124,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 +148,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 +172,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 +196,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,12 +224,13 @@ 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()}}") }, #' @details Engine to use for rendering templates. + #' @param engine Engine function. engine = function(engine){ if(!is_renderer_obj(engine)) engine <- as_renderer(engine) @@ -235,13 +238,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 +257,7 @@ 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()) + private$.routers <- append(private$.routers, use) } if(is_renderer_obj(use) && private$.is_router){ @@ -318,27 +319,96 @@ Routing <- R6::R6Class( # pass middleware if(is.function(use)) { assert_that(is_handler(use)) - attr(use, "basepath") <- sprintf("^%s", private$.basepath) private$.middleware <- append(private$.middleware, use) return(invisible(self)) } invisible(self) }, -#' @details Get the routes - get_routes = function(){ - return(private$.routes) + #' @details Get the routes + #' @param routes Existing list of routes. + #' @param parent Parent path. + get_routes = function(routes = list(), parent = ""){ + routes <- append( + routes, + private$.routes |> + lapply(\(route) { + route$route$as_pattern(parent) + route$route$decompose(parent) + route + }) + ) + + if(!length(private$.routers)) return(routes) + + parent <- paste0(parent, private$.basepath) + + for(router in private$.routers) { + routes <- router$get_routes(routes, parent) + } + + return(routes) }, -#' @details Get the receivers - get_receivers = function(){ - return(private$.receivers) + #' @details Get the websocket receivers + #' @param receivers Existing list of receivers + get_receivers = function(receivers = list()){ + receivers <- append(receivers, private$.receivers) + + if(!length(private$.routers)) return(receivers) + + for(router in private$.routers) { + receivers <- router$get_receivers(receivers) + } + + return(receivers) }, -#' @details Get the middleware - get_middleware = function(){ - return(private$.middleware) + #' @details Get the middleware + #' @param middlewares Existing list of middleswares + #' @param parent Parent path + get_middleware = function(middlewares = list(), parent = ""){ + middlewares <- append( + middlewares, + private$.middleware |> + lapply(\(fn) { + attr(fn, "basepath") <- paste0(parent, private$.basepath) + return(fn) + }) + ) + + if(!length(private$.routers)) return(middlewares) + + parent <- paste0(parent, private$.basepath) + + for(router in private$.routers) { + middlewares <- router$get_middleware(middlewares, parent) + } + + return(middlewares) + }, + #' @details Prepare routes and decomposes paths + prepare = function() { + for(route in private$.routes) { + route$route$as_pattern() + route$route$decompose() + } + + 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 +429,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 +438,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 +446,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 +478,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) diff --git a/man/Ambiorix.Rd b/man/Ambiorix.Rd index 2685e7f..d5b66a2 100644 --- a/man/Ambiorix.Rd +++ b/man/Ambiorix.Rd @@ -175,6 +175,7 @@ if(interactive())
ambiorix::Routing$options()
ambiorix::Routing$patch()
ambiorix::Routing$post()
ambiorix::Routing$prepare()
ambiorix::Routing$put()
ambiorix::Routing$receive()
ambiorix::Routing$use()
ambiorix::Routing$options()
ambiorix::Routing$patch()
ambiorix::Routing$post()
ambiorix::Routing$prepare()
ambiorix::Routing$put()
ambiorix::Routing$receive()
ambiorix::Routing$use()