diff --git a/DESCRIPTION b/DESCRIPTION index cb31b341..5a4297aa 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -18,7 +18,7 @@ Description: Gives the ability to automatically generate and serve an HTTP API from R functions using the annotations in the R documentation around your functions. Depends: - R (>= 3.0.0) + R (>= 3.5.0) Imports: R6 (>= 2.0.0), stringi (>= 0.3.0), diff --git a/NEWS.md b/NEWS.md index cd188a2d..a3b65a6c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,19 @@ # plumber (development version) +## Breaking changes + +* When mounting a router at an existing location, the old mounted router is removed and the router to be mounted is added to the end of the mount list (default; `after = NULL`). This makes it so prior mount calls do not affect current mount calls. (#882) + +* The default value of `debug` has been changed from `base::interactive()` to `rlang::is_interactive()`. This allows for tests to be consistent when run interactively or in batch mode. (#882) + +## New features + +* Added support for `pr_mount(after=)` / `Plumber$mount(after=)` which allows for mounts to be added in non-sequential order (#882) + +* Always mount OpenAPI documentation at `/__docs__/` as the first mount to have preference when using conflicting mount locations (e.g. a static mount at `/`). (#882) + +## Bug fixes + # plumber 1.2.1 diff --git a/R/plumber.R b/R/plumber.R index 03cefd73..1620ef69 100644 --- a/R/plumber.R +++ b/R/plumber.R @@ -143,7 +143,7 @@ Plumber <- R6Class( #' Mac OS X, port numbers smaller than 1025 require root privileges. #' #' This value does not need to be explicitly assigned. To explicitly set it, see [options_plumber()]. - #' @param debug If `TRUE`, it will provide more insight into your API errors. Using this value will only last for the duration of the run. If a `$setDebug()` has not been called, `debug` will default to `interactive()` at `$run()` time. See `$setDebug()` for more details. + #' @param debug If `TRUE`, it will provide more insight into your API errors. Using this value will only last for the duration of the run. If a `$setDebug()` has not been called, `debug` will default to [`rlang::is_interactive()`] at `$run()` time. See `$setDebug()` for more details. #' @param swagger Deprecated. Please use `docs` instead. See `$setDocs(docs)` or `$setApiSpec()` for more customization. #' @param swaggerCallback An optional single-argument function that is #' called back with the URL to an OpenAPI user interface when one becomes @@ -232,7 +232,7 @@ Plumber <- R6Class( }, add = TRUE) # Fix the debug value while running. self$setDebug( - # Order: Run method param, internally set value, is interactive() + # Order: Run method param, internally set value, `is_interactive()` # `$getDebug()` is dynamic given `setDebug()` has never been called. rlang::maybe_missing(debug, self$getDebug()) ) @@ -252,7 +252,7 @@ Plumber <- R6Class( # Set and restore the wd to make it appear that the proc is running local to the file's definition. if (!is.null(private$filename)) { old_wd <- setwd(dirname(private$filename)) - on.exit({setwd(old_wd)}, add = TRUE) + on.exit(setwd(old_wd), add = TRUE, after = FALSE) } if (isTRUE(docs_info$enabled)) { @@ -264,10 +264,11 @@ Plumber <- R6Class( callback = swaggerCallback, quiet = quiet ) - on.exit(unmount_docs(self, docs_info), add = TRUE) + on.exit(unmount_docs(self, docs_info), add = TRUE, after = FALSE) } - on.exit(private$runHooks("exit"), add = TRUE) + # Run user exit hooks given docs and working directory + on.exit(private$runHooks("exit"), add = TRUE, after = FALSE) httpuv::runServer(host, port, self) }, @@ -278,8 +279,13 @@ Plumber <- R6Class( #' by paths which is a great technique for decomposing large APIs into smaller files. #' #' See also: [pr_mount()] - #' @param path a character string. Where to mount router. - #' @param router a Plumber router. Router to be mounted. + #' @param path a character string. Where to mount the sub router. + #' @param router a Plumber router. Sub router to be mounted. + #' @param ... Ignored. Used for possible parameter expansion. + #' @param after If `NULL` (default), the router will be appended to the end + #' of the mounts. If a number, the router will be inserted at the given + #' index. E.g. `after = 0` will prepend the sub router, giving it + #' preference over other mounted routers. #' @examples #' \dontrun{ #' root <- pr() @@ -290,7 +296,9 @@ Plumber <- R6Class( #' products <- Plumber$new("products.R") #' root$mount("/products", products) #' } - mount = function(path, router) { + mount = function(path, router, ..., after = NULL) { + ellipsis::check_dots_empty() + # Ensure that the path has both a leading and trailing slash. if (!grepl("^/", path)) { path <- paste0("/", path) @@ -299,7 +307,15 @@ Plumber <- R6Class( path <- paste0(path, "/") } - private$mnts[[path]] <- router + # Remove prior mount if it exists + self$unmount(path) + + # Add new mount + # Mount order matters + after <- after %||% length(private$mnts) + mntList <- list() + mntList[[path]] <- router + private$mnts <- append(private$mnts, mntList, after = after) }, #' @description Unmount a Plumber router #' @param path a character string. Where to unmount router. @@ -963,11 +979,11 @@ Plumber <- R6Class( #' #' See also: `$getDebug()` and [pr_set_debug()] #' @param debug `TRUE` provides more insight into your API errors. - setDebug = function(debug = interactive()) { + setDebug = function(debug = is_interactive()) { stopifnot(length(debug) == 1) private$debug <- isTRUE(debug) }, - #' @description Retrieve the `debug` value. If it has never been set, the result of `interactive()` will be used. + #' @description Retrieve the `debug` value. If it has never been set, the result of [`rlang::is_interactive()`] will be used. #' #' See also: `$getDebug()` and [pr_set_debug()] getDebug = function() { @@ -1335,8 +1351,10 @@ upgrade_docs_parameter <- function(docs, ...) { +#' @importFrom rlang is_interactive +# Method needed for testing mocking default_debug <- function() { - interactive() + is_interactive() } diff --git a/R/pr.R b/R/pr.R index 40f1b4be..9237054d 100644 --- a/R/pr.R +++ b/R/pr.R @@ -210,8 +210,13 @@ pr_head <- function(pr, #' returns the updated router. #' #' @param pr The host Plumber router. -#' @param path A character string. Where to mount router. -#' @param router A Plumber router. Router to be mounted. +#' @param path a character string. Where to mount the sub router. +#' @param router a Plumber router. Sub router to be mounted. +#' @param ... Ignored. Used for possible parameter expansion. +#' @param after If `NULL` (default), the router will be appended to the end +#' of the mounts. If a number, the router will be inserted at the given +#' index. E.g. `after = 0` will prepend the sub router, giving it +#' preference over other mounted routers. #' #' @return A Plumber router with the supplied router mounted #' @@ -229,9 +234,11 @@ pr_head <- function(pr, #' @export pr_mount <- function(pr, path, - router) { + router, + ..., + after = NULL) { validate_pr(pr) - pr$mount(path = path, router = router) + pr$mount(path = path, router = router, ..., after = after) pr } @@ -490,7 +497,7 @@ pr_filter <- function(pr, #' @param ... Should be empty. #' @param debug If `TRUE`, it will provide more insight into your API errors. #' Using this value will only last for the duration of the run. -#' If [pr_set_debug()] has not been called, `debug` will default to `interactive()` at [pr_run()] time +#' If [pr_set_debug()] has not been called, `debug` will default to [`rlang::is_interactive()`] at [pr_run()] time #' @param docs Visual documentation value to use while running the API. #' This value will only be used while running the router. #' If missing, defaults to information previously set with [pr_set_docs()]. diff --git a/R/pr_set.R b/R/pr_set.R index dd66561f..375563fb 100644 --- a/R/pr_set.R +++ b/R/pr_set.R @@ -98,7 +98,7 @@ pr_set_error <- function(pr, fun) { #' Set debug value to include error messages of routes cause an error #' #' To hide any error messages in production, set the debug value to `FALSE`. -#' The `debug` value is enabled by default for [interactive()] sessions. +#' The `debug` value is enabled by default for [`rlang::is_interactive()`] sessions. #' #' @template param_pr #' @param debug `TRUE` provides more insight into your API errors. @@ -118,7 +118,7 @@ pr_set_error <- function(pr, fun) { #' pr_get("/boom", function() stop("boom")) %>% #' pr_run() #' } -pr_set_debug <- function(pr, debug = interactive()) { +pr_set_debug <- function(pr, debug = is_interactive()) { validate_pr(pr) pr$setDebug(debug = debug) pr diff --git a/R/ui.R b/R/ui.R index f6bcf11e..5708b51e 100644 --- a/R/ui.R +++ b/R/ui.R @@ -1,4 +1,5 @@ #' @include globals.R +docs_root <- paste0("/__docs__/") # Mount OpenAPI and Docs #' @noRd @@ -184,7 +185,6 @@ register_docs <- function(name, index, static = NULL) { stopifnot(is.function(index)) if (!is.null(static)) stopifnot(is.function(static)) - docs_root <- paste0("/__docs__/") docs_paths <- c("/index.html", "/") mount_docs_func <- function(pr, api_url, ...) { # Save initial extra argument values @@ -211,7 +211,7 @@ register_docs <- function(name, index, static = NULL) { message("") } - pr$mount(docs_root, docs_router) + pr$mount(docs_root, docs_router, after = 0) # add legacy swagger redirects (RStudio Connect) redirect_info <- swagger_redirects() diff --git a/man/Plumber.Rd b/man/Plumber.Rd index 576d47c8..1c8b0cf2 100644 --- a/man/Plumber.Rd +++ b/man/Plumber.Rd @@ -273,15 +273,22 @@ by paths which is a great technique for decomposing large APIs into smaller file See also: \code{\link[=pr_mount]{pr_mount()}} \subsection{Usage}{ -\if{html}{\out{