Skip to content

Commit fe148de

Browse files
committed
Add graphics serializers and make "..." select serializers of the same type
1 parent 7356b99 commit fe148de

File tree

6 files changed

+249
-79
lines changed

6 files changed

+249
-79
lines changed

NAMESPACE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,22 @@ export(api_session_cookie)
6363
export(api_statics)
6464
export(api_trace)
6565
export(api_trace_header)
66+
export(device_formatter)
67+
export(format_bmp)
6668
export(format_cat)
6769
export(format_csv)
6870
export(format_feather)
6971
export(format_format)
7072
export(format_geojson)
7173
export(format_htmlwidget)
74+
export(format_jpeg)
7275
export(format_parquet)
76+
export(format_pdf)
77+
export(format_png)
7378
export(format_print)
7479
export(format_rds)
80+
export(format_svg)
81+
export(format_tiff)
7582
export(format_tsv)
7683
export(format_unboxed)
7784
export(format_yaml)

R/parsers.R

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,6 @@ get_parsers <- function(parsers = NULL) {
9898
return(list2(!!elem_names[i] := parsers[[i]]))
9999
}
100100
if (elem_names[i] == "" && is_character(parsers[[i]])) {
101-
if (any(grepl("/", parsers[[i]], fixed = TRUE))) {
102-
cli::cli_abort("mime types must be provided with a function")
103-
}
104101
return(get_parsers_internal(
105102
parsers[[i]],
106103
env = env,

R/serializers.R

Lines changed: 163 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ registry$serializers <- list()
2626
#'
2727
#' @export
2828
#'
29-
register_serializer <- function(name, fun, mime_type) {
29+
register_serializer <- function(name, fun, mime_type, graphic = FALSE) {
3030
check_function(fun)
3131
check_string(mime_type)
3232
check_string(name)
33+
check_bool(graphic)
3334
if (grepl("/", name, fixed = TRUE)) {
3435
cli::cli_abort(
3536
"{.arg name} must not contain the forward slash character ({.field /})"
@@ -40,7 +41,11 @@ register_serializer <- function(name, fun, mime_type) {
4041
"{.arg name} must not be {.val {c('...', 'none')}}"
4142
)
4243
}
43-
registry$serializers[[name]] <- list(fun = fun, type = mime_type)
44+
registry$serializers[[name]] <- list(
45+
fun = fun,
46+
type = mime_type,
47+
graphic = graphic
48+
)
4449
invisible(NULL)
4550
}
4651

@@ -51,13 +56,18 @@ register_serializer <- function(name, fun, mime_type) {
5156
#' * Any unnamed elements containing a character vector will be considered as
5257
#' names of registered serializers constructed with default values. The
5358
#' special value `"..."` will fetch all the serializers that are otherwise not
54-
#' specified in the call
59+
#' specified in the call.
5560
#' * Any element containing a function are considered as a provided serializer
5661
#' and the element must be named by the mime type the serializer understands
5762
#' * Any remaining named elements will be considered names of registered
5863
#' serializers that should be constructed with the arguments given in the
5964
#' element
6065
#'
66+
#' @note Using the `...` will provide remaining graphics serializers if a
67+
#' graphics serializer is explicitely requested elsewhere. Otherwise it will
68+
#' provide the remaining non-graphics serializers. A warning is thrown if a mix
69+
#' of graphics and non-graphics serializers are requested.
70+
#'
6171
#' @export
6272
get_serializers <- function(serializers = NULL) {
6373
if (is.null(serializers)) {
@@ -97,13 +107,11 @@ get_serializers <- function(serializers = NULL) {
97107
return(list2(!!elem_names[i] := serializers[[i]]))
98108
}
99109
if (elem_names[i] == "" && is_character(serializers[[i]])) {
100-
if (any(grepl("/", serializers[[i]], fixed = TRUE))) {
101-
cli::cli_abort("mime types must be provided with a function")
102-
}
103110
return(get_serializers_internal(
104111
serializers[[i]],
105112
env = env,
106-
dots_serializers = dots_serializers
113+
dots_serializers = dots_serializers,
114+
prune_dots = FALSE
107115
))
108116
}
109117
if (elem_names[i] != "") {
@@ -119,13 +127,29 @@ get_serializers <- function(serializers = NULL) {
119127
}
120128
cli::cli_abort("Don't know how to parse element {i} in {.arg serializers}")
121129
})
122-
unlist(serializers, recursive = FALSE)
130+
from_dots <- unlist(lapply(
131+
serializers,
132+
function(x) attr(x, "from_dots") %||% rep_along(x, FALSE)
133+
))
134+
serializers <- unlist(serializers, recursive = FALSE)
135+
is_graphics <- vapply(serializers, is_device_formatter, logical(1))
136+
use_graphics <- if (all(from_dots)) FALSE else is_graphics[!from_dots][1]
137+
keep <- !from_dots | is_graphics == use_graphics
138+
serializers <- serializers[keep]
139+
is_graphics <- is_graphics[keep]
140+
if (!(all(is_graphics) || all(!is_graphics))) {
141+
cli::cli_warn(
142+
"Serializers are a mix of standard and graphics serializers"
143+
)
144+
}
145+
serializers
123146
}
124147

125148
get_serializers_internal <- function(
126149
types = NULL,
127150
env = caller_env(),
128-
dots_serializers = NULL
151+
dots_serializers = NULL,
152+
prune_dots = TRUE
129153
) {
130154
if (isTRUE(tolower(types) == "none")) {
131155
return(NULL)
@@ -134,10 +158,19 @@ get_serializers_internal <- function(
134158
types <- names(registry$serializers)
135159
}
136160
dots <- which(types == "...")
161+
from_dots <- rep_along(types, FALSE)
137162
if (length(dots) != 0) {
163+
if (length(dots) > 1) {
164+
cli::cli_abort("{.val ...} can only be used once")
165+
}
166+
dnames <- dots_serializers %||% setdiff(names(registry$serializers), types)
167+
from_dots <- rep(
168+
c(FALSE, TRUE, FALSE),
169+
c(dots - 1, length(dnames), length(types) - dots)
170+
)
138171
types <- c(
139172
types[seq_len(dots - 1)],
140-
dots_serializers %||% setdiff(names(registry$serializers), types),
173+
dnames,
141174
types[dots + seq_len(length(types) - dots)]
142175
)
143176
}
@@ -177,7 +210,22 @@ get_serializers_internal <- function(
177210
lapply(serializers, `[[`, "fun"),
178211
vapply(serializers, `[[`, character(1), "type")
179212
)
180-
serializers[!duplicated(names(serializers))]
213+
is_graphics <- vapply(serializers, is_device_formatter, logical(1))
214+
if (prune_dots) {
215+
use_graphics <- if (all(from_dots)) FALSE else is_graphics[!from_dots][1]
216+
keep <- !from_dots | is_graphics == use_graphics
217+
serializers <- serializers[keep]
218+
is_graphics <- is_graphics[keep]
219+
if (!(all(is_graphics) || all(!is_graphics))) {
220+
cli::cli_warn(
221+
"Serializers are a mix of standard and graphics serializers"
222+
)
223+
}
224+
}
225+
structure(
226+
serializers[!duplicated(names(serializers))],
227+
from_dots = from_dots[!duplicated(names(serializers))]
228+
)
181229
}
182230

183231
# Default serializers ----------------------------------------------------------
@@ -195,25 +243,25 @@ get_serializers_internal <- function(
195243
#' as `"tsv"` to the mime type `text/tsv`
196244
#' * `format_rds()` uses [serialize()] for formatting. It is registered as
197245
#' `"rds"` to the mime type `application/rds`
198-
#' * `format_geojson`uses [geojsonsf::sfc_geojson()] or [geojsonsf::sf_geojson()]
246+
#' * `format_geojson()` uses [geojsonsf::sfc_geojson()] or [geojsonsf::sf_geojson()]
199247
#' for formatting depending on the class of the response body. It is
200248
#' registered as `"geojson"` to the mime type `application/geo+json`
201-
#' * `format_feather`uses [arrow::write_feather()] for formatting. It is
249+
#' * `format_feather()` uses [arrow::write_feather()] for formatting. It is
202250
#' registered as `"feather"` to the mime type
203251
#' `application/vnd.apache.arrow.file`
204-
#' * `format_parquet`uses [nanoparquet::write_parquet()] for formatting. It is
252+
#' * `format_parquet()` uses [nanoparquet::write_parquet()] for formatting. It is
205253
#' registered as `"parquet"` to the mime type `application/vnd.apache.parquet`
206-
#' * `format_yaml`uses [yaml::as.yaml()] for formatting. It is registered
254+
#' * `format_yaml()` uses [yaml::as.yaml()] for formatting. It is registered
207255
#' as `"yaml"` to the mime type `text/yaml`
208-
#' * `format_htmlwidget`uses [htmlwidgets::saveWidget()] for formatting. It is
256+
#' * `format_htmlwidget()` uses [htmlwidgets::saveWidget()] for formatting. It is
209257
#' registered as `"htmlwidget"` to the mime type `text/html`
210-
#' * `format_format`uses [format()] for formatting. It is registered
258+
#' * `format_format()` uses [format()] for formatting. It is registered
211259
#' as `"format"` to the mime type `text/plain`
212-
#' * `format_print`uses [print()] for formatting. It is registered
260+
#' * `format_print()` uses [print()] for formatting. It is registered
213261
#' as `"print"` to the mime type `text/plain`
214-
#' * `format_cat`uses [cat()] for formatting. It is registered
262+
#' * `format_cat()` uses [cat()] for formatting. It is registered
215263
#' as `"cat"` to the mime type `text/plain`
216-
#' * `format_unboxed`uses [reqres::format_json()] with `auto_unbox = TRUE` for
264+
#' * `format_unboxed()` uses [reqres::format_json()] with `auto_unbox = TRUE` for
217265
#' formatting. It is registered as `"unboxedJSON"` to the mime type
218266
#' `application/json`
219267
#'
@@ -227,6 +275,25 @@ get_serializers_internal <- function(
227275
#' * [reqres::format_plain()] is registered as "`text`" to the mime type
228276
#' `text/plain`
229277
#'
278+
#' # Provided graphics serializers
279+
#' Serializing graphic output is special because it requires operations before
280+
#' and after the handler is executed. Further, handlers creating graphics are
281+
#' expected to do so through side-effects (ie. call to graphics rendering) or
282+
#' by returning a ggplot2 object. If you want to create your own graphics
283+
#' serializer you should use [device_formatter()] for constructing it.
284+
#' * `format_png()` uses [ragg::agg_png()] for rendering. It is registered
285+
#' as `"png"` to the mime type `image/png`
286+
#' * `format_jpeg()` uses [ragg::agg_jpeg()] for rendering. It is registered
287+
#' as `"jpeg"` to the mime type `image/jpeg`
288+
#' * `format_tiff()` uses [ragg::agg_tiff()] for rendering. It is registered
289+
#' as `"tiff"` to the mime type `image/tiff`
290+
#' * `format_svg()` uses [svglite::svglite()] for rendering. It is registered
291+
#' as `"svg"` to the mime type `image/svg+xml`
292+
#' * `format_bmp()` uses [grDevices::bmp()] for rendering. It is registered
293+
#' as `"bmp"` to the mime type `image/bmp`
294+
#' * `format_pdf()` uses [grDevices::pdf()] for rendering. It is registered
295+
#' as `"pdf"` to the mime type `application/pdf`
296+
#'
230297
#' @param ... Further argument passed on to the internal formatting function.
231298
#' See Details for information on which function handles the formatting
232299
#' internally in each serializer
@@ -392,10 +459,19 @@ on_load({
392459

393460
# Device serializers -----------------------------------------------------------
394461

395-
# TODO: Somehow, we need to keep these distinct from the other serializers so
396-
# that ... when used together with a device serializer only retrieves other
397-
# device serializers and vice versa
398-
462+
#' Create a graphics device formatter
463+
#'
464+
#' This internal function facilitates creating a formatter that uses a specific
465+
#' device for rendering.
466+
#'
467+
#' @param dev_open The function that opens the device
468+
#' @param dev_close The function closing the device. Usually this would be
469+
#' [grDevices::dev.off()]
470+
#'
471+
#' @return A device formatter function
472+
#' @keywords internal
473+
#' @export
474+
#'
399475
device_formatter <- function(dev_open, dev_close = grDevices::dev.off()) {
400476
dev_name <- caller_arg(dev_open)
401477
check_function(dev_open)
@@ -408,50 +484,54 @@ device_formatter <- function(dev_open, dev_close = grDevices::dev.off()) {
408484
)
409485
}
410486
}
411-
function(...) {
412-
provided_args <- names(enquos(...))
413-
dev_args <- fn_fmls_names(dev_open)
414-
extra_args <- setdiff(provided_args, dev_args)
415-
if (length(extra_args) != 0 && !"..." %in% dev_args) {
416-
cli::cli_abort(
417-
"Provided arguments does not match arguments in {.fun {dev_name}}"
418-
)
419-
}
420-
init_dev <- function() {
421-
output_file <- tempfile()
422-
dev_open(filename = output_file, ...)
423-
dev_id <- grDevices::dev.cur()
424-
list(path = output_file, dev = dev_id)
425-
}
426-
close_dev <- function(info) {
427-
grDevices::dev.set(info$dev)
428-
grDevices::dev.off()
429-
if (!file.exists(info$path)) {
430-
return(NULL)
487+
structure(
488+
function(...) {
489+
provided_args <- names(enquos(...))
490+
dev_args <- fn_fmls_names(dev_open)
491+
extra_args <- setdiff(provided_args, dev_args)
492+
if (length(extra_args) != 0 && !"..." %in% dev_args) {
493+
cli::cli_abort(
494+
"Provided arguments does not match arguments in {.fun {dev_name}}"
495+
)
496+
}
497+
init_dev <- function() {
498+
output_file <- tempfile()
499+
dev_open(filename = output_file, ...)
500+
dev_id <- grDevices::dev.cur()
501+
list(path = output_file, dev = dev_id)
431502
}
432-
con <- file(info$path, "rb")
433-
on.exit(
434-
{
435-
close(con)
436-
unlink(info$path)
437-
},
438-
add = TRUE
503+
close_dev <- function(info) {
504+
grDevices::dev.set(info$dev)
505+
grDevices::dev.off()
506+
if (!file.exists(info$path)) {
507+
return(NULL)
508+
}
509+
con <- file(info$path, "rb")
510+
on.exit(
511+
{
512+
close(con)
513+
unlink(info$path)
514+
},
515+
add = TRUE
516+
)
517+
readBin(con, "raw", file.info(info$path)$size)
518+
}
519+
clean_dev <- function(info) {
520+
grDevices::dev.set(info$dev)
521+
grDevices::dev.off()
522+
unlink(info$path)
523+
}
524+
structure(
525+
identity,
526+
init = init_dev,
527+
close = close_dev,
528+
clean = clean_dev,
529+
class = "device_formatter"
439530
)
440-
readBin(con, "raw", file.info(info$path)$size)
441-
}
442-
clean_dev <- function(info) {
443-
grDevices::dev.set(info$dev)
444-
grDevices::dev.off()
445-
unlink(info$path)
446531
}
447-
structure(
448-
identity,
449-
init = init_dev,
450-
close = close_dev,
451-
clean = clean_dev
452-
)
453-
}
532+
)
454533
}
534+
is_device_formatter <- function(x) inherits(x, "device_formatter")
455535

456536
init_formatter <- function(formatter) {
457537
init_fun <- attr(formatter, "init")
@@ -477,22 +557,40 @@ clean_formatter <- function(formatter, info) {
477557
clean_fun(info)
478558
}
479559

560+
#' @rdname serializers
561+
#' @export
480562
#' @importFrom ragg agg_png
563+
#'
481564
format_png <- device_formatter(agg_png)
565+
#' @rdname serializers
566+
#' @export
482567
#' @importFrom ragg agg_jpeg
568+
#'
483569
format_jpeg <- device_formatter(agg_jpeg)
570+
#' @rdname serializers
571+
#' @export
484572
#' @importFrom ragg agg_tiff
573+
#'
485574
format_tiff <- device_formatter(agg_tiff)
575+
#' @rdname serializers
576+
#' @export
486577
#' @importFrom svglite svglite
578+
#'
487579
format_svg <- device_formatter(svglite)
580+
#' @rdname serializers
581+
#' @export
582+
#'
488583
format_bmp <- device_formatter(grDevices::bmp)
584+
#' @rdname serializers
585+
#' @export
586+
#'
489587
format_pdf <- device_formatter(grDevices::pdf)
490588

491589
on_load({
492590
register_serializer("png", format_png, "image/png")
493591
register_serializer("jpeg", format_jpeg, "image/jpeg")
494592
register_serializer("tiff", format_tiff, "image/tiff")
495593
register_serializer("svg", format_svg, "image/svg+xml")
496-
register_serializer("bmp", format_bmp, "aimage/bmp")
594+
register_serializer("bmp", format_bmp, "image/bmp")
497595
register_serializer("pdf", format_pdf, "application/pdf")
498596
})

0 commit comments

Comments
 (0)