diff --git a/DESCRIPTION b/DESCRIPTION index 6a6d4cd5..880d1d75 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,7 +17,7 @@ Depends: R (>= 4.0) Imports: cli (>= 3.0.0), - curl (>= 6.0.1), + curl (>= 6.1.0), glue, lifecycle, magrittr, diff --git a/NEWS.md b/NEWS.md index 6cb61f4a..e9fe252c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # httr2 (development version) * `curl_translate()` now translates cookie headers to `req_cookies_set()` (#431). +* `curl_transform()` will now use `req_body_json_modify()` for JSON data (#258). * `resp_stream_is_complete()` tells you if there is still data remaining to be streamed (#559). * New `url_modify()`, `url_modify_query()`, and `url_modify_relative()` make it easier to modify an existing url (#464). * New `url_query_parse()` and `url_query_build()` allow you to parse and build a query string (#425). diff --git a/R/curl.R b/R/curl.R index a07a9949..a9789652 100644 --- a/R/curl.R +++ b/R/curl.R @@ -72,8 +72,14 @@ curl_translate <- function(cmd, simplify_headers = TRUE) { if (!identical(data$data, "")) { type <- type %||% "application/x-www-form-urlencoded" - body <- data$data - steps <- add_curl_step(steps, "req_body_raw", main_args = c(body, type)) + if (type == "application/json" && idempotent_json(data$data)) { + json <- jsonlite::parse_json(data$data) + args <- list(data = I(deparse1(json))) + steps <- add_curl_step(steps, "req_body_json", dots = args) + } else { + body <- data$data + steps <- add_curl_step(steps, "req_body_raw", main_args = c(body, type)) + } } steps <- add_curl_step(steps, "req_auth_basic", main_args = unname(data$auth)) @@ -155,6 +161,11 @@ curl_normalize <- function(cmd, error_call = caller_env()) { method <- NULL } + if (has_name(args, "--json")) { + args <- c(args, list(`--data-raw` = args[["--json"]])) + headers[["Content-Type"]] <- "application/json" + } + # https://curl.se/docs/manpage.html#-d # --data-ascii, --data # * if first element is @, treat as path to read from, stripping CRLF @@ -215,6 +226,7 @@ curl_opts <- "Usage: curl [] [-H
...] [-d ...] [options] [< --data-ascii HTTP POST ASCII data --data-binary HTTP POST binary data --data-urlencode HTTP POST data url encoded + --json HTTP POST JSON -G, --get Put the post data in the URL and use GET -I, --head Show document info only -H, --header
Pass custom header(s) to server @@ -271,11 +283,9 @@ quote_name <- function(x) { add_curl_step <- function(steps, f, - ..., main_args = NULL, dots = NULL, keep_if_empty = FALSE) { - check_dots_empty0(...) args <- c(main_args, dots) if (is_empty(args) && !keep_if_empty) { @@ -283,7 +293,7 @@ add_curl_step <- function(steps, } names <- quote_name(names2(args)) - string <- vapply(args, is.character, logical(1L)) + string <- map_lgl(args, function(x) is.character(x) && !inherits(x, "AsIs")) values <- unlist(args) values <- ifelse(string, encode_string2(values), values) @@ -338,3 +348,9 @@ cookies_parse <- function(x) { names(out) <- curl::curl_unescape(names(cookies)) out } + +idempotent_json <- function(old) { + args <- formals(req_body_json)[c("auto_unbox", "null", "digits")] + new <- exec(jsonlite::toJSON, jsonlite::parse_json(old), !!!args) + jsonlite::minify(old) == jsonlite::minify(new) +} diff --git a/tests/testthat/_snaps/curl.md b/tests/testthat/_snaps/curl.md index e812da44..fd8f649c 100644 --- a/tests/testthat/_snaps/curl.md +++ b/tests/testthat/_snaps/curl.md @@ -117,6 +117,26 @@ ) |> req_perform() +# can translate json + + Code + curl_translate( + "curl http://example.com --data-raw '{\"a\": 1, \"b\": \"text\"}' -H Content-Type:application/json") + Output + request("http://example.com/") |> + req_body_json( + data = list(a = 1L, b = "text"), + ) |> + req_perform() + Code + curl_translate("curl http://example.com --json '{\"a\": 1, \"b\": \"text\"}'") + Output + request("http://example.com/") |> + req_body_json( + data = list(a = 1L, b = "text"), + ) |> + req_perform() + # content type stays in header if no data Code @@ -158,6 +178,8 @@ Output request("http://example.com/") |> req_method("PATCH") |> - req_body_raw('{"data":{"x":1,"y":"a","nested":{"z":[1,2,3]}}}', "application/json") |> + req_body_json( + data = list(data = list(x = 1L, y = "a", nested = list(z = list(1L, 2L, 3L)))), + ) |> req_perform() diff --git a/tests/testthat/test-curl.R b/tests/testthat/test-curl.R index b504c648..a9cff6ad 100644 --- a/tests/testthat/test-curl.R +++ b/tests/testthat/test-curl.R @@ -150,6 +150,15 @@ test_that("can translate ocokies", { }) }) +test_that("can translate json", { + skip_if(getRversion() < "4.1") + + expect_snapshot({ + curl_translate(r"--{curl http://example.com --data-raw '{"a": 1, "b": "text"}' -H Content-Type:application/json}--") + curl_translate(r"--{curl http://example.com --json '{"a": 1, "b": "text"}'}--") + }) +}) + test_that("content type stays in header if no data", { skip_if(getRversion() < "4.1")