From f22105b9be4289fb213179065a867980f84aac91 Mon Sep 17 00:00:00 2001 From: Josiah Parry Date: Wed, 20 Aug 2025 08:44:05 -0700 Subject: [PATCH 1/6] add httr2_translate function --- NAMESPACE | 1 + R/httr2-translate.R | 190 +++++++++++++++++++++++ man/httr2_translate.Rd | 35 +++++ tests/testthat/_snaps/httr2-translate.md | 135 ++++++++++++++++ tests/testthat/test-httr2-translate.R | 163 +++++++++++++++++++ 5 files changed, 524 insertions(+) create mode 100644 R/httr2-translate.R create mode 100644 man/httr2_translate.Rd create mode 100644 tests/testthat/_snaps/httr2-translate.md create mode 100644 tests/testthat/test-httr2-translate.R diff --git a/NAMESPACE b/NAMESPACE index 7cf5bb43..205ec32f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -24,6 +24,7 @@ export(curl_help) export(curl_translate) export(example_github_client) export(example_url) +export(httr2_translate) export(is_online) export(iterate_with_cursor) export(iterate_with_link_url) diff --git a/R/httr2-translate.R b/R/httr2-translate.R new file mode 100644 index 00000000..bec13bc4 --- /dev/null +++ b/R/httr2-translate.R @@ -0,0 +1,190 @@ +#' Translate httr2 request to curl command +#' +#' Convert an httr2 request object to equivalent curl command line syntax. +#' This is useful for debugging, sharing requests, or converting to other tools. +#' +#' @param .req An httr2 request object created with [request()]. +#' @return A character string containing the curl command. +#' @export +#' @examples +#' \dontrun{ +#' # Basic GET request +#' request("https://httpbin.org/get") |> +#' httr2_translate() +#' +#' # POST with JSON body +#' request("https://httpbin.org/post") |> +#' req_body_json(list(name = "value")) |> +#' httr2_translate() +#' +#' # POST with form data +#' request("https://httpbin.org/post") |> +#' req_body_form(name = "value") |> +#' httr2_translate() +#' } +httr2_translate <- function(.req) { + # validate the request + check_request(.req) + + # Extract URL + url <- .req$url + + # use the request's method if it is set, otherwise infer + method <- .req$method %||% + { + if (!is.null(.req$body$data)) { + "POST" + } else { + "GET" + } + } + + # we will append to cmd_parts to build up the request + cmd_parts <- c("curl") + + # if the method isn't GET, it needs to be specified with `-X` + if (method != "GET") { + cmd_parts <- c(cmd_parts, paste0("-X ", method)) + } + + # if headers are present, add them using -H flag + if (!is.null(.req$headers) && length(.req$headers) > 0) { + headers <- .req$headers + for (name in names(headers)) { + value <- headers[[name]] + + # Handle weak references first + if (rlang::is_weakref(value)) { + value <- rlang::wref_value(value) + } + + # Handle obfuscated values by revealing them + if (is_obfuscated(value)) { + value <- unobfuscate(value, handle = "reveal") + } + + cmd_parts <- c(cmd_parts, paste0('-H "', name, ': ', value, '"')) + } + } + + # Handle options (curl options like timeout, proxy, etc.) + if (!is.null(.req$options) && length(.req$options) > 0) { + options <- .req$options + for (name in names(options)) { + value <- options[[name]] + # Convert common curl options to curl command flags + curl_flag <- switch( + name, + "timeout" = paste0("--max-time ", value), + "connecttimeout" = paste0("--connect-timeout ", value), + "proxy" = paste0("--proxy ", value), + "useragent" = paste0('--user-agent "', value, '"'), + "referer" = paste0('--referer "', value, '"'), + "followlocation" = if (value) "--location" else NULL, + "ssl_verifypeer" = if (!value) "--insecure" else NULL, + "verbose" = if (value) "--verbose" else NULL, + "cookiejar" = paste0('--cookie-jar "', value, '"'), + "cookiefile" = paste0('--cookie "', value, '"'), + # For unknown options, create a generic --option format + paste0("--", gsub("_", "-", name), " ", value) + ) + if (!is.null(curl_flag)) { + cmd_parts <- c(cmd_parts, curl_flag) + } + } + } + + # Handle body data if present + if (!is.null(.req$body)) { + body_type <- .req$body$type %||% "empty" + # if content_type set here we use it + content_type <- .req$body$content_type + + # if content_type not set we need to infer from body type + if (is.null(content_type) || !nzchar(content_type)) { + if (body_type == "json") { + content_type <- "application/json" + } else if (body_type == "form") { + content_type <- "application/x-www-form-urlencoded" + } + } + + # Add content-type header if we have one and it's not already set + if (!is.null(content_type)) { + if ( + is.null(.req$headers) || + !("content-type" %in% tolower(names(.req$headers))) + ) { + cmd_parts <- c( + cmd_parts, + paste0('-H "Content-Type: ', content_type, '"') + ) + } + } + + # add body data + switch( + body_type, + "string" = { + data <- .req$body$data + cmd_parts <- c(cmd_parts, paste0('-d "', gsub('"', '\\"', data), '"')) + }, + "raw" = { + # Raw bytes - use --data-binary + cmd_parts <- c(cmd_parts, '--data-binary "@-"') + }, + "file" = { + path <- .req$body$data + cmd_parts <- c(cmd_parts, paste0('--data-binary "@', path, '"')) + }, + "json" = { + data <- unobfuscate(.req$body$data, handle = "reveal") + json_data <- jsonlite::toJSON(data, auto_unbox = TRUE) + cmd_parts <- c(cmd_parts, paste0('-d \'', json_data, '\'')) + }, + "form" = { + form_data <- unobfuscate(.req$body$data, handle = "reveal") + form_string <- paste( + names(form_data), + form_data, + sep = "=", + collapse = "&" + ) + cmd_parts <- c(cmd_parts, paste0('-d "', form_string, '"')) + }, + "multipart" = { + form_data <- unobfuscate(.req$body$data, handle = "reveal") + for (name in names(form_data)) { + value <- form_data[[name]] + cmd_parts <- c(cmd_parts, paste0('-F "', name, '=', value, '"')) + } + } + ) + } + + cmd_parts <- c(cmd_parts, paste0('"', url, '"')) + + # Join all parts with proper formatting + if (length(cmd_parts) <= 2) { + # Simple commands on one line + paste(cmd_parts, collapse = " ") + } else { + # Multi-line format with continuation - but keep first part on same line as 'curl' + first_part <- paste(cmd_parts[1:2], collapse = " ") + remaining_parts <- cmd_parts[-(1:2)] + + if (length(remaining_parts) == 0) { + first_part + } else { + formatted_parts <- paste0(" ", remaining_parts, " \\") + # Remove the trailing backslash from the last part + formatted_parts[length(formatted_parts)] <- gsub( + " \\\\$", + "", + formatted_parts[length(formatted_parts)] + ) + + paste(c(paste0(first_part, " \\"), formatted_parts), collapse = "\n") + } + } +} diff --git a/man/httr2_translate.Rd b/man/httr2_translate.Rd new file mode 100644 index 00000000..a610d2f1 --- /dev/null +++ b/man/httr2_translate.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/httr2-translate.R +\name{httr2_translate} +\alias{httr2_translate} +\title{Translate httr2 request to curl command} +\usage{ +httr2_translate(.req) +} +\arguments{ +\item{.req}{An httr2 request object created with \code{\link[=request]{request()}}.} +} +\value{ +A character string containing the curl command. +} +\description{ +Convert an httr2 request object to equivalent curl command line syntax. +This is useful for debugging, sharing requests, or converting to other tools. +} +\examples{ +\dontrun{ +# Basic GET request +request("https://httpbin.org/get") |> + httr2_translate() + +# POST with JSON body +request("https://httpbin.org/post") |> + req_body_json(list(name = "value")) |> + httr2_translate() + +# POST with form data +request("https://httpbin.org/post") |> + req_body_form(name = "value") |> + httr2_translate() +} +} diff --git a/tests/testthat/_snaps/httr2-translate.md b/tests/testthat/_snaps/httr2-translate.md new file mode 100644 index 00000000..c29b3fcd --- /dev/null +++ b/tests/testthat/_snaps/httr2-translate.md @@ -0,0 +1,135 @@ +# httr2_translate() works with basic GET requests + + Code + httr2_translate(request("https://httpbin.org/get")) + Output + [1] "curl \"https://httpbin.org/get\"" + +# httr2_translate() works with POST methods + + Code + httr2_translate(req_method(request("https://httpbin.org/post"), "POST")) + Output + [1] "curl -X POST \\\n \"https://httpbin.org/post\"" + +# httr2_translate() works with headers + + Code + httr2_translate(req_headers(request("https://httpbin.org/get"), Accept = "application/json", + `User-Agent` = "httr2/1.0")) + Output + [1] "curl -H \"Accept: application/json\" \\\n -H \"User-Agent: httr2/1.0\" \\\n \"https://httpbin.org/get\"" + +# httr2_translate() works with JSON bodies + + Code + httr2_translate(req_body_json(request("https://httpbin.org/post"), list(name = "test", + value = 123))) + Output + [1] "curl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\":\"test\",\"value\":123}' \\\n \"https://httpbin.org/post\"" + +# httr2_translate() works with form bodies + + Code + httr2_translate(req_body_form(request("https://httpbin.org/post"), name = "test", + value = "123")) + Output + [1] "curl -X POST \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"name=test&value=123\" \\\n \"https://httpbin.org/post\"" + +# httr2_translate() works with multipart bodies + + Code + httr2_translate(req_body_multipart(request("https://httpbin.org/post"), name = "test", + value = "123")) + Output + [1] "curl -X POST \\\n -F \"name=test\" \\\n -F \"value=123\" \\\n \"https://httpbin.org/post\"" + +# httr2_translate() works with string bodies + + Code + httr2_translate(req_body_raw(request("https://httpbin.org/post"), "test data", + type = "text/plain")) + Output + [1] "curl -X POST \\\n -H \"Content-Type: text/plain\" \\\n -d \"test data\" \\\n \"https://httpbin.org/post\"" + +# httr2_translate() works with file bodies + + Code + httr2_translate(req_body_file(request("https://httpbin.org/post"), path, type = "text/plain")) + Output + [1] "curl -X POST \\\n -H \"Content-Type: text/plain\" \\\n --data-binary \"@\" \\\n \"https://httpbin.org/post\"" + +# httr2_translate() works with custom content types + + Code + httr2_translate(req_body_json(request("https://httpbin.org/post"), list(test = "data"), + type = "application/vnd.api+json")) + Output + [1] "curl -X POST \\\n -H \"Content-Type: application/vnd.api+json\" \\\n -d '{\"test\":\"data\"}' \\\n \"https://httpbin.org/post\"" + +# httr2_translate() works with options + + Code + httr2_translate(req_options(request("https://httpbin.org/get"), timeout = 30, + verbose = TRUE, ssl_verifypeer = FALSE)) + Output + [1] "curl --max-time 30 \\\n --verbose \\\n --insecure \\\n \"https://httpbin.org/get\"" + +# httr2_translate() works with cookies + + Code + httr2_translate(req_options(request("https://httpbin.org/cookies"), cookiejar = cookie_file, + cookiefile = cookie_file)) + Output + [1] "curl --cookie-jar \"\" \\\n --cookie \"\" \\\n \"https://httpbin.org/cookies\"" + +# httr2_translate() works with obfuscated values in headers + + Code + httr2_translate(req_headers(request("https://httpbin.org/get"), Authorization = obfuscated( + "ZdYJeG8zwISodg0nu4UxBhs"))) + Output + [1] "curl -H \"Authorization: y\" \\\n \"https://httpbin.org/get\"" + +# httr2_translate() works with obfuscated values in JSON body + + Code + httr2_translate(req_body_json(request("https://httpbin.org/post"), list( + username = "test", password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs")))) + Output + [1] "curl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d '{\"username\":\"test\",\"password\":\"y\"}' \\\n \"https://httpbin.org/post\"" + +# httr2_translate() works with obfuscated values in form body + + Code + httr2_translate(req_body_form(request("https://httpbin.org/post"), username = "test", + password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs"))) + Output + [1] "curl -X POST \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"username=test&password=y\" \\\n \"https://httpbin.org/post\"" + +# httr2_translate() works with complex requests + + Code + httr2_translate(req_options(req_body_json(req_headers(req_method(request( + "https://api.github.com/user/repos"), "POST"), Accept = "application/vnd.github.v3+json", + Authorization = obfuscated("ZdYJeG8zwISodg0nu4UxBhs"), `User-Agent` = "MyApp/1.0"), + list(name = "test-repo", description = "A test repository", private = TRUE)), + timeout = 60)) + Output + [1] "curl -X POST \\\n -H \"Accept: application/vnd.github.v3+json\" \\\n -H \"Authorization: y\" \\\n -H \"User-Agent: MyApp/1.0\" \\\n --max-time 60 \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\":\"test-repo\",\"description\":\"A test repository\",\"private\":true}' \\\n \"https://api.github.com/user/repos\"" + +# httr2_translate() works with simple requests (single line) + + Code + httr2_translate(request("https://httpbin.org/get")) + Output + [1] "curl \"https://httpbin.org/get\"" + +# httr2_translate() validates input + + Code + httr2_translate("not a request") + Condition + Error in `httr2_translate()`: + ! `.req` must be an HTTP request object, not the string "not a request". + diff --git a/tests/testthat/test-httr2-translate.R b/tests/testthat/test-httr2-translate.R new file mode 100644 index 00000000..b3980dcd --- /dev/null +++ b/tests/testthat/test-httr2-translate.R @@ -0,0 +1,163 @@ +test_that("httr2_translate() works with basic GET requests", { + expect_snapshot({ + request("https://httpbin.org/get") |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with POST methods", { + expect_snapshot({ + request("https://httpbin.org/post") |> + req_method("POST") |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with headers", { + expect_snapshot({ + request("https://httpbin.org/get") |> + req_headers("Accept" = "application/json", "User-Agent" = "httr2/1.0") |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with JSON bodies", { + expect_snapshot({ + request("https://httpbin.org/post") |> + req_body_json(list(name = "test", value = 123)) |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with form bodies", { + expect_snapshot({ + request("https://httpbin.org/post") |> + req_body_form(name = "test", value = "123") |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with multipart bodies", { + expect_snapshot({ + request("https://httpbin.org/post") |> + req_body_multipart(name = "test", value = "123") |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with string bodies", { + expect_snapshot({ + request("https://httpbin.org/post") |> + req_body_raw("test data", type = "text/plain") |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with file bodies", { + path <- tempfile() + writeLines("test content", path) + + expect_snapshot( + { + request("https://httpbin.org/post") |> + req_body_file(path, type = "text/plain") |> + httr2_translate() + }, + transform = function(x) { + gsub(path, "", x, fixed = TRUE) + } + ) +}) + +test_that("httr2_translate() works with custom content types", { + expect_snapshot({ + request("https://httpbin.org/post") |> + req_body_json(list(test = "data"), type = "application/vnd.api+json") |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with options", { + expect_snapshot({ + request("https://httpbin.org/get") |> + req_options(timeout = 30, verbose = TRUE, ssl_verifypeer = FALSE) |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with cookies", { + cookie_file <- tempfile() + + expect_snapshot( + { + request("https://httpbin.org/cookies") |> + req_options(cookiejar = cookie_file, cookiefile = cookie_file) |> + httr2_translate() + }, + transform = function(x) { + gsub(cookie_file, "", x, fixed = TRUE) + } + ) +}) + +test_that("httr2_translate() works with obfuscated values in headers", { + expect_snapshot({ + request("https://httpbin.org/get") |> + req_headers("Authorization" = obfuscated("ZdYJeG8zwISodg0nu4UxBhs")) |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with obfuscated values in JSON body", { + expect_snapshot({ + request("https://httpbin.org/post") |> + req_body_json(list( + username = "test", + password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs") + )) |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with obfuscated values in form body", { + expect_snapshot({ + request("https://httpbin.org/post") |> + req_body_form( + username = "test", + password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs") + ) |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with complex requests", { + expect_snapshot({ + request("https://api.github.com/user/repos") |> + req_method("POST") |> + req_headers( + "Accept" = "application/vnd.github.v3+json", + "Authorization" = obfuscated("ZdYJeG8zwISodg0nu4UxBhs"), + "User-Agent" = "MyApp/1.0" + ) |> + req_body_json(list( + name = "test-repo", + description = "A test repository", + private = TRUE + )) |> + req_options(timeout = 60) |> + httr2_translate() + }) +}) + +test_that("httr2_translate() works with simple requests (single line)", { + expect_snapshot({ + request("https://httpbin.org/get") |> + httr2_translate() + }) +}) + +test_that("httr2_translate() validates input", { + expect_snapshot(error = TRUE, { + httr2_translate("not a request") + }) +}) \ No newline at end of file From 86e8255e2c7e87bb7fc9e860aee070f389629974 Mon Sep 17 00:00:00 2001 From: Josiah Parry Date: Wed, 20 Aug 2025 08:56:05 -0700 Subject: [PATCH 2/6] run air and clarify some comments --- R/httr2-translate.R | 25 ++++++++++++------------- tests/testthat/test-httr2-translate.R | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/R/httr2-translate.R b/R/httr2-translate.R index bec13bc4..0e01b0b0 100644 --- a/R/httr2-translate.R +++ b/R/httr2-translate.R @@ -52,27 +52,27 @@ httr2_translate <- function(.req) { headers <- .req$headers for (name in names(headers)) { value <- headers[[name]] - - # Handle weak references first + + # handle weakrefs if (rlang::is_weakref(value)) { value <- rlang::wref_value(value) } - - # Handle obfuscated values by revealing them + + # unobfuscate obfuscated if (is_obfuscated(value)) { value <- unobfuscate(value, handle = "reveal") } - + cmd_parts <- c(cmd_parts, paste0('-H "', name, ': ', value, '"')) } } - # Handle options (curl options like timeout, proxy, etc.) + # manage options if (!is.null(.req$options) && length(.req$options) > 0) { options <- .req$options for (name in names(options)) { value <- options[[name]] - # Convert common curl options to curl command flags + # convert options to curl flags curl_flag <- switch( name, "timeout" = paste0("--max-time ", value), @@ -85,7 +85,7 @@ httr2_translate <- function(.req) { "verbose" = if (value) "--verbose" else NULL, "cookiejar" = paste0('--cookie-jar "', value, '"'), "cookiefile" = paste0('--cookie "', value, '"'), - # For unknown options, create a generic --option format + # for unknown options try guess the flag if it was the intention paste0("--", gsub("_", "-", name), " ", value) ) if (!is.null(curl_flag)) { @@ -94,7 +94,6 @@ httr2_translate <- function(.req) { } } - # Handle body data if present if (!is.null(.req$body)) { body_type <- .req$body$type %||% "empty" # if content_type set here we use it @@ -109,7 +108,7 @@ httr2_translate <- function(.req) { } } - # Add content-type header if we have one and it's not already set + # add content-type header if we have one and it's not already set if (!is.null(content_type)) { if ( is.null(.req$headers) || @@ -164,12 +163,12 @@ httr2_translate <- function(.req) { cmd_parts <- c(cmd_parts, paste0('"', url, '"')) - # Join all parts with proper formatting + # join all parts with proper formatting if (length(cmd_parts) <= 2) { - # Simple commands on one line paste(cmd_parts, collapse = " ") } else { - # Multi-line format with continuation - but keep first part on same line as 'curl' + # need to ensure that "curl" isn't on its own line + # for compatibility with curl_translate() first_part <- paste(cmd_parts[1:2], collapse = " ") remaining_parts <- cmd_parts[-(1:2)] diff --git a/tests/testthat/test-httr2-translate.R b/tests/testthat/test-httr2-translate.R index b3980dcd..03a28268 100644 --- a/tests/testthat/test-httr2-translate.R +++ b/tests/testthat/test-httr2-translate.R @@ -16,7 +16,10 @@ test_that("httr2_translate() works with POST methods", { test_that("httr2_translate() works with headers", { expect_snapshot({ request("https://httpbin.org/get") |> - req_headers("Accept" = "application/json", "User-Agent" = "httr2/1.0") |> + req_headers( + "Accept" = "application/json", + "User-Agent" = "httr2/1.0" + ) |> httr2_translate() }) }) @@ -56,7 +59,7 @@ test_that("httr2_translate() works with string bodies", { test_that("httr2_translate() works with file bodies", { path <- tempfile() writeLines("test content", path) - + expect_snapshot( { request("https://httpbin.org/post") |> @@ -72,7 +75,10 @@ test_that("httr2_translate() works with file bodies", { test_that("httr2_translate() works with custom content types", { expect_snapshot({ request("https://httpbin.org/post") |> - req_body_json(list(test = "data"), type = "application/vnd.api+json") |> + req_body_json( + list(test = "data"), + type = "application/vnd.api+json" + ) |> httr2_translate() }) }) @@ -87,7 +93,7 @@ test_that("httr2_translate() works with options", { test_that("httr2_translate() works with cookies", { cookie_file <- tempfile() - + expect_snapshot( { request("https://httpbin.org/cookies") |> @@ -123,7 +129,7 @@ test_that("httr2_translate() works with obfuscated values in form body", { expect_snapshot({ request("https://httpbin.org/post") |> req_body_form( - username = "test", + username = "test", password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs") ) |> httr2_translate() @@ -160,4 +166,4 @@ test_that("httr2_translate() validates input", { expect_snapshot(error = TRUE, { httr2_translate("not a request") }) -}) \ No newline at end of file +}) From be8141d32fb5aadfe9361420b2979b234f139b07 Mon Sep 17 00:00:00 2001 From: Josiah Parry Date: Wed, 20 Aug 2025 09:13:48 -0700 Subject: [PATCH 3/6] update snaps to work with windows and update pkgdown --- _pkgdown.yml | 3 ++- tests/testthat/_snaps/httr2-translate.md | 4 ++-- tests/testthat/test-httr2-translate.R | 13 +++++++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/_pkgdown.yml b/_pkgdown.yml index 7ec7f236..1231f0c9 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -76,6 +76,7 @@ reference: - title: Miscellaneous helpers contents: - curl_translate + - httr2_translate - is_online - title: OAuth @@ -88,7 +89,7 @@ reference: - title: Developer tooling desc: > These functions are useful when developing packges that use httr2. - + - subtitle: Keeping secrets contents: - obfuscate diff --git a/tests/testthat/_snaps/httr2-translate.md b/tests/testthat/_snaps/httr2-translate.md index c29b3fcd..f46219f0 100644 --- a/tests/testthat/_snaps/httr2-translate.md +++ b/tests/testthat/_snaps/httr2-translate.md @@ -57,7 +57,7 @@ Code httr2_translate(req_body_file(request("https://httpbin.org/post"), path, type = "text/plain")) Output - [1] "curl -X POST \\\n -H \"Content-Type: text/plain\" \\\n --data-binary \"@\" \\\n \"https://httpbin.org/post\"" + [1] "curl -X POST \\\n -H \"Content-Type: text/plain\" \\\n --data-binary \"@/file123d32a1a3f82\" \\\n \"https://httpbin.org/post\"" # httr2_translate() works with custom content types @@ -81,7 +81,7 @@ httr2_translate(req_options(request("https://httpbin.org/cookies"), cookiejar = cookie_file, cookiefile = cookie_file)) Output - [1] "curl --cookie-jar \"\" \\\n --cookie \"\" \\\n \"https://httpbin.org/cookies\"" + [1] "curl --cookie-jar \"/file123d315b9cab\" \\\n --cookie \"/file123d315b9cab\" \\\n \"https://httpbin.org/cookies\"" # httr2_translate() works with obfuscated values in headers diff --git a/tests/testthat/test-httr2-translate.R b/tests/testthat/test-httr2-translate.R index 03a28268..8aa88184 100644 --- a/tests/testthat/test-httr2-translate.R +++ b/tests/testthat/test-httr2-translate.R @@ -60,6 +60,9 @@ test_that("httr2_translate() works with file bodies", { path <- tempfile() writeLines("test content", path) + # normalize the path + path <- normalizePath(path) + expect_snapshot( { request("https://httpbin.org/post") |> @@ -67,7 +70,7 @@ test_that("httr2_translate() works with file bodies", { httr2_translate() }, transform = function(x) { - gsub(path, "", x, fixed = TRUE) + gsub(dirname(path), "", x, fixed = TRUE) } ) }) @@ -94,6 +97,12 @@ test_that("httr2_translate() works with options", { test_that("httr2_translate() works with cookies", { cookie_file <- tempfile() + # create the tempfile + file.create(cookie_file) + + # normalize the path + cookie_file <- normalizePath(cookie_file) + expect_snapshot( { request("https://httpbin.org/cookies") |> @@ -101,7 +110,7 @@ test_that("httr2_translate() works with cookies", { httr2_translate() }, transform = function(x) { - gsub(cookie_file, "", x, fixed = TRUE) + gsub(dirname(cookie_file), "", x, fixed = TRUE) } ) }) From 3b030f3c5b14f6fb2e0df1fe852988a68874786f Mon Sep 17 00:00:00 2001 From: Josiah Parry Date: Wed, 20 Aug 2025 09:48:57 -0700 Subject: [PATCH 4/6] replace entire temp path not just directory --- tests/testthat/_snaps/httr2-translate.md | 4 ++-- tests/testthat/test-httr2-translate.R | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/testthat/_snaps/httr2-translate.md b/tests/testthat/_snaps/httr2-translate.md index f46219f0..c29b3fcd 100644 --- a/tests/testthat/_snaps/httr2-translate.md +++ b/tests/testthat/_snaps/httr2-translate.md @@ -57,7 +57,7 @@ Code httr2_translate(req_body_file(request("https://httpbin.org/post"), path, type = "text/plain")) Output - [1] "curl -X POST \\\n -H \"Content-Type: text/plain\" \\\n --data-binary \"@/file123d32a1a3f82\" \\\n \"https://httpbin.org/post\"" + [1] "curl -X POST \\\n -H \"Content-Type: text/plain\" \\\n --data-binary \"@\" \\\n \"https://httpbin.org/post\"" # httr2_translate() works with custom content types @@ -81,7 +81,7 @@ httr2_translate(req_options(request("https://httpbin.org/cookies"), cookiejar = cookie_file, cookiefile = cookie_file)) Output - [1] "curl --cookie-jar \"/file123d315b9cab\" \\\n --cookie \"/file123d315b9cab\" \\\n \"https://httpbin.org/cookies\"" + [1] "curl --cookie-jar \"\" \\\n --cookie \"\" \\\n \"https://httpbin.org/cookies\"" # httr2_translate() works with obfuscated values in headers diff --git a/tests/testthat/test-httr2-translate.R b/tests/testthat/test-httr2-translate.R index 8aa88184..d1c0047d 100644 --- a/tests/testthat/test-httr2-translate.R +++ b/tests/testthat/test-httr2-translate.R @@ -70,7 +70,7 @@ test_that("httr2_translate() works with file bodies", { httr2_translate() }, transform = function(x) { - gsub(dirname(path), "", x, fixed = TRUE) + gsub(path, "", x, fixed = TRUE) } ) }) @@ -110,7 +110,7 @@ test_that("httr2_translate() works with cookies", { httr2_translate() }, transform = function(x) { - gsub(dirname(cookie_file), "", x, fixed = TRUE) + gsub(cookie_file, "", x, fixed = TRUE) } ) }) From 3e5c4e4b266f01aae5b1ee9ae1c310385a0486b0 Mon Sep 17 00:00:00 2001 From: Josiah Parry Date: Wed, 20 Aug 2025 10:18:41 -0700 Subject: [PATCH 5/6] use / for winslash --- tests/testthat/test-httr2-translate.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-httr2-translate.R b/tests/testthat/test-httr2-translate.R index d1c0047d..652dd7c1 100644 --- a/tests/testthat/test-httr2-translate.R +++ b/tests/testthat/test-httr2-translate.R @@ -61,7 +61,7 @@ test_that("httr2_translate() works with file bodies", { writeLines("test content", path) # normalize the path - path <- normalizePath(path) + path <- normalizePath(path, winslash = "/") expect_snapshot( { @@ -101,7 +101,7 @@ test_that("httr2_translate() works with cookies", { file.create(cookie_file) # normalize the path - cookie_file <- normalizePath(cookie_file) + cookie_file <- normalizePath(cookie_file, winslash = "/") expect_snapshot( { From 822fa8c5a6ec062da0d941d24aea69812b9e274b Mon Sep 17 00:00:00 2001 From: Josiah Parry Date: Wed, 27 Aug 2025 13:45:31 -0700 Subject: [PATCH 6/6] address PR feedback --- NAMESPACE | 2 +- R/curl.R | 1 + R/httr2-translate.R | 189 --------------- R/req-as-curl.R | 224 ++++++++++++++++++ man/curl_translate.Rd | 3 + man/{httr2_translate.Rd => req_as_curl.Rd} | 20 +- tests/testthat/_snaps/httr2-translate.md | 135 ----------- tests/testthat/_snaps/req-as-curl.md | 176 ++++++++++++++ ...t-httr2-translate.R => test-req-as-curl.R} | 98 ++++---- 9 files changed, 463 insertions(+), 385 deletions(-) delete mode 100644 R/httr2-translate.R create mode 100644 R/req-as-curl.R rename man/{httr2_translate.Rd => req_as_curl.Rd} (67%) delete mode 100644 tests/testthat/_snaps/httr2-translate.md create mode 100644 tests/testthat/_snaps/req-as-curl.md rename tests/testthat/{test-httr2-translate.R => test-req-as-curl.R} (53%) diff --git a/NAMESPACE b/NAMESPACE index 205ec32f..ef35a344 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -24,7 +24,6 @@ export(curl_help) export(curl_translate) export(example_github_client) export(example_url) -export(httr2_translate) export(is_online) export(iterate_with_cursor) export(iterate_with_link_url) @@ -62,6 +61,7 @@ export(oauth_token) export(oauth_token_cached) export(obfuscate) export(obfuscated) +export(req_as_curl) export(req_auth_aws_v4) export(req_auth_basic) export(req_auth_bearer_token) diff --git a/R/curl.R b/R/curl.R index 0f4605ad..8bcba7b5 100644 --- a/R/curl.R +++ b/R/curl.R @@ -24,6 +24,7 @@ #' was copied from the clipboard, the translation will be copied back #' to the clipboard. #' @export +#' @seealso [req_as_curl()] #' @examples #' curl_translate("curl http://example.com") #' curl_translate("curl http://example.com -X DELETE") diff --git a/R/httr2-translate.R b/R/httr2-translate.R deleted file mode 100644 index 0e01b0b0..00000000 --- a/R/httr2-translate.R +++ /dev/null @@ -1,189 +0,0 @@ -#' Translate httr2 request to curl command -#' -#' Convert an httr2 request object to equivalent curl command line syntax. -#' This is useful for debugging, sharing requests, or converting to other tools. -#' -#' @param .req An httr2 request object created with [request()]. -#' @return A character string containing the curl command. -#' @export -#' @examples -#' \dontrun{ -#' # Basic GET request -#' request("https://httpbin.org/get") |> -#' httr2_translate() -#' -#' # POST with JSON body -#' request("https://httpbin.org/post") |> -#' req_body_json(list(name = "value")) |> -#' httr2_translate() -#' -#' # POST with form data -#' request("https://httpbin.org/post") |> -#' req_body_form(name = "value") |> -#' httr2_translate() -#' } -httr2_translate <- function(.req) { - # validate the request - check_request(.req) - - # Extract URL - url <- .req$url - - # use the request's method if it is set, otherwise infer - method <- .req$method %||% - { - if (!is.null(.req$body$data)) { - "POST" - } else { - "GET" - } - } - - # we will append to cmd_parts to build up the request - cmd_parts <- c("curl") - - # if the method isn't GET, it needs to be specified with `-X` - if (method != "GET") { - cmd_parts <- c(cmd_parts, paste0("-X ", method)) - } - - # if headers are present, add them using -H flag - if (!is.null(.req$headers) && length(.req$headers) > 0) { - headers <- .req$headers - for (name in names(headers)) { - value <- headers[[name]] - - # handle weakrefs - if (rlang::is_weakref(value)) { - value <- rlang::wref_value(value) - } - - # unobfuscate obfuscated - if (is_obfuscated(value)) { - value <- unobfuscate(value, handle = "reveal") - } - - cmd_parts <- c(cmd_parts, paste0('-H "', name, ': ', value, '"')) - } - } - - # manage options - if (!is.null(.req$options) && length(.req$options) > 0) { - options <- .req$options - for (name in names(options)) { - value <- options[[name]] - # convert options to curl flags - curl_flag <- switch( - name, - "timeout" = paste0("--max-time ", value), - "connecttimeout" = paste0("--connect-timeout ", value), - "proxy" = paste0("--proxy ", value), - "useragent" = paste0('--user-agent "', value, '"'), - "referer" = paste0('--referer "', value, '"'), - "followlocation" = if (value) "--location" else NULL, - "ssl_verifypeer" = if (!value) "--insecure" else NULL, - "verbose" = if (value) "--verbose" else NULL, - "cookiejar" = paste0('--cookie-jar "', value, '"'), - "cookiefile" = paste0('--cookie "', value, '"'), - # for unknown options try guess the flag if it was the intention - paste0("--", gsub("_", "-", name), " ", value) - ) - if (!is.null(curl_flag)) { - cmd_parts <- c(cmd_parts, curl_flag) - } - } - } - - if (!is.null(.req$body)) { - body_type <- .req$body$type %||% "empty" - # if content_type set here we use it - content_type <- .req$body$content_type - - # if content_type not set we need to infer from body type - if (is.null(content_type) || !nzchar(content_type)) { - if (body_type == "json") { - content_type <- "application/json" - } else if (body_type == "form") { - content_type <- "application/x-www-form-urlencoded" - } - } - - # add content-type header if we have one and it's not already set - if (!is.null(content_type)) { - if ( - is.null(.req$headers) || - !("content-type" %in% tolower(names(.req$headers))) - ) { - cmd_parts <- c( - cmd_parts, - paste0('-H "Content-Type: ', content_type, '"') - ) - } - } - - # add body data - switch( - body_type, - "string" = { - data <- .req$body$data - cmd_parts <- c(cmd_parts, paste0('-d "', gsub('"', '\\"', data), '"')) - }, - "raw" = { - # Raw bytes - use --data-binary - cmd_parts <- c(cmd_parts, '--data-binary "@-"') - }, - "file" = { - path <- .req$body$data - cmd_parts <- c(cmd_parts, paste0('--data-binary "@', path, '"')) - }, - "json" = { - data <- unobfuscate(.req$body$data, handle = "reveal") - json_data <- jsonlite::toJSON(data, auto_unbox = TRUE) - cmd_parts <- c(cmd_parts, paste0('-d \'', json_data, '\'')) - }, - "form" = { - form_data <- unobfuscate(.req$body$data, handle = "reveal") - form_string <- paste( - names(form_data), - form_data, - sep = "=", - collapse = "&" - ) - cmd_parts <- c(cmd_parts, paste0('-d "', form_string, '"')) - }, - "multipart" = { - form_data <- unobfuscate(.req$body$data, handle = "reveal") - for (name in names(form_data)) { - value <- form_data[[name]] - cmd_parts <- c(cmd_parts, paste0('-F "', name, '=', value, '"')) - } - } - ) - } - - cmd_parts <- c(cmd_parts, paste0('"', url, '"')) - - # join all parts with proper formatting - if (length(cmd_parts) <= 2) { - paste(cmd_parts, collapse = " ") - } else { - # need to ensure that "curl" isn't on its own line - # for compatibility with curl_translate() - first_part <- paste(cmd_parts[1:2], collapse = " ") - remaining_parts <- cmd_parts[-(1:2)] - - if (length(remaining_parts) == 0) { - first_part - } else { - formatted_parts <- paste0(" ", remaining_parts, " \\") - # Remove the trailing backslash from the last part - formatted_parts[length(formatted_parts)] <- gsub( - " \\\\$", - "", - formatted_parts[length(formatted_parts)] - ) - - paste(c(paste0(first_part, " \\"), formatted_parts), collapse = "\n") - } - } -} diff --git a/R/req-as-curl.R b/R/req-as-curl.R new file mode 100644 index 00000000..0c027b52 --- /dev/null +++ b/R/req-as-curl.R @@ -0,0 +1,224 @@ +#' Translate an httr2 request to a curl command +#' +#' Convert an httr2 request object to equivalent curl command line syntax. +#' This is useful for debugging, sharing requests, or converting to other tools. +#' +#' @inheritParams req_perform +#' @return A character string containing the curl command. +#' @export +#' @examples +#' @seealso [curl_translate()] +#' \dontrun{ +#' # Basic GET request +#' request("https://httpbin.org/get") |> +#' req_as_curl() +#' +#' # POST with JSON body +#' request("https://httpbin.org/post") |> +#' req_body_json(list(name = "value")) |> +#' req_as_curl() +#' +#' # POST with form data +#' request("https://httpbin.org/post") |> +#' req_body_form(name = "value") |> +#' req_as_curl() +#' } +req_as_curl <- function(req) { + # validate the request + check_request(req) + + # Extract URL + url <- req_get_url(req) + + # use the request's method if it is set, otherwise infer + method <- req$method %||% + { + if (!is.null(req$body$data)) { + "POST" + } else { + "GET" + } + } + + # we will append to cmd_args to build up the request + cmd_args <- c() + + # if the method isn't GET, it needs to be specified with `-X` + if (method != "GET") { + cmd_args <- c(cmd_args, paste0("-X ", method)) + } + + # get headers and reveal obfuscated values + headers <- req_get_headers(req, redacted = "reveal") + + # if headers are present, add them using -H flag + if (!rlang::is_empty(headers)) { + for (name in names(headers)) { + value <- headers[[name]] + cmd_args <- c(cmd_args, paste0('-H "', name, ': ', value, '"')) + } + } + + known_curl_opts <- c( + "timeout", + "connecttimeout", + "proxy", + "useragent", + "referer", + "followlocation", + "verbose", + "cookiejar", + "cookiefile" + ) + + # manage options + # TODO make introspection function for options + options <- req$options + + # extract names of request's options + used_opts <- names(options) + + # identify options that are not known / handled + unknown_opts <- !used_opts %in% known_curl_opts + + # if any options are found that are not handled below, emit a message + if (any(unknown_opts)) { + cli::cli_alert_warning( + "Unable to translate option{?s} {.val {used_opts[unknown_opts]}}" + ) + } + + for (name in used_opts) { + value <- options[[name]] + # convert known options to curl flags other values are ignored + curl_flag <- switch( + name, + # supports req_timeout() + "timeout" = paste0("--max-time ", value), + "connecttimeout" = paste0("--connect-timeout ", value), + # supports req_proxy() + "proxy" = paste0("--proxy ", value), + # supports req_user_agent() + "useragent" = paste0('--user-agent "', value, '"'), + "referer" = paste0('--referer "', value, '"'), + # supports defualt behavior or httr2 following redirects + # rather than returning 302 status + "followlocation" = if (value) "--location" else NULL, + # support req_verbose() + "verbose" = if (value) "--verbose" else NULL, + # support req_cookie_preserve() and req_cookies_set() + "cookiejar" = paste0('--cookie-jar "', value, '"'), + "cookiefile" = paste0('--cookie "', value, '"') + ) + cmd_args <- c(cmd_args, curl_flag) + } + + cmd_args <- req_body_as_curl(req, cmd_args) + + # quote the url + url_quoted <- sprintf('"%s"', url) + + # if we have no arguments we just paste curl and the url together + res <- if (length(cmd_args) == 0) { + paste0("curl ", url_quoted) + } else { + cmd_lines <- paste0(cmd_args, " \\") + + # indent all args except the first + cmd_lines[-1] <- paste0(" ", cmd_lines[-1]) + + # append the url + cmd_lines <- c(cmd_lines, paste0(" ", url_quoted)) + + # combine with new line separation for all but first argument + res <- paste0( + "curl ", + cmd_lines[1], + "\n", + paste0(cmd_lines[-1], collapse = "\n") + ) + } + + structure(res, class = "httr2_cmd") +} + + +req_body_as_curl <- function(req, cmd_args) { + # extract the body and reveal obfuscated values + body <- req_get_body(req, obfuscated = "reveal") + + if (rlang::is_null(body)) { + return(cmd_args) + } + + body_type <- req$body$type %||% "empty" + + # if content_type set here we use it + content_type <- req$body$content_type + + # if content_type not set we need to infer from body type + if (rlang::is_null(content_type) || !nzchar(content_type)) { + content_type <- switch( + body_type, + "json" = "application/json", + "form" = "application/x-www-form-urlencoded" + ) + } + + # fetch headers for content-type check + headers <- req_get_headers(req) + + # if the headers aren't empty AND the content-type header is set + # we use that instead of what is inferred from the request object + if ( + !rlang::is_empty(headers) && ("content-type" %in% tolower(names(headers))) + ) { + content_type <- headers[["content-type"]] + } + + if (!rlang::is_null(content_type)) { + cmd_args <- c( + cmd_args, + paste0('-H "Content-Type: ', content_type, '"') + ) + } + + # add body data + switch( + body_type, + "string" = { + cmd_args <- c( + cmd_args, + paste0('-d "', gsub('"', '\\"', body), '"') + ) + }, + "raw" = { + # TODO: should the raw bytes be written to a temp file + # and be hanlded similarly to file? + cmd_args <- c(cmd_args, '--data-binary "@-"') + }, + "file" = { + cmd_args <- c(cmd_args, paste0('--data-binary "@', body, '"')) + }, + "json" = { + json_data <- jsonlite::toJSON(body, auto_unbox = TRUE) + cmd_args <- c(cmd_args, paste0('-d \'', json_data, '\'')) + }, + "form" = { + form_string <- paste( + names(body), + body, + sep = "=", + collapse = "&" + ) + cmd_args <- c(cmd_args, paste0('-d "', form_string, '"')) + }, + "multipart" = { + for (name in names(body)) { + value <- body[[name]] + cmd_args <- c(cmd_args, paste0('-F "', name, '=', value, '"')) + } + } + ) + cmd_args +} diff --git a/man/curl_translate.Rd b/man/curl_translate.Rd index f978854c..46ebfc3b 100644 --- a/man/curl_translate.Rd +++ b/man/curl_translate.Rd @@ -44,3 +44,6 @@ curl_translate("curl http://example.com -X DELETE") curl_translate("curl http://example.com --header A:1 --header B:2") curl_translate("curl http://example.com --verbose") } +\seealso{ +\code{\link[=req_as_curl]{req_as_curl()}} +} diff --git a/man/httr2_translate.Rd b/man/req_as_curl.Rd similarity index 67% rename from man/httr2_translate.Rd rename to man/req_as_curl.Rd index a610d2f1..006fafcc 100644 --- a/man/httr2_translate.Rd +++ b/man/req_as_curl.Rd @@ -1,13 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/httr2-translate.R -\name{httr2_translate} -\alias{httr2_translate} +% Please edit documentation in R/req-as-curl.R +\name{req_as_curl} +\alias{req_as_curl} \title{Translate httr2 request to curl command} \usage{ -httr2_translate(.req) -} -\arguments{ -\item{.req}{An httr2 request object created with \code{\link[=request]{request()}}.} +req_as_curl(.req) } \value{ A character string containing the curl command. @@ -16,20 +13,21 @@ A character string containing the curl command. Convert an httr2 request object to equivalent curl command line syntax. This is useful for debugging, sharing requests, or converting to other tools. } -\examples{ +\seealso{ +\code{\link[=curl_translate]{curl_translate()}} \dontrun{ # Basic GET request request("https://httpbin.org/get") |> - httr2_translate() + req_as_curl() # POST with JSON body request("https://httpbin.org/post") |> req_body_json(list(name = "value")) |> - httr2_translate() + req_as_curl() # POST with form data request("https://httpbin.org/post") |> req_body_form(name = "value") |> - httr2_translate() + req_as_curl() } } diff --git a/tests/testthat/_snaps/httr2-translate.md b/tests/testthat/_snaps/httr2-translate.md deleted file mode 100644 index c29b3fcd..00000000 --- a/tests/testthat/_snaps/httr2-translate.md +++ /dev/null @@ -1,135 +0,0 @@ -# httr2_translate() works with basic GET requests - - Code - httr2_translate(request("https://httpbin.org/get")) - Output - [1] "curl \"https://httpbin.org/get\"" - -# httr2_translate() works with POST methods - - Code - httr2_translate(req_method(request("https://httpbin.org/post"), "POST")) - Output - [1] "curl -X POST \\\n \"https://httpbin.org/post\"" - -# httr2_translate() works with headers - - Code - httr2_translate(req_headers(request("https://httpbin.org/get"), Accept = "application/json", - `User-Agent` = "httr2/1.0")) - Output - [1] "curl -H \"Accept: application/json\" \\\n -H \"User-Agent: httr2/1.0\" \\\n \"https://httpbin.org/get\"" - -# httr2_translate() works with JSON bodies - - Code - httr2_translate(req_body_json(request("https://httpbin.org/post"), list(name = "test", - value = 123))) - Output - [1] "curl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\":\"test\",\"value\":123}' \\\n \"https://httpbin.org/post\"" - -# httr2_translate() works with form bodies - - Code - httr2_translate(req_body_form(request("https://httpbin.org/post"), name = "test", - value = "123")) - Output - [1] "curl -X POST \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"name=test&value=123\" \\\n \"https://httpbin.org/post\"" - -# httr2_translate() works with multipart bodies - - Code - httr2_translate(req_body_multipart(request("https://httpbin.org/post"), name = "test", - value = "123")) - Output - [1] "curl -X POST \\\n -F \"name=test\" \\\n -F \"value=123\" \\\n \"https://httpbin.org/post\"" - -# httr2_translate() works with string bodies - - Code - httr2_translate(req_body_raw(request("https://httpbin.org/post"), "test data", - type = "text/plain")) - Output - [1] "curl -X POST \\\n -H \"Content-Type: text/plain\" \\\n -d \"test data\" \\\n \"https://httpbin.org/post\"" - -# httr2_translate() works with file bodies - - Code - httr2_translate(req_body_file(request("https://httpbin.org/post"), path, type = "text/plain")) - Output - [1] "curl -X POST \\\n -H \"Content-Type: text/plain\" \\\n --data-binary \"@\" \\\n \"https://httpbin.org/post\"" - -# httr2_translate() works with custom content types - - Code - httr2_translate(req_body_json(request("https://httpbin.org/post"), list(test = "data"), - type = "application/vnd.api+json")) - Output - [1] "curl -X POST \\\n -H \"Content-Type: application/vnd.api+json\" \\\n -d '{\"test\":\"data\"}' \\\n \"https://httpbin.org/post\"" - -# httr2_translate() works with options - - Code - httr2_translate(req_options(request("https://httpbin.org/get"), timeout = 30, - verbose = TRUE, ssl_verifypeer = FALSE)) - Output - [1] "curl --max-time 30 \\\n --verbose \\\n --insecure \\\n \"https://httpbin.org/get\"" - -# httr2_translate() works with cookies - - Code - httr2_translate(req_options(request("https://httpbin.org/cookies"), cookiejar = cookie_file, - cookiefile = cookie_file)) - Output - [1] "curl --cookie-jar \"\" \\\n --cookie \"\" \\\n \"https://httpbin.org/cookies\"" - -# httr2_translate() works with obfuscated values in headers - - Code - httr2_translate(req_headers(request("https://httpbin.org/get"), Authorization = obfuscated( - "ZdYJeG8zwISodg0nu4UxBhs"))) - Output - [1] "curl -H \"Authorization: y\" \\\n \"https://httpbin.org/get\"" - -# httr2_translate() works with obfuscated values in JSON body - - Code - httr2_translate(req_body_json(request("https://httpbin.org/post"), list( - username = "test", password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs")))) - Output - [1] "curl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d '{\"username\":\"test\",\"password\":\"y\"}' \\\n \"https://httpbin.org/post\"" - -# httr2_translate() works with obfuscated values in form body - - Code - httr2_translate(req_body_form(request("https://httpbin.org/post"), username = "test", - password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs"))) - Output - [1] "curl -X POST \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"username=test&password=y\" \\\n \"https://httpbin.org/post\"" - -# httr2_translate() works with complex requests - - Code - httr2_translate(req_options(req_body_json(req_headers(req_method(request( - "https://api.github.com/user/repos"), "POST"), Accept = "application/vnd.github.v3+json", - Authorization = obfuscated("ZdYJeG8zwISodg0nu4UxBhs"), `User-Agent` = "MyApp/1.0"), - list(name = "test-repo", description = "A test repository", private = TRUE)), - timeout = 60)) - Output - [1] "curl -X POST \\\n -H \"Accept: application/vnd.github.v3+json\" \\\n -H \"Authorization: y\" \\\n -H \"User-Agent: MyApp/1.0\" \\\n --max-time 60 \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\":\"test-repo\",\"description\":\"A test repository\",\"private\":true}' \\\n \"https://api.github.com/user/repos\"" - -# httr2_translate() works with simple requests (single line) - - Code - httr2_translate(request("https://httpbin.org/get")) - Output - [1] "curl \"https://httpbin.org/get\"" - -# httr2_translate() validates input - - Code - httr2_translate("not a request") - Condition - Error in `httr2_translate()`: - ! `.req` must be an HTTP request object, not the string "not a request". - diff --git a/tests/testthat/_snaps/req-as-curl.md b/tests/testthat/_snaps/req-as-curl.md new file mode 100644 index 00000000..e7350bce --- /dev/null +++ b/tests/testthat/_snaps/req-as-curl.md @@ -0,0 +1,176 @@ +# req_as_curl() works with basic GET requests + + Code + req_as_curl(request("https://hb.cran.dev/get")) + Output + curl "https://hb.cran.dev/get" + +# req_as_curl() works with POST methods + + Code + req_as_curl(req_method(request("https://hb.cran.dev/post"), "POST")) + Output + curl -X POST \ + "https://hb.cran.dev/post" + +# req_as_curl() works with headers + + Code + req_as_curl(req_headers(request("https://hb.cran.dev/get"), Accept = "application/json", + `User-Agent` = "httr2/1.0")) + Output + curl -H "Accept: application/json" \ + -H "User-Agent: httr2/1.0" \ + "https://hb.cran.dev/get" + +# req_as_curl() works with JSON bodies + + Code + req_as_curl(req_body_json(request("https://hb.cran.dev/post"), list(name = "test", + value = 123))) + Output + curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"name":"test","value":123}' \ + "https://hb.cran.dev/post" + +# req_as_curl() works with form bodies + + Code + req_as_curl(req_body_form(request("https://hb.cran.dev/post"), name = "test", + value = "123")) + Output + curl -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "name=test&value=123" \ + "https://hb.cran.dev/post" + +# req_as_curl() works with multipart bodies + + Code + req_as_curl(req_body_multipart(request("https://hb.cran.dev/post"), name = "test", + value = "123")) + Output + curl -X POST \ + -F "name=test" \ + -F "value=123" \ + "https://hb.cran.dev/post" + +# req_as_curl() works with string bodies + + Code + req_as_curl(req_body_raw(request("https://hb.cran.dev/post"), "test data", + type = "text/plain")) + Output + curl -X POST \ + -H "Content-Type: text/plain" \ + -d "test data" \ + "https://hb.cran.dev/post" + +# req_as_curl() works with file bodies + + Code + req_as_curl(req_body_file(request("https://hb.cran.dev/post"), path, type = "text/plain")) + Output + curl -X POST \ + -H "Content-Type: text/plain" \ + --data-binary "@" \ + "https://hb.cran.dev/post" + +# req_as_curl() works with custom content types + + Code + req_as_curl(req_body_json(request("https://hb.cran.dev/post"), list(test = "data"), + type = "application/vnd.api+json")) + Output + curl -X POST \ + -H "Content-Type: application/vnd.api+json" \ + -d '{"test":"data"}' \ + "https://hb.cran.dev/post" + +# req_as_curl() works with options + + Code + req_as_curl(req_options(request("https://hb.cran.dev/get"), timeout = 30, + verbose = TRUE, ssl_verifypeer = FALSE)) + Message + ! Unable to translate option "ssl_verifypeer" + Output + curl --max-time 30 \ + --verbose \ + "https://hb.cran.dev/get" + +# req_as_curl() works with cookies + + Code + req_as_curl(req_options(request("https://hb.cran.dev/cookies"), cookiejar = cookie_file, + cookiefile = cookie_file)) + Output + curl --cookie-jar "" \ + --cookie "" \ + "https://hb.cran.dev/cookies" + +# req_as_curl() works with obfuscated values in headers + + Code + req_as_curl(req_headers(request("https://hb.cran.dev/get"), Authorization = obfuscated( + "ZdYJeG8zwISodg0nu4UxBhs"))) + Output + curl -H "Authorization: ZdYJeG8zwISodg0nu4UxBhs" \ + "https://hb.cran.dev/get" + +# req_as_curl() works with obfuscated values in JSON body + + Code + req_as_curl(req_body_json(request("https://hb.cran.dev/post"), list(username = "test", + password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs")))) + Output + curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"username":"test","password":"y"}' \ + "https://hb.cran.dev/post" + +# req_as_curl() works with obfuscated values in form body + + Code + req_as_curl(req_body_form(request("https://hb.cran.dev/post"), username = "test", + password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs"))) + Output + curl -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=test&password=y" \ + "https://hb.cran.dev/post" + +# req_as_curl() works with complex requests + + Code + req_as_curl(req_options(req_body_json(req_headers(req_method(request( + "https://api.github.com/user/repos"), "POST"), Accept = "application/vnd.github.v3+json", + Authorization = obfuscated("ZdYJeG8zwISodg0nu4UxBhs"), `User-Agent` = "MyApp/1.0"), + list(name = "test-repo", description = "A test repository", private = TRUE)), + timeout = 60)) + Output + curl -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: ZdYJeG8zwISodg0nu4UxBhs" \ + -H "User-Agent: MyApp/1.0" \ + --max-time 60 \ + -H "Content-Type: application/json" \ + -d '{"name":"test-repo","description":"A test repository","private":true}' \ + "https://api.github.com/user/repos" + +# req_as_curl() works with simple requests (single line) + + Code + req_as_curl(request("https://hb.cran.dev/get")) + Output + curl "https://hb.cran.dev/get" + +# req_as_curl() validates input + + Code + req_as_curl("not a request") + Condition + Error in `req_as_curl()`: + ! `req` must be an HTTP request object, not the string "not a request". + diff --git a/tests/testthat/test-httr2-translate.R b/tests/testthat/test-req-as-curl.R similarity index 53% rename from tests/testthat/test-httr2-translate.R rename to tests/testthat/test-req-as-curl.R index 652dd7c1..303cae3c 100644 --- a/tests/testthat/test-httr2-translate.R +++ b/tests/testthat/test-req-as-curl.R @@ -1,62 +1,62 @@ -test_that("httr2_translate() works with basic GET requests", { +test_that("req_as_curl() works with basic GET requests", { expect_snapshot({ - request("https://httpbin.org/get") |> - httr2_translate() + request("https://hb.cran.dev/get") |> + req_as_curl() }) }) -test_that("httr2_translate() works with POST methods", { +test_that("req_as_curl() works with POST methods", { expect_snapshot({ - request("https://httpbin.org/post") |> + request("https://hb.cran.dev/post") |> req_method("POST") |> - httr2_translate() + req_as_curl() }) }) -test_that("httr2_translate() works with headers", { +test_that("req_as_curl() works with headers", { expect_snapshot({ - request("https://httpbin.org/get") |> + request("https://hb.cran.dev/get") |> req_headers( "Accept" = "application/json", "User-Agent" = "httr2/1.0" ) |> - httr2_translate() + req_as_curl() }) }) -test_that("httr2_translate() works with JSON bodies", { +test_that("req_as_curl() works with JSON bodies", { expect_snapshot({ - request("https://httpbin.org/post") |> + request("https://hb.cran.dev/post") |> req_body_json(list(name = "test", value = 123)) |> - httr2_translate() + req_as_curl() }) }) -test_that("httr2_translate() works with form bodies", { +test_that("req_as_curl() works with form bodies", { expect_snapshot({ - request("https://httpbin.org/post") |> + request("https://hb.cran.dev/post") |> req_body_form(name = "test", value = "123") |> - httr2_translate() + req_as_curl() }) }) -test_that("httr2_translate() works with multipart bodies", { +test_that("req_as_curl() works with multipart bodies", { expect_snapshot({ - request("https://httpbin.org/post") |> + request("https://hb.cran.dev/post") |> req_body_multipart(name = "test", value = "123") |> - httr2_translate() + req_as_curl() }) }) -test_that("httr2_translate() works with string bodies", { +test_that("req_as_curl() works with string bodies", { expect_snapshot({ - request("https://httpbin.org/post") |> + request("https://hb.cran.dev/post") |> req_body_raw("test data", type = "text/plain") |> - httr2_translate() + req_as_curl() }) }) -test_that("httr2_translate() works with file bodies", { +test_that("req_as_curl() works with file bodies", { path <- tempfile() writeLines("test content", path) @@ -65,9 +65,9 @@ test_that("httr2_translate() works with file bodies", { expect_snapshot( { - request("https://httpbin.org/post") |> + request("https://hb.cran.dev/post") |> req_body_file(path, type = "text/plain") |> - httr2_translate() + req_as_curl() }, transform = function(x) { gsub(path, "", x, fixed = TRUE) @@ -75,26 +75,26 @@ test_that("httr2_translate() works with file bodies", { ) }) -test_that("httr2_translate() works with custom content types", { +test_that("req_as_curl() works with custom content types", { expect_snapshot({ - request("https://httpbin.org/post") |> + request("https://hb.cran.dev/post") |> req_body_json( list(test = "data"), type = "application/vnd.api+json" ) |> - httr2_translate() + req_as_curl() }) }) -test_that("httr2_translate() works with options", { +test_that("req_as_curl() works with options", { expect_snapshot({ - request("https://httpbin.org/get") |> + request("https://hb.cran.dev/get") |> req_options(timeout = 30, verbose = TRUE, ssl_verifypeer = FALSE) |> - httr2_translate() + req_as_curl() }) }) -test_that("httr2_translate() works with cookies", { +test_that("req_as_curl() works with cookies", { cookie_file <- tempfile() # create the tempfile @@ -105,9 +105,9 @@ test_that("httr2_translate() works with cookies", { expect_snapshot( { - request("https://httpbin.org/cookies") |> + request("https://hb.cran.dev/cookies") |> req_options(cookiejar = cookie_file, cookiefile = cookie_file) |> - httr2_translate() + req_as_curl() }, transform = function(x) { gsub(cookie_file, "", x, fixed = TRUE) @@ -115,37 +115,37 @@ test_that("httr2_translate() works with cookies", { ) }) -test_that("httr2_translate() works with obfuscated values in headers", { +test_that("req_as_curl() works with obfuscated values in headers", { expect_snapshot({ - request("https://httpbin.org/get") |> + request("https://hb.cran.dev/get") |> req_headers("Authorization" = obfuscated("ZdYJeG8zwISodg0nu4UxBhs")) |> - httr2_translate() + req_as_curl() }) }) -test_that("httr2_translate() works with obfuscated values in JSON body", { +test_that("req_as_curl() works with obfuscated values in JSON body", { expect_snapshot({ - request("https://httpbin.org/post") |> + request("https://hb.cran.dev/post") |> req_body_json(list( username = "test", password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs") )) |> - httr2_translate() + req_as_curl() }) }) -test_that("httr2_translate() works with obfuscated values in form body", { +test_that("req_as_curl() works with obfuscated values in form body", { expect_snapshot({ - request("https://httpbin.org/post") |> + request("https://hb.cran.dev/post") |> req_body_form( username = "test", password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs") ) |> - httr2_translate() + req_as_curl() }) }) -test_that("httr2_translate() works with complex requests", { +test_that("req_as_curl() works with complex requests", { expect_snapshot({ request("https://api.github.com/user/repos") |> req_method("POST") |> @@ -160,19 +160,19 @@ test_that("httr2_translate() works with complex requests", { private = TRUE )) |> req_options(timeout = 60) |> - httr2_translate() + req_as_curl() }) }) -test_that("httr2_translate() works with simple requests (single line)", { +test_that("req_as_curl() works with simple requests (single line)", { expect_snapshot({ - request("https://httpbin.org/get") |> - httr2_translate() + request("https://hb.cran.dev/get") |> + req_as_curl() }) }) -test_that("httr2_translate() validates input", { +test_that("req_as_curl() validates input", { expect_snapshot(error = TRUE, { - httr2_translate("not a request") + req_as_curl("not a request") }) })