R/api.R

Defines functions launch_server dots_to_plumber_files api_parse is_plumber_api api

Documented in api api_parse is_plumber_api

#' Create a new plumber API, optionally based on one or more plumber files
#'
#' This is the main way to create a new [Plumber2] object that encapsulates your
#' full api. It is also possible to add files to the API after creation using
#' `api_parse()`
#'
#' @param ... plumber files or directories containing plumber files to be parsed
#' in the given order. The order of parsing determines the final order of the
#' routes in the stack. If `...` contains a `_server.yml` file then all other
#' files in `...` will be ignored and the `_server.yml` file will be used as the
#' basis for the API
#' @param host A string that is a valid IPv4 address that is owned by this
#' server
#' @param port A number or integer that indicates the server port that should be
#' listened on. Note that on most Unix-like systems including Linux and macOS,
#' port numbers smaller than 1024 require root privileges.
#' @param doc_type The type of API documentation to generate. Can be either
#' `"rapidoc"` (the default), `"redoc"`, `"swagger"`, or `NULL` (equating to not
#' generating API docs)
#' @param doc_path The URL path to serve the api documentation from
#' @param reject_missing_methods Should requests to paths that doesn't
#' have a handler for the specific method automatically be rejected with a
#' 405 Method Not Allowed response with the correct Allow header informing
#' the client of the implemented methods. Assigning a handler to `"any"` for
#' the same path at a later point will overwrite this functionality. Be
#' aware that setting this to `TRUE` will prevent the request from falling
#' through to other routes that might have a matching method and path. This
#' setting anly affects handlers on the request router.
#' @param ignore_trailing_slash Logical. Should the trailing slash of a path
#' be ignored when adding handlers and handling requests. Setting this will
#' not change the request or the path associated with but just ensure that
#' both `path/to/resource` and `path/to/resource/` ends up in the same
#' handler.
#' @param max_request_size Sets a maximum size of request bodies. Setting this
#' will add a handler to the header router that automatically rejects requests
#' based on their `Content-Length` header
#' @param shared_secret Assigns a shared secret to the api. Setting this will
#' add a handler to the header router that automatically rejects requests if
#' their `Plumber-Shared-Secret` header doesn't contain the same value. Be aware
#' that this type of authentication is very weak. Never put the shared secret in
#' plain text but rely on e.g. the keyring package for storage. Even so, if
#' requests are send over HTTP (not HTTPS) then anyone can read the secret and
#' use it
#' @param compression_limit The size threshold in bytes for trying to
#' compress the response body (it is still dependant on content negotiation)
#' @param default_async The default evaluator to use for async request handling
#' @param env The parent environment to the environment the files should be
#' evaluated in. Each file will be evaluated in it's own environment so they
#' don't interfere with each other
#'
#' @return A [Plumber2] object
#'
#' @export
#'
#' @seealso [api_package()] for creating an api based on files distributed with
#' a package
#' @seealso [get_opts()] for how to set default options
#'
#' @examples
#' # When creating an API programmatically you'll usually initialise the object
#' # without pointing to any route files or a _server.yml file
#' pa <- api()
#'
#' # You can pass it a directory and it will load up all recognised files it
#' # contains
#' example_dir <- system.file("plumber2", "quickstart", package = "plumber2")
#' pa <- api(example_dir)
#'
#' # Or you can pass files directly
#' pa <- api(list.files(example_dir, full.names = TRUE)[1])
#'
api <- function(
  ...,
  host = get_opts("host", "127.0.0.1"),
  port = get_opts("port", 8080),
  doc_type = get_opts("docType", "rapidoc"),
  doc_path = get_opts("docPath", "__docs__"),
  reject_missing_methods = get_opts("rejectMissingMethods", FALSE),
  ignore_trailing_slash = get_opts("ignoreTrailingSlash", TRUE),
  max_request_size = get_opts("maxRequestSize"),
  shared_secret = get_opts("sharedSecret"),
  compression_limit = get_opts("compressionLimit", 1e3),
  default_async = get_opts("async", "mirai"),
  env = caller_env()
) {
  locations <- dots_to_plumber_files(...)
  if (isTRUE(is_plumber2_server_yml(locations))) {
    server_yml <- yaml::read_yaml(locations)
    if (!is.null(server_yml$constructor)) {
      api <- source(
        fs::path(
          fs::path_dir(locations),
          server_yml$constructor
        ),
        verbose = FALSE
      )$value
      if (!is_plumber_api(api)) {
        cli::cli_abort(
          "The constructor file in {.file {locations}} did not produce a plumber2 API"
        )
      }
    } else {
      api <- Plumber2$new(
        host = server_yml$options$host %||% host,
        port = server_yml$options$port %||% port,
        doc_type = server_yml$options$docType %||% doc_type,
        doc_path = server_yml$options$docPath %||% doc_path,
        reject_missing_methods = server_yml$options$rejectMissingMethods %||%
          reject_missing_methods,
        ignore_trailing_slash = server_yml$options$ignoreTrailingSlash %||%
          ignore_trailing_slash,
        max_request_size = server_yml$options$maxRequestSize %||%
          max_request_size,
        shared_secret = shared_secret,
        compression_limit = server_yml$options$compressionLimit %||%
          compression_limit,
        default_async = server_yml$options$default_async %||% default_async,
        env = env
      )
    }
    locations <- fs::path(
      fs::path_dir(locations),
      server_yml$routes
    )
  } else {
    api <- Plumber2$new(
      host = host,
      port = port,
      doc_type = doc_type,
      doc_path = doc_path,
      reject_missing_methods = reject_missing_methods,
      ignore_trailing_slash = ignore_trailing_slash,
      max_request_size = max_request_size,
      shared_secret = shared_secret,
      compression_limit = compression_limit,
      default_async = default_async,
      env = env
    )
  }
  api_parse(api, !!!locations)
}
#' @rdname api
#' @param x An object to test for whether it is a plumber api
#' @export
#'
is_plumber_api <- function(x) inherits(x, "Plumber2")

