Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Plot Endpoints #897

Closed
wants to merge 2 commits into from
Closed

Dynamic Plot Endpoints #897

wants to merge 2 commits into from

Conversation

slodge
Copy link

@slodge slodge commented Jan 4, 2023

Pull Request

First attempt at #837

Still to do:

  • Lots of QA - both manual and unit tests
  • Lots of Docs
  • Merging with the other open PRs - this is going to be a bit of a mess now :/
  • News.md
  • devtools::Check Package
  • Review
  • Possibly more granular control on parameters - do we want to turn things on/off per endpoint? do we want more options? (e.g. quality)? do we want less?

To consider

  • breaking backward compatibility - don't think it does - but it's possible...
  • is hard to understand - possibly
  • is hard to maintain in the future - possibly ... but no worse than the rest of plumber? there's some technical debt building up in the open api params I think

Minimal reproducible example

Have run:

devtools::load_all()
library(tidyverse)

#* @apiTitle Plumber Example API
#* @apiDescription Plumber example description.

#* Plot out data from the iris dataset
#* @param spec If provided, filter the data to only this species (e.g. 'setosa')
#* @get /plot
#* @serializer png
function(spec = "setosa"){
  myData <- iris
  title <- "All Species"

  # Filter if the species was specified
  if (!missing(spec)){
    title <- paste0("Only the '", spec, "' Species")
    myData <- subset(iris, Species == spec)
  }

  plot(myData$Sepal.Length, myData$Petal.Length,
       main=title, xlab="Sepal Length", ylab="Petal Length")
}

This shows as:
image

and:
image

Seems to work... 👍

@CLAassistant
Copy link

CLAassistant commented Jan 4, 2023

CLA assistant check
All committers have signed the CLA.

@slodge slodge changed the title Dynamic endpoints Dynamic Plot Endpoints Jan 4, 2023
}

dev_requires_req <- "req" %in% names(formals(dev_on))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding validations on dev_on()

** Untested

