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)
}

porcelain is a package which allows creation of robust, testable HTTP API's from R. These can be used to expose any R calculation to other applications while communicating explicit expectations for inputs and outputs. It is implemented on top of the excellent plumber package and uses plumber's programmatic interface to build its API. The interface, goals and tradeoffs are very different to plumber, but we think that this leads to high-quality APIs that are easy to put into production.

Motivation

The plumber package makes it trivially easy to create APIs from R. For example, from the documentation

#* Return the sum of two numbers
#* @param a The first number to add
#* @param b The second number to add
#* @post /sum
function(a, b) {
  as.numeric(a) + as.numeric(b)
}

Starting this API is easy,

library(plumber)
# 'plumber.R' is the location of the file shown above
pr("plumber.R") %>%
  pr_run(port=8000)

and we can interact with it using curl:

curl -w"\n" --data "a=4&b=3" "http://localhost:8000/sum"
[7]

This simple example illustrates several issues that porcelain tries to solve:

Introduction - a porcelain approach to adding-as-a-service

We start with a very different approach to plumber, one that is undeniably much more work to get started with as everything will be implemented in a package. We'll start by creating a version of the above example (adding two arguments) which makes explicit some of the type decisions here.

To begin, we'll write an endpoint that takes two arguments as query parameters (not body parameters as in the plumber example). We will do this in a small package:

withr::with_dir(porcelain_file("examples"), fs::dir_tree("add"))

The DESCRIPTION file is minimal, importing porcelain and suggesting testthat for the package tests.

show_file(porcelain_file("examples/add/DESCRIPTION"), "")

The NAMESPACE file exports the api function (described below)

show_file(porcelain_file("examples/add/NAMESPACE"), "")

Most of the work is in R/api.R which implements the API (there is nothing special about this filename)

show_file(porcelain_file("examples/add/R/api.R"), "r")

There are three functions here:

With this, you could bring up the api with

add::api()$run()

At this point, the porcelain approach probably just looks like much more work than the 7 lines of plumber code above!

The benefit comes from the type checking we can do in the endpoint function, and the fact that these functions are in a package so are easily testable.

In the endpoint we have

porcelain::porcelain_input_query(a = "numeric", b = "numeric")

which advertises (and enforces) that the query parameters a and b expected and numeric - additional query parameters provided will raise an error. Similarly, the argument

returning = porcelain::porcelain_returning_json("numeric")

indicates that we will be returning json with a numeric value. This is different to the numeric above, as here it refers to a json schema - this is included in the package within inst/schema as numeric.json

show_file(porcelain_file("examples/add/inst/schema/numeric.json"), "json")

This is going to turn out to be much more useful with more complex apis.

Finally, the package has tests, and with this we can make explicit some of the questions posed in the motivation section.

show_file(porcelain_file("examples/add/tests/testthat/test-api.R"), "r")

Effectively testing such a simple API is quite hard, as we're not really doing much in the implementation, but these should give an idea of the sorts of strategies that we have used in testing our APIs:

Typically we also include a few light end-to-end tests where we bring up the API in a separate process and use httr to interact with it.



reside-ic/porcelain documentation built on March 4, 2024, 11:11 p.m.