knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) show_file <- function(path, lang) { writeLines(c(paste0("```", lang), readLines(path), "```")) } porcelain_file <- function(path) { system.file(path, package = "porcelain", mustWork = TRUE) }
The approach described in vignette("porcelain")
allows maximum flexibility, but porcelain
also allows a less programmatic, more declarative approach via custom roxygen tags. This approach returns to the original expressiveness of plumber
, but with the type-checking and testability of porcelain
.
You must first update your DESCRIPTION
file to to add the line
Roxygen: list(roclets = c("rd", "namespace", "porcelain::porcelain_roclet"))
or if you are using roxygen-with-markdown
Roxygen: list(markdown = TRUE, roclets = c("rd", "namespace", "porcelain::porcelain_roclet"))
The important part is the porcelain::porcelain_roclet
entry in "roclets" which will activate the @porcelain
tag that we will use.
Consider our simple add
function from vignette("porcelain")
, which takes numeric query arguments and returns a number. We can write
#' @porcelain #' GET / => json("numeric") #' query a :: numeric #' query b :: numeric add <- function(a, b) { jsonlite::unbox(a + b) } api <- function(validate = FALSE) { api <- porcelain::porcelain$new(validate = validate) api$include_package_endpoints() api }
Then running devtools::document()
(or similar) will generate the file R/porcelain.R
with a hook function that contains code that will be included when running the include_package_endpoints
method.
The basic tag parts required are
#' @porcelain <METHOD> <path> => <returning>
This string can be split across many lines and additional whitespace is ignored, so for the simple example above, we could equivalently write
#' @porcelain #' GET /add/<a:int>/<b:int> => #' json("numeric")
The arguments are all passed to porcelain::porcelain_endpoint
; the method and path are passed directly as method
and path
. The =>
symbol exists to read "returning" and help mark the end of the path.
The "returning" argument is transformed before being passed through. In the example above json("numeric")
became porcelain::porcelain_returning_json("numeric")
We translate with the rule:
json
: porcelain::porcelain_returning_json
binary
: porcelain::porcelain_returning_binary
generic
porcelain::porcelain_returning
If if no arguments are provided we perform a call with no arguments (e.g., GET /path => json
would use porcelain::porcelain_returning_json()
). If arguments are given, then we will attempt to auto-quote these.
The above function used query parameters to provide inputs to the target function; you can specify query, body and bound state this way (path parameters remain in the path, as before).
All input parameters have the format
#' <location> <name> :: <description>
where
<location>
is one of query
, body
or state
<name>
is the name of the argument in the target function that you will send the input to<description>
is one or more arguments that describes the input furtherThe interface here is a little different to the underlying porcelain functions (porcelain::porcelain_input_query
, porcelain::porcelain_input_body
and porcelain::porcelain_state
) in that every input parameter is given individually even if you might treat these together in the functions. For example, for two input queries we used two lines.
For query inputs, the description will be one of the valid types for porcelain::porcelain_input_query
; logical
, integer
, numeric
or string
, for example an endpoint accepting two query inputs:
#' @porcelain #' POST /path => json(OutputSchema) #' query a :: numeric #' query b :: integer f <- function(a, b) { # implementation }
For a json body it is likely that you will want to add a schema
#' @porcelain #' POST /path => json(OutputSchema) #' query a :: numeric #' query b :: integer #' body data :: json(InputSchema) f <- function(a, b, data) { ... }
For a binary body you can optionally specify the incoming mime type, for example
#' body data :: binary(application/zip)
would refer to a zip input.
No special markup is required for path parameters, include these using plumber's syntax
#' @porcelain #' POST /path/<a>/<b:int> => json(OutputSchema) f <- function(a, b) { ... }
Finally consider binding state into the API. We need to do this where we have some mutable state in the API that endpoints (typically POST
or DELETE
) will modify. Examples include database connections or queues.
Suppose that you have an endpoint that will count the number of times that it has been accessed, returning that number. We might use a counter like this:
counter <- R6::R6Class( "counter", private = list(n = 0), public = list( value = function() { private$n }, increase = function() { private$n <- private$n + 1 private$n }))
Outside of porcelain we can use this like so:
obj <- counter$new() obj$increase() obj$increase() obj$value()
We can write a set of endpoints that that share a counter
object like this, and add roxygen comments to configure it:
#' @porcelain #' GET /counter/value => json(number) #' state obj :: counter increase <- function(obj) { jsonlite::unbox(obj$value()) } #' @porcelain #' POST /counter/increase => json(number) #' state obj :: counter value <- function(obj) { jsonlite::unbox(obj$increase()) }
Here we have a pair of endpoints; the first GET /counter/value
will return the value of the counter and POST /counter/increase
will increase its value by one and return that. We could have other methods like POST /counter/reset
to reset the counter, but the key thing is that all endpoints must share the same counter and to do that we must pass it to the api when we create it.
Here we say that doing POST /counter
will return a number. The endpoint takes a counter
object as above, but that won't come from the HTTP request - it will be bound into the API, so it might be shared. By adding the roxygen comment
#' state obj :: counter
we set this up. We need to pass the state through when creating the API:
api <- function(validate = FALSE) { state <- list(counter = counter$new()) api <- porcelain::porcelain$new(validate = validate) api$include_package_endpoints(state) api }
Here our api creation function creates a new zeroed counter (you could accept one as an argument of course), puts that into a list with name counter
, corresponding to the rhs of the roxygen comment, and passes that through to include_package_endpoints
.
Because the code is generated into the porcelain.R file you need to use porcelain::porcelain_package_endpoint
in order to extract the raw endpoint object to take advantage of porcelain's test helper.
endpoint <- porcelain::porcelain_package_endpoint("mypkg", "GET", "/path") endpoint$run()
We include a very small complete example
tmp <- tempfile() dir.create(tmp, FALSE, TRUE) file.copy(porcelain_file("examples/add2"), tmp, recursive = TRUE) path <- file.path(tmp, "add2")
withr::with_dir(dirname(path), fs::dir_tree("add2"))
As for the simple example in vignette("porcelain")
we have written the api into a single file R/api.R
but this could be split over as many files as you prefer
show_file(file.path(path, "R/api.R"), "r")
Our DESCRIPTION
file includes the roxygen2 setup
show_file(file.path(path, "DESCRIPTION"), "")
We include a small json schema inst/schema/numeric.json
:
show_file(file.path(path, "inst/schema/numeric.json"), "json")
Running roxygen2::roxygenize(path)
on the file will build the interface
roxygen2::roxygenize(path)
The contents now include R/porcelain.R
(and man
, created by roxygen2 but empty)
withr::with_dir(dirname(path), fs::dir_tree("add2"))
The R/porcelain.R
file contains automatically-generated endpoint definitions
show_file(file.path(path, "R/porcelain.R"), "r")
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.