Suggested change
dev_requires_req <- "req" %in% names(formals(dev_on))
dev_on_arg_names <- names(formals(dev_on))
dev_on_requires_req <- "req" %in% dev_on_arg_names
# Make sure extra args exist
if (dev_on_requires_req) {
dots_pos <- which("..." %in% dev_on_arg_names
if (length(dots_pos) == 0) stop("`dev_on()` must contain arguments `...` if using `req`")
req_pos <- which("..." %in% dev_on_arg_names
if (!isTRUE(dots_pos < req_pos)) {
stop("`dev_on()` must have arguments `...` before `req=`)
}
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the immediate feedback...

Given how much there is, it might be an idea to make and edit these changes in an editor and to add some unit tests as we go?

If it helps, we can hold off on this (and the other PRs) until RStudio have capacity to invest the time? (That might also help me negotiate some wriggle room with my work pressures too!)

Comment on lines +611 to +613
ignored <- Map(function(x) {
params[[x]] <<- NULL
}, all_except)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might work?

** Untested

Suggested change
ignored <- Map(function(x) {
params[[x]] <<- NULL
}, all_except)
params[all_except] <- NULL

Comment on lines +44 to +46
#' \item{`plumber.staticSerializers`}{Plumber will use fixed serializers and
#' will not interpret e.g. plot_width and plot_height parameters as plot image
#' size instructions}
Copy link
Collaborator

@schloerke schloerke Jan 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#' \item{`plumber.staticSerializers`}{Plumber will use fixed serializers and
#' will not interpret e.g. plot_width and plot_height parameters as plot image
#' size instructions}
#' \item{`plumber.staticSerializers`}{If `TRUE`, Plumber will use fixed serializers and
#' will not interpret e.g. `plot_width` and `plot_height` parameters as plot image
#' size instructions. Defaults to `FALSE`}

@@ -446,6 +446,7 @@ serializer_xml <- function() {
#' @param preexec_hook Function to be run directly before a [PlumberEndpoint] calls its route method.
#' @param postexec_hook Function to be run directly after a [PlumberEndpoint] calls its route method.
#' @param aroundexec_hook Function to be run around a [PlumberEndpoint] call. Must handle a `.next` argument to continue execution. \lifecycle{experimental}
#' @param serializer_params Dynamic serializer parameters. More docs needed here!
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#' @param serializer_params Dynamic serializer parameters. More docs needed here!
#' @param serializer_params Dynamic serializer parameters. For internal use.

@@ -497,26 +501,37 @@ self_set_serializer <- function(self, serializer) {
#' The graphics device `dev_on` function will receive any arguments supplied to the serializer in addition to `filename`.
#' `filename` points to the temporary file name that should be used when saving content.
#' @param dev_off Function to turn off the graphics device. Defaults to [grDevices::dev.off()]
#' @param serializer_params More docs needed here
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO

stopifnot(is.function(dev_off))

endpoint_serializer(
serializer = serializer_content_type(type),
aroundexec_hook = function(..., .next) {
tmpfile <- tempfile()

dev_on(filename = tmpfile)
if (dev_requires_req) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Using larger name from above suggestion)

Suggested change
if (dev_requires_req) {
if (dev_on_requires_req) {

list(plot_units = list(desc="Units of plot image", type="string", required=FALSE, isArray=FALSE))
}
serializer_param_pointsize <- function() {
list(plot_pointsize = list(desc="Point size of plot image - TODO - better desc", type="number", required=FALSE, isArray=FALSE))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO


# if legacy plumber.staticSerializers is specified then ignore all req
# based parameters
if (getOption("staticSerializers", default="FALSE") == "TRUE") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same change as above

Suggested change
if (getOption("staticSerializers", default="FALSE") == "TRUE") {
if (isTRUE(getOption("plumber.staticSerializers", default=FALSE))) {

Comment on lines +629 to +633
if (is.null(option)) {
NULL
} else {
as.numeric(option)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cosmetic

Suggested change
if (is.null(option)) {
NULL
} else {
as.numeric(option)
}
if (is.null(option)) return(NULL)
as.numeric(option)

Comment on lines +649 to +650
dimension_args <- serialize_dimensions_args_preparer(req, ...)
rlang::exec(grFunc, filename, !!!dimension_args)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cosmetic

Suggested change
dimension_args <- serialize_dimensions_args_preparer(req, ...)
rlang::exec(grFunc, filename, !!!dimension_args)
dev_args <- serialize_dev_args_preparer(req, ...)
rlang::exec(grFunc, filename, !!!dev_args)

}


serialize_dimensions_args_preparer <- function(req, ...) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matching cosmetic change below

Suggested change
serialize_dimensions_args_preparer <- function(req, ...) {
serialize_dev_args_preparer <- function(req, ...) {

Comment on lines +637 to +642
doc_args$width <- as_numeric_nullable(req$args$plot_width, doc_args$width)
doc_args$height <- as_numeric_nullable(req$args$plot_height, doc_args$height)
doc_args$units <- req$args$plot_units %||% doc_args$units
doc_args$res <- as_numeric_nullable(req$args$plot_res, doc_args$res)
doc_args$pointsize <- as_numeric_nullable(req$args$plot_pointsize, doc_args$pointsize)
doc_args$bg <- req$args$plot_bg %||% doc_args$bg
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is your motivation to use plot_* for the arg names?
Why not pass them through directly?

My interpretation:

  • Minimal param clashing with plot_*
  • Confusion of two differently named parameters
    • Params in @serializer png list(width=200)
    • Params in http://....../plot_route?plot_width=200

If there isn't too much motivation for using plot_* names, I'd like to keep the consistent arg names when possible. (Knowing there could be param name clashing with the regular route params)

Copy link
Author

@slodge slodge Jan 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As well as avoiding clashes, my main motivation behind plot_ names was to make it easier to distinguish the plot parameters from the business logic - e.g. in https://.../palmerPenguins?min_flipper_length=200&min_bill_length=40&plot_width=400&plot_height=500

The naming also makes the business logic vs plot parameter separation obvious in the Swagger UI (maybe some more ordering there might also help?)

e.g. it's obvious here which params are business vs which are plot:
image

Asides:

  • initially I was wondering about using e.g. plot.width to try to make the plot parameters look like a separate object in the REST url (but couldn't spot any docs on that in https://swagger.io/docs/specification/describing-parameters/)
  • also wondering whether we should allow HTTP header insertion of these parameters (not sure that's very RESTFUL though)

Comment on lines +21 to +28
# if legacy plumber.staticSerializers is specified then ignore all req serializer
# based parameters
if (getOption("plumber.staticSerializers", default="FALSE") == "TRUE") {
serializerParams <- list()
} else {
# Get the plumber serializer defined endpoint params
serializerParams <- routerEndpointEntry$getSerializerParams()
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's let the endpoint determine it's serializer params.

This will allow for the endpoint to have full control vs using a globally set flag.

Counter example app: API with two routes: 1 w/ dynamic serializer and 1 w/ static serializer.

Suggested change
# if legacy plumber.staticSerializers is specified then ignore all req serializer
# based parameters
if (getOption("plumber.staticSerializers", default="FALSE") == "TRUE") {
serializerParams <- list()
} else {
# Get the plumber serializer defined endpoint params
serializerParams <- routerEndpointEntry$getSerializerParams()
}
# Get the plumber serializer defined endpoint params
serializerParams <- routerEndpointEntry$getSerializerParams()

Comment on lines +301 to +307
#' @description retrieve serializer endpoint parameters
getSerializerParams = function() {
if (is.null(self$serializer_params)) {
return(list())
}
return(self$serializer_params)
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#' @description retrieve serializer endpoint parameters
getSerializerParams = function() {
if (is.null(self$serializer_params)) {
return(list())
}
return(self$serializer_params)
},
#' @description retrieve serializer endpoint parameters
getSerializerParams = function() {
self$serializer_params %||% list()
},

#' @export
serializer_device <- function(type, dev_on, dev_off = grDevices::dev.off) {
serializer_device <- function(type, dev_on, dev_off = grDevices::dev.off, serializer_params = list()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use a rlang::missing_arg() as a placeholder and handle it if it is missing via the option value.

Suggested change
serializer_device <- function(type, dev_on, dev_off = grDevices::dev.off, serializer_params = list()) {
serializer_device <- function(type, dev_on, dev_off = grDevices::dev.off, serializer_params = missing_arg()) {

#' @export
serializer_device <- function(type, dev_on, dev_off = grDevices::dev.off) {
serializer_device <- function(type, dev_on, dev_off = grDevices::dev.off, serializer_params = list()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use a rlang::missing_arg() as a placeholder and handle it if it is missing via the option value.

Suggested change
serializer_device <- function(type, dev_on, dev_off = grDevices::dev.off, serializer_params = list()) {
serializer_device <- function(type, dev_on, dev_off = grDevices::dev.off, serializer_params = missing_arg()) {

}

dev_requires_req <- "req" %in% names(formals(dev_on))

stopifnot(is.function(dev_off))

endpoint_serializer(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handling the serializer_params

** Untested

Suggested change
endpoint_serializer(
serializer_params <- rlang::maybe_missing(serializer_params, {
# Legacy support for static serializer parameters
if (isTRUE(getOption("plumber.staticSerializers", default=FALSE)) return(list())
# All known parameters for devices
serializer_param_list()
})
endpoint_serializer(

Comment on lines +199 to +200
#' @field serializer_params serializer parameters
serializer_params = NULL,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As much as I don't like it, let's make the camelCase and change to dynamicSerializerParams (or something to hint that it is the dynamic serializer params... dynSerParams?)

Will need to change all usage as well. :-/

Comment on lines 657 to 663
serializer_jpeg <- function(..., type = "image/jpeg") {
serializer_device(
type = type,
dev_on = function(filename) {
grDevices::jpeg(filename, ...)
}
dev_on = serializer_image_dev_on_func(grDevices::jpeg, ...),
serializer_params = serializer_param_list()
)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should allow users to determine if the dynamic params are used as the serializer level.

Maybe something like:

get_serializer_params <- function(dynamic_params) {
  has_dynamic_params <- 
    rlang::maybe_missing(dynamic_params, {
      # Legacy support for static serializer parameters
      if (isTRUE(getOption("plumber.staticSerializers", default=FALSE)) return(FALSE)
      # Support dynamic params
      TRUE
    })
  if (!has_dynamic_params) return(list())
  serializer_param_list()
}
serializer_jpeg <- function(..., type = "image/jpeg", dynamic_params = missing_arg()) {
  serializer_device(
    type = type,
    dev_on = serializer_image_dev_on_func(grDevices::jpeg, ...),
    serializer_params = get_serializer_params(dynamic_params)
  )
}

Will need to document the dynamic_params parameter.

@slodge
Copy link
Author

slodge commented Feb 22, 2023

Closing - too hard to maintain long running (and stale) PRs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants