diff --git a/DESCRIPTION b/DESCRIPTION index c87d132a5..18b7f883b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -41,7 +41,8 @@ Suggests: visNetwork, analogsea (>= 0.7.0), later, - readr + readr, + yaml Remotes: rstudio/swagger Collate: @@ -74,6 +75,7 @@ Collate: 'serializer-htmlwidget.R' 'serializer-rds.R' 'serializer-xml.R' + 'serializer-yaml.R' 'serializer.R' 'session-cookie.R' 'swagger.R' diff --git a/NAMESPACE b/NAMESPACE index a3c24d47f..a47db3bb0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -22,6 +22,7 @@ export(parser_octet) export(parser_query) export(parser_rds) export(parser_text) +export(parser_yaml) export(plumb) export(plumber) export(randomCookieKey) @@ -32,6 +33,7 @@ export(serializer_htmlwidget) export(serializer_json) export(serializer_rds) export(serializer_unboxed_json) +export(serializer_yaml) export(sessionCookie) import(R6) import(crayon) diff --git a/NEWS.md b/NEWS.md index f96dce42f..75cdec3df 100644 --- a/NEWS.md +++ b/NEWS.md @@ -48,6 +48,8 @@ plumber 0.5.0 ### New features +* Add yaml support, serializer and parser. (@meztez, #556) + * Added Swagger support for array parameters using syntax `name:[type]` and new type `list` (synonym df, data.frame). (@meztez, #532) * Added support for promises in endpoints, filters, and hooks. (#248) diff --git a/R/post-parsers.R b/R/post-parsers.R index 526e9b491..03b4f3a87 100644 --- a/R/post-parsers.R +++ b/R/post-parsers.R @@ -29,7 +29,7 @@ NULL #' to build parser are `value`, `content_type` and `filename` (only available #' in `multipart-form` body). #' ```r -#' parser <- function(...) { +#' parser <- function() { #' function(value, content_type = "ct", filename, ...) { #' # do something with raw value #' } @@ -40,7 +40,7 @@ NULL #' plumber endpoint function args. #' #' @examples -#' parser_json <- function(...) { +#' parser_json <- function() { #' function(value, content_type = "application/json", ...) { #' charset <- getCharacterSet(content_type) #' value <- rawToChar(value) @@ -68,9 +68,8 @@ addParser <- function(name, parser, pattern = NULL) { #' JSON #' @rdname parsers -#' @param ... Raw values and headers are passed there. #' @export -parser_json <- function(...) { +parser_json <- function() { function(value, content_type = NULL, ...) { charset <- getCharacterSet(content_type) value <- rawToChar(value) @@ -85,7 +84,7 @@ parser_json <- function(...) { #' QUERY STRING #' @rdname parsers #' @export -parser_query <- function(...) { +parser_query <- function() { function(value, content_type = NULL, ...) { charset <- getCharacterSet(content_type) value <- rawToChar(value) @@ -100,7 +99,7 @@ parser_query <- function(...) { #' TEXT #' @rdname parsers #' @export -parser_text <- function(...) { +parser_text <- function() { function(value, content_type = NULL, ...) { charset <- getCharacterSet(content_type) value <- rawToChar(value) @@ -115,7 +114,7 @@ parser_text <- function(...) { #" RDS #' @rdname parsers #' @export -parser_rds <- function(...) { +parser_rds <- function() { function(value, filename, ...) { tmp <- tempfile("plumb", fileext = paste0("_", basename(filename))) on.exit(file.remove(tmp), add = TRUE) @@ -131,7 +130,7 @@ parser_rds <- function(...) { #' @rdname parsers #' @export #' @importFrom webutils parse_multipart -parser_multi <- function(...) { +parser_multi <- function() { function(value, content_type, ...) { if (!stri_detect_fixed(content_type, "boundary=", case_insensitive = TRUE)) stop("No boundary found in multipart content-type header: ", content_type) @@ -152,9 +151,8 @@ parser_multi <- function(...) { #' OCTET #' @rdname parsers -#' @param ... Raw values and headers are passed there. #' @export -parser_octet <- function(...) { +parser_octet <- function() { function(value, filename = NULL, ...) { attr(value, "filename") <- filename return(value) @@ -164,6 +162,24 @@ parser_octet <- function(...) { +#' YAML +#' @rdname parsers +#' @export +parser_yaml <- function() { + if (!requireNamespace("yaml", quietly = TRUE)) { + stop("yaml must be installed for the yaml parser to work") + } + function(value, content_type = NULL, ...) { + charset <- getCharacterSet(content_type) + value <- rawToChar(value) + Encoding(value) <- charset + yaml::yaml.load(value) + } +} + + + + addParsers_onLoad <- function() { addParser("json", parser_json, "application/json") addParser("query", parser_query, "application/x-www-form-urlencoded") @@ -171,4 +187,5 @@ addParsers_onLoad <- function() { addParser("rds", parser_rds, "application/rds") addParser("multi", parser_multi, "multipart/form-data") addParser("octet", parser_octet, "application/octet") + addParser("yaml", parser_yaml, "application/x-yaml") } diff --git a/R/serializer-yaml.R b/R/serializer-yaml.R new file mode 100644 index 000000000..a7e5c9ec8 --- /dev/null +++ b/R/serializer-yaml.R @@ -0,0 +1,21 @@ +#' @rdname serializers +#' @export +serializer_yaml <- function(...) { + if (!requireNamespace("yaml", quietly = TRUE)) { + stop("yaml must be installed for the yaml serializer to work") + } + function(val, req, res, errorHandler) { + tryCatch({ + yaml <- yaml::as.yaml(val, ...) + res$setHeader("Content-Type", "application/x-yaml") + res$body <- yaml + + return(res$toResponse()) + }, error = function(e){ + errorHandler(req, res, e) + }) + } +} + +#' @include globals.R +.globals$serializers[["yaml"]] <- serializer_yaml diff --git a/man/addParser.Rd b/man/addParser.Rd index de3941aa9..b24966c18 100644 --- a/man/addParser.Rd +++ b/man/addParser.Rd @@ -28,7 +28,7 @@ case sensitive. Parser function structure is something like below. Available parameters to build parser are \code{value}, \code{content_type} and \code{filename} (only available -in \code{multipart-form} body).\if{html}{\out{
}}\preformatted{parser <- function(...) \{ +in \code{multipart-form} body).\if{html}{\out{
}}\preformatted{parser <- function() \{ function(value, content_type = "ct", filename, ...) \{ # do something with raw value \} @@ -39,7 +39,7 @@ It should return a named list if you want values to map to plumber endpoint function args. } \examples{ -parser_json <- function(...) { +parser_json <- function() { function(value, content_type = "application/json", ...) { charset <- getCharacterSet(content_type) value <- rawToChar(value) diff --git a/man/parsers.Rd b/man/parsers.Rd index 903b4e349..f00adbb94 100644 --- a/man/parsers.Rd +++ b/man/parsers.Rd @@ -8,22 +8,22 @@ \alias{parser_rds} \alias{parser_multi} \alias{parser_octet} +\alias{parser_yaml} \title{Plumber Parsers} \usage{ -parser_json(...) +parser_json() -parser_query(...) +parser_query() -parser_text(...) +parser_text() -parser_rds(...) +parser_rds() -parser_multi(...) +parser_multi() -parser_octet(...) -} -\arguments{ -\item{...}{Raw values and headers are passed there.} +parser_octet() + +parser_yaml() } \description{ Parsers are used in Plumber to transform the raw body content received diff --git a/man/serializers.Rd b/man/serializers.Rd index bdb3f4e20..f37da14a2 100644 --- a/man/serializers.Rd +++ b/man/serializers.Rd @@ -1,7 +1,7 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/serializer-json.R, R/serializer-content-type.R, % R/serializer-csv.R, R/serializer-html.R, R/serializer-htmlwidget.R, -% R/serializer-rds.R, R/serializer.R +% R/serializer-rds.R, R/serializer-yaml.R, R/serializer.R \name{serializer_json} \alias{serializer_json} \alias{serializer_unboxed_json} @@ -10,6 +10,7 @@ \alias{serializer_html} \alias{serializer_htmlwidget} \alias{serializer_rds} +\alias{serializer_yaml} \alias{serializers} \title{Plumber Serializers} \usage{ @@ -26,6 +27,8 @@ serializer_html() serializer_htmlwidget(...) serializer_rds(version = "2", ascii = FALSE, ...) + +serializer_yaml(...) } \arguments{ \item{...}{extra arguments supplied to respective internal serialization function.} diff --git a/tests/testthat/test-postbody.R b/tests/testthat/test-postbody.R index 5d7ae8a7b..1cce315b9 100644 --- a/tests/testthat/test-postbody.R +++ b/tests/testthat/test-postbody.R @@ -49,6 +49,13 @@ test_that("Test text parser", { expect_equal(parseBody("Ceci est un texte.", "text/html"), "Ceci est un texte.") }) +test_that("Test yaml parser", { + skip_if_not_installed("yaml") + + r_object <- list(a=1,b=list(c=2,d=list(e=3,f=4:6))) + expect_equal(parseBody(charToRaw(yaml::as.yaml(r_object)), "application/x-yaml"), r_object) +}) + test_that("Test multipart parser", { bin_file <- test_path("files/multipart-form.bin") diff --git a/tests/testthat/test-serializer-yaml.R b/tests/testthat/test-serializer-yaml.R new file mode 100644 index 000000000..385bffca7 --- /dev/null +++ b/tests/testthat/test-serializer-yaml.R @@ -0,0 +1,36 @@ +context("YAML serializer") + +test_that("YAML serializes properly", { + skip_if_not_installed("yaml") + + l <- list(a=1, b=2, c="hi") + val <- serializer_yaml()(l, list(), PlumberResponse$new(), stop) + expect_equal(val$status, 200L) + expect_equal(val$headers$`Content-Type`, "application/x-yaml") + expect_equal(val$body, yaml::as.yaml(l)) + + l <- list(a=1, b=2, c="hi", na=NA) + val <- serializer_yaml()(l, list(), PlumberResponse$new(), stop) + expect_equal(val$status, 200L) + expect_equal(val$headers$`Content-Type`, "application/x-yaml") + expect_equal(val$body, yaml::as.yaml(l)) + + l <- list(a=1, b=2, c="hi", na=NA) + val <- serializer_yaml(indent = 4)(l, list(), PlumberResponse$new(), stop) + expect_equal(val$status, 200L) + expect_equal(val$headers$`Content-Type`, "application/x-yaml") + expect_equal(val$body, yaml::as.yaml(l, indent = 4)) +}) + +test_that("Errors call error handler", { + skip_if_not_installed("yaml") + + errors <- 0 + errHandler <- function(req, res, err){ + errors <<- errors + 1 + } + + expect_equal(errors, 0) + serializer_yaml()(parse(text="hi"), list(), PlumberResponse$new("yaml"), err = errHandler) + expect_equal(errors, 1) +})