From d7f4410bad7014b7c321ae342fd8216876c6eeac Mon Sep 17 00:00:00 2001 From: josiahparry Date: Wed, 22 Sep 2021 17:45:26 -0400 Subject: [PATCH 01/11] Create geojson serializer --- R/serializer.R | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/R/serializer.R b/R/serializer.R index 5ac4ac8a2..863339e64 100644 --- a/R/serializer.R +++ b/R/serializer.R @@ -231,6 +231,26 @@ serializer_unboxed_json <- function(auto_unbox = TRUE, ..., type = "application/ serializer_json(auto_unbox = auto_unbox, ..., type = type) } +#' @describeIn serializers GeoJSON serializer. See also [geojsonsf::sf_geojson()] and [[geojsonsf::sfc_geojson()]]. +#' @inheritParams geojsonsf::sf_geojson +#' @inheritParams geojsonsf::sfc_geojson +#' @export +serializer_geojson <- function(..., type = "application/geo+json") { + serializer_content_type(type, function(val) { + if (any(class(val) == "sfc")) { + + geojsonsf::sfc_geojson(val, ...) + + } else if (any(class(val) == "sf")) { + + geojsonsf::sf_geojson(val, ...) + + } else { + stop('Object must be of class "sf" or "sfc"', call. = FALSE) + } + + }) +} @@ -603,6 +623,7 @@ add_serializers_onLoad <- function() { register_serializer("tsv", serializer_tsv) register_serializer("feather", serializer_feather) register_serializer("yaml", serializer_yaml) + register_serializer("geojson", serializer_geojson) # text register_serializer("text", serializer_text) From 37b2a859b18ae6e0b8c592b355d472aa6e93c350 Mon Sep 17 00:00:00 2001 From: josiahparry Date: Wed, 22 Sep 2021 17:45:58 -0400 Subject: [PATCH 02/11] Create geojson serializer tests --- tests/testthat/test-serializer-geojson.R | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/testthat/test-serializer-geojson.R diff --git a/tests/testthat/test-serializer-geojson.R b/tests/testthat/test-serializer-geojson.R new file mode 100644 index 000000000..0a03d3a8c --- /dev/null +++ b/tests/testthat/test-serializer-geojson.R @@ -0,0 +1,37 @@ +context("geojson serializer") + +test_that("GeoJSON serializes properly", { + skip_if_not_installed("geojsonsf") + skip_if_not_installed("sf") + + # Objects taken from ?st_sf() examples. + sfc <- sf::st_sfc(sf::st_point(1:2), sf::st_point(3:4)) + sf <- sf::st_sf(a = 3:4, g) + + # Test sfc + val <- serializer_geojson()(sfc, data.frame(), PlumberResponse$new(), stop) + expect_equal(val$status, 200L) + expect_equal(val$headers$`Content-Type`, "application/geo+json") + expect_equal(val$body, geojsonsf::sfc_geojson(sfc)) + + # Test sf + val <- serializer_geojson()(sf, data.frame(), PlumberResponse$new(), stop) + expect_equal(val$status, 200L) + expect_equal(val$headers$`Content-Type`, "application/geo+json") + expect_equal(val$body, geojsonsf::sf_geojson(sf)) + +}) + +test_that("Errors call error handler", { + skip_if_not_installed("geojsonsf") + skip_if_not_installed("sf") + + errors <- 0 + errHandler <- function(req, res, err){ + errors <<- errors + 1 + } + + expect_equal(errors, 0) + serializer_geojson()(parse(text="hi"), data.frame(), PlumberResponse$new("csv"), errorHandler = errHandler) + expect_equal(errors, 1) +}) From fb453e943cc2818784fc3fb039ab470a88550dfa Mon Sep 17 00:00:00 2001 From: josiahparry Date: Wed, 22 Sep 2021 17:46:53 -0400 Subject: [PATCH 03/11] update serializers doc --- man/serializers.Rd | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/man/serializers.Rd b/man/serializers.Rd index 1e4321464..32499e21d 100644 --- a/man/serializers.Rd +++ b/man/serializers.Rd @@ -8,6 +8,7 @@ \alias{serializer_html} \alias{serializer_json} \alias{serializer_unboxed_json} +\alias{serializer_geojson} \alias{serializer_rds} \alias{serializer_feather} \alias{serializer_yaml} @@ -40,6 +41,8 @@ serializer_json(..., type = "application/json") serializer_unboxed_json(auto_unbox = TRUE, ..., type = "application/json") +serializer_geojson(..., type = "application/geo+json") + serializer_rds(version = "2", ascii = FALSE, ..., type = "application/rds") serializer_feather(type = "application/feather") @@ -128,9 +131,11 @@ more details on Plumber serializers and how to customize their behavior. \item \code{serializer_unboxed_json}: JSON serializer with \code{auto_unbox} defaulting to \code{TRUE}. See also: \code{\link[jsonlite:fromJSON]{jsonlite::toJSON()}} +\item \code{serializer_geojson}: GeoJSON serializer. See also \code{\link[geojsonsf:sf_geojson]{geojsonsf::sf_geojson()}} and [\code{\link[geojsonsf:sfc_geojson]{geojsonsf::sfc_geojson()}}]. + \item \code{serializer_rds}: RDS serializer. See also: \code{\link[base:serialize]{base::serialize()}} -\item \code{serializer_feather}: feather serializer. See also: \code{\link[feather:read_feather]{feather::write_feather()}} +\item \code{serializer_feather}: feather serializer. See also: \code{\link[feather:write_feather]{feather::write_feather()}} \item \code{serializer_yaml}: YAML serializer. See also: \code{\link[yaml:as.yaml]{yaml::as.yaml()}} From bc58baf453e3f4afcc21c4e9d784ea9aabd0214c Mon Sep 17 00:00:00 2001 From: josiahparry Date: Wed, 22 Sep 2021 17:47:05 -0400 Subject: [PATCH 04/11] add Josiah as contributor --- DESCRIPTION | 1 + 1 file changed, 1 insertion(+) diff --git a/DESCRIPTION b/DESCRIPTION index 817d10588..a13a72b03 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -10,6 +10,7 @@ Authors@R: c( person("Bruno", "Tremblay", role = "ctb", email = "cran@neoxone.com"), person("Frans", "van Dunné", role = "ctb", email = "frans@ixpantia.com"), person("Sebastiaan", "Vandewoude", role="ctb", email = "sebastiaanvandewoude@gmail.com"), + person("Josiah", "Parry", role = "ctb", email = "josiah.parry@gmail.com", comment = c(ORCID = "0000-0001-9910-865X")), person(family = "RStudio", role = c("cph", "fnd"))) License: MIT + file LICENSE BugReports: https://github.com/rstudio/plumber/issues From 9540c7af4d7e072713dddf34b084a2036919cf59 Mon Sep 17 00:00:00 2001 From: josiahparry Date: Wed, 22 Sep 2021 17:47:52 -0400 Subject: [PATCH 05/11] add serializer_geojson to NAMESPACE --- NAMESPACE | 1 + 1 file changed, 1 insertion(+) diff --git a/NAMESPACE b/NAMESPACE index fa6fd8477..862e71237 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -76,6 +76,7 @@ export(serializer_csv) export(serializer_device) export(serializer_feather) export(serializer_format) +export(serializer_geojson) export(serializer_headers) export(serializer_html) export(serializer_htmlwidget) From 3ac0b819f97ef346761450e42a4b66f25f505771 Mon Sep 17 00:00:00 2001 From: josiahparry Date: Wed, 22 Sep 2021 19:05:57 -0400 Subject: [PATCH 06/11] add geojsonsf to suggests --- DESCRIPTION | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index a13a72b03..3048ae3af 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -50,7 +50,8 @@ Suggests: future, rstudioapi, spelling, - mockery (>= 0.4.2) + mockery (>= 0.4.2), + geojsonsf RoxygenNote: 7.1.1 Collate: 'async.R' From 0a3a38c76203cd9e86d7f6d86d82350a89600225 Mon Sep 17 00:00:00 2001 From: josiahparry Date: Thu, 23 Sep 2021 16:39:57 -0400 Subject: [PATCH 07/11] add geojson parser along tests --- R/parse-body.R | 11 ++++++++ tests/testthat/test-parse-body.R | 34 ++++++++++++++++++++++++ tests/testthat/test-serializer-geojson.R | 2 +- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/R/parse-body.R b/R/parse-body.R index f9281da30..5cc3d75b9 100644 --- a/R/parse-body.R +++ b/R/parse-body.R @@ -386,6 +386,16 @@ parser_json <- function(...) { }) } +#' @describeIn parsers GeoJSON parser. See [geojsonsf::geojson_sf()] for more details. +#' @export +parser_geojson <- function(...) { + parser_text(function(val) { + if (!requireNamespace("geojsonsf", quietly = TRUE)) { + stop("`geojsonsf` must be installed for `parser_geojson` to work") + } + geojsonsf::geojson_sf(val, ...) + }) +} #' @describeIn parsers Helper parser to parse plain text #' @param parse_fn function to further decode a text string into an object @@ -564,6 +574,7 @@ register_parsers_onLoad <- function() { # yaml types: https://stackoverflow.com/a/38000954/591574 register_parser("yaml", parser_yaml, fixed = c("text/vnd.yaml", "application/yaml", "application/x-yaml", "text/yaml", "text/x-yaml")) register_parser("none", parser_none, regex = "*") + register_parser("geojson", parser_geojson, fixed = c("application/geo+json", "geo+json")) parser_all <- function() { stop("This function should never be called. It should be handled by `make_parser('all')`") diff --git a/tests/testthat/test-parse-body.R b/tests/testthat/test-parse-body.R index f2054e8e8..5c93ee5b8 100644 --- a/tests/testthat/test-parse-body.R +++ b/tests/testthat/test-parse-body.R @@ -109,6 +109,40 @@ test_that("Test feather parser", { expect_equal(parsed, r_object) }) +test_that("Test geojson parser", { + skip_if_not_installed("geojsonsf") + skip_if_not_installed("sf") + + # Test sf object w/ fields + geojson <- '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"a":3},"geometry":{"type":"Point","coordinates":[1,2]}},{"type":"Feature","properties":{"a":4},"geometry":{"type":"Point","coordinates":[3,4]}}]}' + parsed <- parse_body(geojson, "application/geo+json", make_parser("geojson")) + expect_equal(parsed, geojson_sf(geojson)) + + # Test sfc + geojson <- '[ + { "type":"Point","coordinates":[0,0]}, + {"type":"LineString","coordinates":[[0,0],[1,1]]} + ]' + parsed <- parse_body(geojson, "application/geo+json", make_parser("geojson")) + expect_equal(parsed, geojson_sf(geojson)) + + # Test simple sf object + geojson <- '{ "type" : "Point", "coordinates" : [0, 0] }' + parsed <- parse_body(geojson, "application/geo+json", make_parser("geojson")) + expect_equal(parsed, geojson_sf(geojson)) + + # Test geojson file + tmp <- tempfile() + on.exit({ + file.remove(tmp) + }, add = TRUE) + + writeLines(geojson, tmp) + val <- readBin(tmp, "raw", 1000) + parsed <- parse_body(val, "application/geo+json", make_parser("geojson")) + expect_equal(parsed, geojson_sf(geojson)) + +}) test_that("Test multipart output is reduced for argument matching", { bin_file <- test_path("files/multipart-file-names.bin") diff --git a/tests/testthat/test-serializer-geojson.R b/tests/testthat/test-serializer-geojson.R index 0a03d3a8c..789b5ae3c 100644 --- a/tests/testthat/test-serializer-geojson.R +++ b/tests/testthat/test-serializer-geojson.R @@ -6,7 +6,7 @@ test_that("GeoJSON serializes properly", { # Objects taken from ?st_sf() examples. sfc <- sf::st_sfc(sf::st_point(1:2), sf::st_point(3:4)) - sf <- sf::st_sf(a = 3:4, g) + sf <- sf::st_sf(a = 3:4, sfc) # Test sfc val <- serializer_geojson()(sfc, data.frame(), PlumberResponse$new(), stop) From 86ccb4039f8ad1d2e6426517806553e6bd8d8bcf Mon Sep 17 00:00:00 2001 From: josiahparry Date: Thu, 23 Sep 2021 16:55:16 -0400 Subject: [PATCH 08/11] make requested adjustments to serializer and serializer test --- R/serializer.R | 17 ++++------------- tests/testthat/test-serializer-geojson.R | 2 -- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/R/serializer.R b/R/serializer.R index 863339e64..9dfb42d7a 100644 --- a/R/serializer.R +++ b/R/serializer.R @@ -232,23 +232,14 @@ serializer_unboxed_json <- function(auto_unbox = TRUE, ..., type = "application/ } #' @describeIn serializers GeoJSON serializer. See also [geojsonsf::sf_geojson()] and [[geojsonsf::sfc_geojson()]]. -#' @inheritParams geojsonsf::sf_geojson -#' @inheritParams geojsonsf::sfc_geojson #' @export serializer_geojson <- function(..., type = "application/geo+json") { serializer_content_type(type, function(val) { - if (any(class(val) == "sfc")) { - - geojsonsf::sfc_geojson(val, ...) - - } else if (any(class(val) == "sf")) { - - geojsonsf::sf_geojson(val, ...) - - } else { - stop('Object must be of class "sf" or "sfc"', call. = FALSE) + if (inherits(val, "sfc")) return(geojsonsf::sfc_geojson(val, ...)) + if (inherits(val, "sf")) return(geojsonsf::sf_geojson(val, ...)) + if (!requireNamespace("geojsonsf", quietly = TRUE)) { + stop("`geojsonsf` must be installed for `serializer_geojson` to work") } - }) } diff --git a/tests/testthat/test-serializer-geojson.R b/tests/testthat/test-serializer-geojson.R index 789b5ae3c..1f7789011 100644 --- a/tests/testthat/test-serializer-geojson.R +++ b/tests/testthat/test-serializer-geojson.R @@ -1,5 +1,3 @@ -context("geojson serializer") - test_that("GeoJSON serializes properly", { skip_if_not_installed("geojsonsf") skip_if_not_installed("sf") From 5e93cd96e2ae2540b9b5ff9547ddda820ed5a814 Mon Sep 17 00:00:00 2001 From: josiahparry Date: Thu, 23 Sep 2021 16:55:46 -0400 Subject: [PATCH 09/11] add sf under suggest for tests --- DESCRIPTION | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 3048ae3af..b7223dcc3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -10,7 +10,6 @@ Authors@R: c( person("Bruno", "Tremblay", role = "ctb", email = "cran@neoxone.com"), person("Frans", "van Dunné", role = "ctb", email = "frans@ixpantia.com"), person("Sebastiaan", "Vandewoude", role="ctb", email = "sebastiaanvandewoude@gmail.com"), - person("Josiah", "Parry", role = "ctb", email = "josiah.parry@gmail.com", comment = c(ORCID = "0000-0001-9910-865X")), person(family = "RStudio", role = c("cph", "fnd"))) License: MIT + file LICENSE BugReports: https://github.com/rstudio/plumber/issues @@ -51,7 +50,8 @@ Suggests: rstudioapi, spelling, mockery (>= 0.4.2), - geojsonsf + geojsonsf, + sf RoxygenNote: 7.1.1 Collate: 'async.R' From d880abf0f1dfd217e7ddba139a35e4ef2fd36f81 Mon Sep 17 00:00:00 2001 From: josiahparry Date: Thu, 23 Sep 2021 17:09:05 -0400 Subject: [PATCH 10/11] include namespace on geojson_sf call in tests Pass a non valid geojson value to the serializer. --- tests/testthat/test-parse-body.R | 8 ++++---- tests/testthat/test-serializer-geojson.R | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/testthat/test-parse-body.R b/tests/testthat/test-parse-body.R index 5c93ee5b8..d1f89434f 100644 --- a/tests/testthat/test-parse-body.R +++ b/tests/testthat/test-parse-body.R @@ -116,7 +116,7 @@ test_that("Test geojson parser", { # Test sf object w/ fields geojson <- '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"a":3},"geometry":{"type":"Point","coordinates":[1,2]}},{"type":"Feature","properties":{"a":4},"geometry":{"type":"Point","coordinates":[3,4]}}]}' parsed <- parse_body(geojson, "application/geo+json", make_parser("geojson")) - expect_equal(parsed, geojson_sf(geojson)) + expect_equal(parsed, geojsonsf::geojson_sf(geojson)) # Test sfc geojson <- '[ @@ -124,12 +124,12 @@ test_that("Test geojson parser", { {"type":"LineString","coordinates":[[0,0],[1,1]]} ]' parsed <- parse_body(geojson, "application/geo+json", make_parser("geojson")) - expect_equal(parsed, geojson_sf(geojson)) + expect_equal(parsed, geojsonsf::geojson_sf(geojson)) # Test simple sf object geojson <- '{ "type" : "Point", "coordinates" : [0, 0] }' parsed <- parse_body(geojson, "application/geo+json", make_parser("geojson")) - expect_equal(parsed, geojson_sf(geojson)) + expect_equal(parsed, geojsonsf::geojson_sf(geojson)) # Test geojson file tmp <- tempfile() @@ -140,7 +140,7 @@ test_that("Test geojson parser", { writeLines(geojson, tmp) val <- readBin(tmp, "raw", 1000) parsed <- parse_body(val, "application/geo+json", make_parser("geojson")) - expect_equal(parsed, geojson_sf(geojson)) + expect_equal(parsed, geojsonsf::geojson_sf(geojson)) }) diff --git a/tests/testthat/test-serializer-geojson.R b/tests/testthat/test-serializer-geojson.R index 1f7789011..6ac6eaad6 100644 --- a/tests/testthat/test-serializer-geojson.R +++ b/tests/testthat/test-serializer-geojson.R @@ -30,6 +30,6 @@ test_that("Errors call error handler", { } expect_equal(errors, 0) - serializer_geojson()(parse(text="hi"), data.frame(), PlumberResponse$new("csv"), errorHandler = errHandler) + serializer_geojson()(parse(text="h$534i} {!"), data.frame(), PlumberResponse$new(), errorHandler = errHandler) expect_equal(errors, 1) }) From 202537cf62b44381a9d1a6d083b3a1cc64b74c50 Mon Sep 17 00:00:00 2001 From: josiahparry Date: Fri, 24 Sep 2021 11:39:13 -0400 Subject: [PATCH 11/11] Address requested changes in #830. - Update namespace w/ devtools::document() - Update news under "New features" - Move check for `geojsonsf` package to top of function - Add `application/vdn.geo+json` as valid content type --- NAMESPACE | 1 + NEWS.md | 3 +++ R/parse-body.R | 8 ++++---- R/serializer.R | 7 ++++--- man/parsers.Rd | 5 +++++ 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 862e71237..d12ec0541 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -28,6 +28,7 @@ export(options_plumber) export(parser_csv) export(parser_feather) export(parser_form) +export(parser_geojson) export(parser_json) export(parser_multi) export(parser_none) diff --git a/NEWS.md b/NEWS.md index 316b2cd86..27ddfb768 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,9 @@ ## Breaking changes ## New features + +* Introduces new Geojson serializer and parser. Geojson objects are parsed into `sf` objects and `sf` or `sfc` objects will be serialized into geojson. (@josiahparry, #830) + ## Bug fixes * OpenAPI response type detection had a scoping issue. Use serializer defined `Content-Type` header instead. (@meztez, #789) diff --git a/R/parse-body.R b/R/parse-body.R index 5cc3d75b9..d734badc6 100644 --- a/R/parse-body.R +++ b/R/parse-body.R @@ -389,10 +389,10 @@ parser_json <- function(...) { #' @describeIn parsers GeoJSON parser. See [geojsonsf::geojson_sf()] for more details. #' @export parser_geojson <- function(...) { + if (!requireNamespace("geojsonsf", quietly = TRUE)) { + stop("`geojsonsf` must be installed for `parser_geojson` to work") + } parser_text(function(val) { - if (!requireNamespace("geojsonsf", quietly = TRUE)) { - stop("`geojsonsf` must be installed for `parser_geojson` to work") - } geojsonsf::geojson_sf(val, ...) }) } @@ -574,7 +574,7 @@ register_parsers_onLoad <- function() { # yaml types: https://stackoverflow.com/a/38000954/591574 register_parser("yaml", parser_yaml, fixed = c("text/vnd.yaml", "application/yaml", "application/x-yaml", "text/yaml", "text/x-yaml")) register_parser("none", parser_none, regex = "*") - register_parser("geojson", parser_geojson, fixed = c("application/geo+json", "geo+json")) + register_parser("geojson", parser_geojson, fixed = c("application/geo+json", "application/vdn.geo+json")) parser_all <- function() { stop("This function should never be called. It should be handled by `make_parser('all')`") diff --git a/R/serializer.R b/R/serializer.R index 9dfb42d7a..4a6b81b8d 100644 --- a/R/serializer.R +++ b/R/serializer.R @@ -234,12 +234,13 @@ serializer_unboxed_json <- function(auto_unbox = TRUE, ..., type = "application/ #' @describeIn serializers GeoJSON serializer. See also [geojsonsf::sf_geojson()] and [[geojsonsf::sfc_geojson()]]. #' @export serializer_geojson <- function(..., type = "application/geo+json") { + if (!requireNamespace("geojsonsf", quietly = TRUE)) { + stop("`geojsonsf` must be installed for `serializer_geojson` to work") + } serializer_content_type(type, function(val) { if (inherits(val, "sfc")) return(geojsonsf::sfc_geojson(val, ...)) if (inherits(val, "sf")) return(geojsonsf::sf_geojson(val, ...)) - if (!requireNamespace("geojsonsf", quietly = TRUE)) { - stop("`geojsonsf` must be installed for `serializer_geojson` to work") - } + stop("Did not receive an `sf` or `sfc` object. ") }) } diff --git a/man/parsers.Rd b/man/parsers.Rd index a6b9850e7..d4be32d6f 100644 --- a/man/parsers.Rd +++ b/man/parsers.Rd @@ -3,6 +3,7 @@ \name{parser_form} \alias{parser_form} \alias{parser_json} +\alias{parser_geojson} \alias{parser_text} \alias{parser_yaml} \alias{parser_csv} @@ -19,6 +20,8 @@ parser_form() parser_json(...) +parser_geojson(...) + parser_text(parse_fn = identity) parser_yaml(...) @@ -68,6 +71,8 @@ See \code{\link[=registered_parsers]{registered_parsers()}} for a list of regist \item \code{parser_json}: JSON parser. See \code{\link[jsonlite:read_json]{jsonlite::parse_json()}} for more details. (Defaults to using \code{simplifyVectors = TRUE}) +\item \code{parser_geojson}: GeoJSON parser. See \code{\link[geojsonsf:geojson_sf]{geojsonsf::geojson_sf()}} for more details. + \item \code{parser_text}: Helper parser to parse plain text \item \code{parser_yaml}: YAML parser. See \code{\link[yaml:yaml.load]{yaml::yaml.load()}} for more details.