-
Notifications
You must be signed in to change notification settings - Fork 257
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
Conversation
} | ||
|
||
dev_requires_req <- "req" %in% names(formals(dev_on)) |
There was a problem hiding this comment.
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
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=`) | |
} | |
} |
There was a problem hiding this comment.
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!)
ignored <- Map(function(x) { | ||
params[[x]] <<- NULL | ||
}, all_except) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might work?
** Untested
ignored <- Map(function(x) { | |
params[[x]] <<- NULL | |
}, all_except) | |
params[all_except] <- NULL |
#' \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} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#' \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! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#' @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 |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)
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)) |
There was a problem hiding this comment.
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") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same change as above
if (getOption("staticSerializers", default="FALSE") == "TRUE") { | |
if (isTRUE(getOption("plumber.staticSerializers", default=FALSE))) { |
if (is.null(option)) { | ||
NULL | ||
} else { | ||
as.numeric(option) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cosmetic
if (is.null(option)) { | |
NULL | |
} else { | |
as.numeric(option) | |
} | |
if (is.null(option)) return(NULL) | |
as.numeric(option) |
dimension_args <- serialize_dimensions_args_preparer(req, ...) | ||
rlang::exec(grFunc, filename, !!!dimension_args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cosmetic
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, ...) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Matching cosmetic change below
serialize_dimensions_args_preparer <- function(req, ...) { | |
serialize_dev_args_preparer <- function(req, ...) { |
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 |
There was a problem hiding this comment.
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
- Params in
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)
There was a problem hiding this comment.
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:
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)
# 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() | ||
} |
There was a problem hiding this comment.
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.
# 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() |
#' @description retrieve serializer endpoint parameters | ||
getSerializerParams = function() { | ||
if (is.null(self$serializer_params)) { | ||
return(list()) | ||
} | ||
return(self$serializer_params) | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#' @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()) { |
There was a problem hiding this comment.
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.
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()) { |
There was a problem hiding this comment.
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.
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( |
There was a problem hiding this comment.
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
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( |
#' @field serializer_params serializer parameters | ||
serializer_params = NULL, |
There was a problem hiding this comment.
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. :-/
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() | ||
) | ||
} |
There was a problem hiding this comment.
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.
Closing - too hard to maintain long running (and stale) PRs |
Pull Request
First attempt at #837
Still to do:
To consider
Minimal reproducible example
Have run:
This shows as:
and:
Seems to work... 👍