#' @rdname api
#' @param api A plumber2 api object to parse files into
#' @export
api_parse <- function(api, ...) {
  locations <- dots_to_plumber_files(..., prefer_yml = FALSE)
  priority <- vapply(
    locations,
    function(path) {
      lines <- readLines(path)
      lines <- lines[seq_len(
        which(grepl("^(?!#('|\\*))", lines, perl = T))[1] - 1
      )]
      order <- which(grepl("^#('|\\*) @routeOrder", lines))
      if (length(order) == 0) {
        order <- NA
      } else {
        order <- utils::type.convert(
          trimws(sub("^#('|\\*) @routeOrder", "", lines[order[1]])),
          as.is = TRUE
        )
      }
      check_number_whole(order, allow_na = TRUE, arg = "@routeOrder")
      order
    },
    numeric(1)
  )
  locations <- locations[order(priority)]
  for (loc in locations) {
    api <- api$parse_file(loc)
  }
  api
}

dots_to_plumber_files <- function(..., prefer_yml = TRUE, call = caller_env()) {
  locations <- unlist(lapply(list2(...), function(loc) {
    if (length(loc) == 0) {
      return(NULL)
    }
    if (fs::is_dir(loc)) {
      loc <- fs::dir_ls(loc, all = TRUE, recurse = TRUE)
      server_yml <- is_plumber2_server_yml(loc)
      if (prefer_yml && any(server_yml)) {
        loc <- loc[server_yml]
      } else {
        loc <- loc[fs::path_ext(loc) %in% c("R", "r")]
      }
    }
    loc
  }))
  if (length(locations) == 0) {
    return(character())
  }
  if (!all(fs::file_exists(locations))) {
    cli::cli_abort("{.arg ...} must point to existing files", call = call)
  }
  server_yml <- is_plumber2_server_yml(locations)
  if (prefer_yml && any(server_yml)) {
    if (sum(server_yml) != 1) {
      cli::cli_abort(
        "You can at most use one {.file _server.yml} file to specify your API",
        call = call
      )
    }
    if (length(locations) != 1) {
      cli::cli_warn(
        "{.file _server.yml} found. Ignoring all other files provided in {.arg ...}",
        call = call
      )
    }
    locations[server_yml]
  } else {
    if (any(server_yml)) {
      cli::cli_warn(
        "Ignoring {.file _server.yml} files in {.arg ...}",
        call = call
      )
    }
    locations[!server_yml]
  }
}

# For use by connect etc
launch_server <- function(settings, host = NULL, port = NULL, ...) {
  pa <- api(settings)
  if (!is.null(host)) {
    pa$host <- host
  }
  if (!is.null(port)) {
    pa$port <- port
  }
  api_run(pa)
}

Try the plumber2 package in your browser

Any scripts or data that you put into this service are public.

plumber2 documentation built on Jan. 20, 2026, 5:06 p.m.