Skip to content

Commit

Permalink
Better JSON deparsing for curl_translate() (#617)
Browse files Browse the repository at this point in the history
Fixes #258
  • Loading branch information
hadley authored Jan 6, 2025
1 parent f680367 commit cd9786d
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 6 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# httr2 (development version)

* `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).
Expand Down
26 changes: 21 additions & 5 deletions R/curl.R
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,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))
Expand Down Expand Up @@ -148,6 +154,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
Expand Down Expand Up @@ -208,6 +219,7 @@ curl_opts <- "Usage: curl [<url>] [-H <header> ...] [-d <data> ...] [options] [<
--data-ascii <data> HTTP POST ASCII data
--data-binary <data> HTTP POST binary data
--data-urlencode <data> HTTP POST data url encoded
--json <data> HTTP POST JSON
-G, --get Put the post data in the URL and use GET
-I, --head Show document info only
-H, --header <header> Pass custom header(s) to server
Expand Down Expand Up @@ -264,19 +276,17 @@ 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) {
return(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)

Expand Down Expand Up @@ -318,3 +328,9 @@ encode_string2 <- function(x) {
names(out) <- names(x)
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)
}
24 changes: 23 additions & 1 deletion tests/testthat/_snaps/curl.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,26 @@
req_body_raw("abcdef", "text/plain") |>
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
Expand Down Expand Up @@ -145,6 +165,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()

9 changes: 9 additions & 0 deletions tests/testthat/test-curl.R
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ test_that("can translate data", {
})
})

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")

Expand Down

0 comments on commit cd9786d

Please sign in to comment.