Skip to content
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

Create GeoJSON serializer #830

Merged
merged 11 commits into from
Sep 28, 2021
4 changes: 3 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ Suggests:
future,
rstudioapi,
spelling,
mockery (>= 0.4.2)
mockery (>= 0.4.2),
geojsonsf,
sf
RoxygenNote: 7.1.1
Collate:
'async.R'
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -76,6 +77,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)
Expand Down
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions R/parse-body.R
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,16 @@ parser_json <- function(...) {
})
}

#' @describeIn parsers GeoJSON parser. See [geojsonsf::geojson_sf()] for more details.
#' @export
schloerke marked this conversation as resolved.
Show resolved Hide resolved
parser_geojson <- function(...) {
if (!requireNamespace("geojsonsf", quietly = TRUE)) {
stop("`geojsonsf` must be installed for `parser_geojson` to work")
}
parser_text(function(val) {
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
Expand Down Expand Up @@ -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", "application/vdn.geo+json"))

parser_all <- function() {
stop("This function should never be called. It should be handled by `make_parser('all')`")
Expand Down
13 changes: 13 additions & 0 deletions R/serializer.R
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,18 @@ 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()]].
#' @export
serializer_geojson <- function(..., type = "application/geo+json") {
schloerke marked this conversation as resolved.
Show resolved Hide resolved
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, ...))
stop("Did not receive an `sf` or `sfc` object. ")
})
}



Expand Down Expand Up @@ -603,6 +615,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)
Expand Down
5 changes: 5 additions & 0 deletions man/parsers.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion man/serializers.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions tests/testthat/test-parse-body.R
Original file line number Diff line number Diff line change
Expand Up @@ -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, geojsonsf::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, 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, geojsonsf::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, geojsonsf::geojson_sf(geojson))

})

test_that("Test multipart output is reduced for argument matching", {
bin_file <- test_path("files/multipart-file-names.bin")
Expand Down
35 changes: 35 additions & 0 deletions tests/testthat/test-serializer-geojson.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
test_that("GeoJSON serializes properly", {
skip_if_not_installed("geojsonsf")
skip_if_not_installed("sf")
schloerke marked this conversation as resolved.
Show resolved Hide resolved

# 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, sfc)

# 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="h$534i} {!"), data.frame(), PlumberResponse$new(), errorHandler = errHandler)
expect_equal(errors, 1)
})