Skip to content

Commit 7356b99

Browse files
committed
documenting parsers and serializers and provide a user facing getter for each
1 parent 02df114 commit 7356b99

11 files changed

+694
-24
lines changed

NAMESPACE

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,31 @@ export(api_session_cookie)
6363
export(api_statics)
6464
export(api_trace)
6565
export(api_trace_header)
66+
export(format_cat)
67+
export(format_csv)
68+
export(format_feather)
69+
export(format_format)
70+
export(format_geojson)
71+
export(format_htmlwidget)
72+
export(format_parquet)
73+
export(format_print)
74+
export(format_rds)
75+
export(format_tsv)
76+
export(format_unboxed)
77+
export(format_yaml)
78+
export(get_parsers)
79+
export(get_serializers)
6680
export(is_plumber_api)
81+
export(parse_csv)
82+
export(parse_feather)
83+
export(parse_geojson)
84+
export(parse_octet)
85+
export(parse_parquet)
6786
export(parse_plumber_file)
87+
export(parse_rds)
88+
export(parse_text)
89+
export(parse_tsv)
90+
export(parse_yaml)
6891
export(register_parser)
6992
export(register_serializer)
7093
import(rlang)

R/api_handlers.R

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,12 @@ handle_constructor <- function(method, header = FALSE) {
192192
#' path
193193
#' @param serializers A named list of serializers that can be used to format the
194194
#' response before sending it back to the client. Which one is selected is based
195-
#' on the request `Accept` header
195+
#' on the request `Accept` header. See [get_serializers()] for a helper to
196+
#' construct this
196197
#' @param parsers A named list of parsers that can be used to parse the
197198
#' request body before passing it in as the `body` argument. Which one is
198-
#' selected is based on the request `Content-Type` header
199+
#' selected is based on the request `Content-Type` header. See [get_parsers()]
200+
#' for a helper to construct this
199201
#' @param use_strict_serializer By default, if a serializer that respects the
200202
#' requests `Accept` header cannot be found, then the first of the provided ones
201203
#' are used. Setting this to `TRUE` will instead send back a

R/parse_plumber_file.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,15 +171,15 @@ parse_handler_block <- function(
171171
} else {
172172
serializers <- NULL
173173
}
174-
serializers <- get_serializers(serializers, env)
174+
serializers <- get_serializers_internal(serializers, env)
175175

176176
parsers <- which(tags == "parser")
177177
if (length(parsers) != 0) {
178178
parsers <- trimws(values[parsers])
179179
} else {
180180
parsers <- NULL
181181
}
182-
parsers <- get_parsers(parsers, env)
182+
parsers <- get_parsers_internal(parsers, env)
183183

184184
download <- which(tags == "download")
185185
if (length(download) != 0) {

R/parsers.R

Lines changed: 189 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
registry$parsers <- list()
22

3-
#' Register a parser to a name for use with the `@parser` tag
3+
#' Register or fetch a parser
44
#'
55
#' plumber2 comes with many parsers that should cover almost all standard
66
#' use cases. Still you might want to provide some of your own, which this
77
#' function facilitates.
88
#'
9+
#' If you want to register your own parser, then the function you register must
10+
#' be a factory function, ie. a function returning a function. The returned
11+
#' function must accept two arguments, the first being a raw vector
12+
#' corresponding to the request body, the second being the parsed directives
13+
#' from the request `Content-Type` header. All arguments to the factory function
14+
#' should be optional.
15+
#'
916
#' @param name The name to register the parser function to. If already
1017
#' present the current parser will be overwritten by the one provided by you
1118
#' @param fun A function that, when called, returns a binary function that can
@@ -15,19 +22,114 @@ registry$parsers <- list()
1522
#' @param mime_types One or more mime types that this parser can handle. The
1623
#' mime types are allowed to contain wildcards, e.g. `"text/*"`
1724
#'
18-
#' @return This function is called for its side effects
25+
#' @return For `get_parsers` a named list of parser functions named by their
26+
#' mime types. The order given in `parsers` is preserved.
1927
#'
28+
#' @seealso [parsers]
2029
#' @seealso [register_serializer()]
2130
#' @export
2231
#'
2332
register_parser <- function(name, fun, mime_types) {
2433
check_function(fun)
2534
check_character(mime_types)
35+
check_string(name)
36+
if (grepl("/", name, fixed = TRUE)) {
37+
cli::cli_abort(
38+
"{.arg name} must not contain the forward slash character ({.field /})"
39+
)
40+
}
41+
if (name %in% c("...", "none")) {
42+
cli::cli_abort(
43+
"{.arg name} must not be {.val {c('...', 'none')}}"
44+
)
45+
}
2646
registry$parsers[[name]] <- list(fun = fun, types = mime_types)
2747
invisible(NULL)
2848
}
2949

30-
get_parsers <- function(types = NULL, env = caller_env()) {
50+
#' @rdname register_parser
51+
#' @param parsers Parsers to collect. This can either be a character vector of
52+
#' names of registered parsers or a list. If it is a list then the following
53+
#' expectations apply:
54+
#' * Any unnamed elements containing a character vector will be considered as
55+
#' names of registered parsers constructed with default values. The special
56+
#' value `"..."` will fetch all the parsers that are otherwise not specified
57+
#' in the call
58+
#' * Any element containing a function are considered as a provided parser and
59+
#' the element must be named by the mime type the parser understands
60+
#' (wildcards allowed)
61+
#' * Any remaining named elements will be considered names of registered parsers
62+
#' that should be constructed with the arguments given in the element
63+
#'
64+
#' @export
65+
get_parsers <- function(parsers = NULL) {
66+
if (is.null(parsers)) {
67+
parsers <- names(registry$parsers)
68+
}
69+
elem_names <- names(parsers) %||% rep_along(parsers, "")
70+
named_parsers <- unlist(lapply(seq_along(parsers), function(i) {
71+
if (elem_names[i] == "") {
72+
if (is_character(parsers[[i]])) {
73+
parsers[[i]]
74+
} else {
75+
NULL
76+
}
77+
} else {
78+
elem_names[i]
79+
}
80+
}))
81+
if (sum(named_parsers == "...") > 1) {
82+
cli::cli_abort("{.val ...} can only be used once in {.arg parsers}")
83+
}
84+
named_parsers <- named_parsers[!grepl("/|^\\.\\.\\.$", named_parsers)]
85+
dots_parsers <- setdiff(names(registry$parsers), named_parsers)
86+
parsers <- lapply(seq_along(parsers), function(i) {
87+
if (is_function(parsers[[i]])) {
88+
if (length(fn_fmls(parsers[[i]])) != 2) {
89+
cli::cli_abort(
90+
"Provided parsers must be binary functions"
91+
)
92+
}
93+
if (!grepl("/", elem_names[i], fixed = TRUE)) {
94+
cli::cli_abort(
95+
"Parsers provided as functions must be named by their mime type"
96+
)
97+
}
98+
return(list2(!!elem_names[i] := parsers[[i]]))
99+
}
100+
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+
}
104+
return(get_parsers_internal(
105+
parsers[[i]],
106+
env = env,
107+
dots_parsers = dots_parsers
108+
))
109+
}
110+
if (elem_names[i] != "") {
111+
if (is.null(registry$parsers[[elem_names[i]]])) {
112+
cli::cli_abort(
113+
"No parser registered with {.val {elem_names[i]}} as name"
114+
)
115+
}
116+
if (!is.list(parsers[[i]])) parsers[[i]] <- list(parsers[[i]])
117+
funs <- rep_named(
118+
registry$parsers[[elem_names[i]]]$types,
119+
list(registry$parsers[[elem_names[i]]]$fun)
120+
)
121+
return(lapply(funs, function(f) inject(f(!!!parsers[[i]]))))
122+
}
123+
cli::cli_abort("Don't know how to parse element {i} in {.arg parsers}")
124+
})
125+
unlist(parsers, recursive = FALSE)
126+
}
127+
128+
get_parsers_internal <- function(
129+
types = NULL,
130+
env = caller_env(),
131+
dots_parsers = NULL
132+
) {
31133
if (isTRUE(tolower(types) == "none")) {
32134
return(NULL)
33135
}
@@ -38,19 +140,19 @@ get_parsers <- function(types = NULL, env = caller_env()) {
38140
if (length(dots) != 0) {
39141
types <- c(
40142
types[seq_len(dots - 1)],
41-
setdiff(names(registry$parsers), types),
143+
dots_parsers %||% setdiff(names(registry$parsers), types),
42144
types[dots + seq_len(length(types) - dots)]
43145
)
44146
}
45147
parsers <- lapply(types, function(type) {
46148
type <- stringi::stri_split_fixed(type, " ", n = 2)[[1]]
47149
if (stringi::stri_count_fixed(type[[1]], "/") == 1) {
48150
parser_fun <- if (length(type) == 2)
49-
eval_bare(parse_expr(type[2]), env = env) else identity
151+
eval_bare(parse_expr(type[2]), env = env) else function(x, ...) x
50152
check_function(parser_fun)
51153
parser <- list(
52154
fun = parser_fun,
53-
type = type[1]
155+
types = type[1]
54156
)
55157
} else {
56158
parser <- registry$parsers[[type[[1]]]]
@@ -82,51 +184,132 @@ get_parsers <- function(types = NULL, env = caller_env()) {
82184
}
83185

84186
# Default parsers --------------------------------------------------------------
187+
188+
#' Parser functions provided by plumber2
189+
#'
190+
#' These functions cover a large area of potential request body formats. They
191+
#' are all registered to their standard mime types but users may want to use
192+
#' them to register them to alternative types if they know it makes sense.
193+
#'
194+
#' # Provided parsers
195+
#' * `parse_csv()` uses [readr::read_csv()] for parsing. It is registered as
196+
#' `"csv"` for the mime types `application/csv`, `application/x-csv`,
197+
#' `text/csv`, and `text/x-csv`
198+
#' * `parse_octet()` passes the raw data through unchanged. It is registered as
199+
#' `"octet"` for the mime type `application/octet-stream`
200+
#' * `parse_rds()` uses [unserialize()] for parsing. It is registered as
201+
#' `"rds"` for the mime type `application/rds`
202+
#' * `parse_feather()` uses [arrow::read_feather()] for parsing. It is
203+
#' registered as `"feather"` for the mime types
204+
#' `application/vnd.apache.arrow.file` and `application/feather`
205+
#' * `parse_parquet()` uses [arrow::read_parquet()] for parsing. It is
206+
#' registered as `"parquet"` for the mime type `application/vnd.apache.parquet`
207+
#' * `parse_text()` uses [rawToChar()] for parsing. It is registered as
208+
#' `"text"` for the mime types `text/plain` and `text/*`
209+
#' * `parse_tsv()` uses [readr::read_tsv()] for parsing. It is registered as
210+
#' `"tsv"` for the mime types `application/tab-separated-values` and
211+
#' `text/tab-separated-values`
212+
#' * `parse_yaml()` uses [yaml::yaml.load()] for parsing. It is registered as
213+
#' `"yaml"` for the mime types `text/vnd.yaml`, `application/yaml`,
214+
#' `application/x-yaml`, `text/yaml`, and `text/x-yaml`
215+
#' * `parse_geojson()` uses [geojsonsf::geojson_sf()] for parsing. It is
216+
#' registered as `"geojson"` for the mime types `application/geo+json` and
217+
#' `application/vdn.geo+json`
218+
#'
219+
#' ## Additional registered parsers
220+
#' * [reqres::parse_json()] is registered as "`json`" for the mime types
221+
#' `application/json` and `text/json`
222+
#' * [reqres::parse_multiform()] is registered as "`multi`" for the mime
223+
#' type `multipart/*`
224+
#' * [reqres::parse_queryform()] is registered as "`form`" for the mime type
225+
#' `application/x-www-form-urlencoded`
226+
#'
227+
#' @param ... Further argument passed on to the internal parsing function. See
228+
#' Details for information on which function handles the parsing internally in
229+
#' each parser
230+
#'
231+
#' @return A function accepting a raw vector along with a `directives` argument
232+
#' that provides further directives from the `Content-Type` to be passed along
233+
#'
234+
#' @seealso [register_parser()]
235+
#' @rdname parsers
236+
#' @name parsers
237+
#'
238+
NULL
239+
240+
#' @rdname parsers
241+
#' @export
242+
#'
85243
parse_csv <- function(...) {
86244
check_installed("readr")
87245
function(raw, directives) {
88246
readr::read_csv(raw, ...)
89247
}
90248
}
249+
#' @rdname parsers
250+
#' @export
251+
#'
91252
parse_octet <- function() {
92253
function(raw, directives) {
93254
raw
94255
}
95256
}
257+
#' @rdname parsers
258+
#' @export
259+
#'
96260
parse_rds <- function(...) {
97261
function(raw, directives) {
98262
unserialize(raw, ...)
99263
}
100264
}
265+
#' @rdname parsers
266+
#' @export
267+
#'
101268
parse_feather <- function(...) {
102269
check_installed("arrow")
103270
function(raw, directives) {
104271
arrow::read_feather(raw, ...)
105272
}
106273
}
274+
#' @rdname parsers
275+
#' @export
276+
#'
107277
parse_parquet <- function(...) {
108278
check_installed("arrow")
109279
function(raw, directives) {
110280
arrow::read_parquet(raw, ...)
111281
}
112282
}
283+
#' @rdname parsers
284+
#' @inheritParams base::rawToChar
285+
#' @export
286+
#'
113287
parse_text <- function(multiple = FALSE) {
114288
function(raw, directives) {
115289
rawToChar(raw, multiple = multiple)
116290
}
117291
}
292+
#' @rdname parsers
293+
#' @export
294+
#'
118295
parse_tsv <- function(...) {
119296
check_installed("readr")
120297
function(raw, directives) {
121298
readr::read_tsv(raw, ...)
122299
}
123300
}
301+
#' @rdname parsers
302+
#' @export
303+
#'
124304
parse_yaml <- function(...) {
125305
check_installed("yaml")
126306
function(raw, directives) {
127307
yaml::yaml.load(rawToChar(raw), eval.expr = FALSE, ...)
128308
}
129309
}
310+
#' @rdname parsers
311+
#' @export
312+
#'
130313
parse_geojson <- function(...) {
131314
check_installed("geojsonsf")
132315
function(raw, directives) {

0 commit comments

Comments
 (0)