diff --git a/NAMESPACE b/NAMESPACE index 4c9b1bea..129a96cb 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -147,7 +147,11 @@ export(signal_total_pages) export(throttle_status) export(url_build) export(url_modify) +export(url_modify_query) +export(url_modify_relative) export(url_parse) +export(url_query_build) +export(url_query_parse) export(with_mock) export(with_mocked_responses) export(with_verbosity) diff --git a/NEWS.md b/NEWS.md index 45248b0b..a72ab2d5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,11 +1,12 @@ # httr2 (development version) +* 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). * `req_url_query()` gains the ability to control how spaces are encoded (#432). * New `resp_request()` aids debugging by returning the request associated with a response (#604). * `print.request()` now correctly escapes `{}` in headers (#586). * New `req_headers_redacted()` provides a user-friendlier way to set redacted headers (#561). * `resp_link_url()` now works if there are multiple `Link` headers (#587). -* New `url_modify()` makes it easier to modify an existing url (#464). * New `req_url_relative()` for constructing relative urls (#449). * `url_parse()` gains `base_url` argument so you can also use it to parse relative URLs (#449). * `url_parse()` now uses `curl::curl_parse_url()` which is much faster and more correct (#577). diff --git a/R/oauth-flow-auth-code.R b/R/oauth-flow-auth-code.R index 6aac75f9..7abda743 100644 --- a/R/oauth-flow-auth-code.R +++ b/R/oauth-flow-auth-code.R @@ -365,7 +365,7 @@ oauth_flow_auth_code_listen <- function(redirect_uri = "http://localhost:1410") # https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 # Spaces are first replaced by + parse_form_urlencoded <- function(query) { - query <- query_parse(query) + query <- url_query_parse(query) query[] <- gsub("+", " ", query, fixed = TRUE) query } diff --git a/R/req-auth-aws.R b/R/req-auth-aws.R index 7d32e69f..43f48e8e 100644 --- a/R/req-auth-aws.R +++ b/R/req-auth-aws.R @@ -129,7 +129,7 @@ aws_v4_signature <- function(method, CanonicalQueryString <- "" } else { sorted_query <- url$query[order(names(url$query))] - CanonicalQueryString <- query_build(CanonicalQueryString) + CanonicalQueryString <- url_query_build(CanonicalQueryString) } headers$host <- url$hostname diff --git a/R/req-body.R b/R/req-body.R index 1087e4d4..6f63e675 100644 --- a/R/req-body.R +++ b/R/req-body.R @@ -227,7 +227,7 @@ req_body_get <- function(req) { raw = req$body$data, form = { data <- unobfuscate(req$body$data) - query_build(data) + url_query_build(data) }, json = exec(jsonlite::toJSON, req$body$data, !!!req$body$params), cli::cli_abort("Unsupported request body type {.str {req$body$type}}.") diff --git a/R/req-url.R b/R/req-url.R index 5964532b..c6898903 100644 --- a/R/req-url.R +++ b/R/req-url.R @@ -6,6 +6,9 @@ #' * `req_url_path()` modifies the path #' * `req_url_path_append()` adds to the path #' +#' Alternatively, to modify only a URL without creating a request, +#' you can instead use [url_modify()] and friends. +#' #' @inheritParams req_perform #' @param url New URL; completely replaces existing. #' @param ... For `req_url_query()`: <[`dynamic-dots`][rlang::dyn-dots]> @@ -56,39 +59,19 @@ req_url <- function(req, url) { #' @rdname req_url req_url_relative <- function(req, url) { check_request(req) - - new_url <- url_parse(url, base_url = req$url) - req_url(req, url_build(new_url)) + req_url(req, url_modify_relative(req$url, url)) } #' @export #' @rdname req_url -#' @param .multi Controls what happens when an element of `...` is a vector -#' containing multiple values: -#' -#' * `"error"`, the default, throws an error. -#' * `"comma"`, separates values with a `,`, e.g. `?x=1,2`. -#' * `"pipe"`, separates values with a `|`, e.g. `?x=1|2`. -#' * `"explode"`, turns each element into its own parameter, e.g. `?x=1&x=2` -#' -#' If none of these options work for your needs, you can instead supply a -#' function that takes a character vector of argument values and returns a -#' a single string. -#' @param .space How should spaces in query params be escaped? The default, -#' "percent", uses standard percent encoding (i.e. `%20`), but you can opt-in -#' to "form" encoding, which uses `+` instead. +#' @inheritParams url_modify_query req_url_query <- function(.req, ..., .multi = c("error", "comma", "pipe", "explode"), - .space = c("percent", "form") - ) { + .space = c("percent", "form")) { check_request(.req) - - dots <- multi_dots(..., .multi = .multi, .space = .space) - - url <- url_parse(.req$url) - url$query <- modify_list(url$query, !!!dots) - req_url(.req, url_build(url)) + url <- url_modify_query(.req$url, ..., .multi = .multi, .space = .space) + req_url(.req, url) } #' @export diff --git a/R/url.R b/R/url.R index 0c308f6d..deac3439 100644 --- a/R/url.R +++ b/R/url.R @@ -42,10 +42,15 @@ url_parse <- function(url, base_url = NULL) { #' Modify a URL #' -#' Modify components of a URL. The default value of each argument, `NULL`, -#' means leave the component as is. If you want to remove a component, -#' set it to `""`. Note that setting `scheme` or `hostname` to `""` will -#' create a relative URL. +#' @description +#' Use `url_modify()` to modify any component of the URL, +#' `url_modify_relative()` to modify with a relative URL, +#' or `url_modify_query()` to modify individual query parameters. +#' +#' For `url_modify()`, components that aren't specified in the +#' function call will be left as is; components set to `NULL` will be removed, +#' and all other values will be updated. Note that removing `scheme` or +#' `hostname` will create a relative URL. #' #' @param url A string or [parsed URL](url_parse). #' @param scheme The scheme, typically either `http` or `https`. @@ -67,16 +72,23 @@ url_parse <- function(url, base_url = NULL) { #' url_modify("http://hadley.nz/abc", path = "") #' url_modify("http://hadley.nz?a=1", query = "b=2") #' url_modify("http://hadley.nz?a=1", query = list(c = 3)) +#' +#' url_modify_query("http://hadley.nz?a=1&b=2", c = 3) +#' url_modify_query("http://hadley.nz?a=1&b=2", b = NULL) +#' url_modify_query("http://hadley.nz?a=1&b=2", a = 100) +#' +#' url_modify_relative("http://hadley.nz/a/b/c.html", "/d.html") +#' url_modify_relative("http://hadley.nz/a/b/c.html", "d.html") +#' url_modify_relative("http://hadley.nz/a/b/c.html", "../d.html") url_modify <- function(url, - scheme = NULL, - hostname = NULL, - username = NULL, - password = NULL, - port = NULL, - path = NULL, - query = NULL, - fragment = NULL) { - + scheme = as_is, + hostname = as_is, + username = as_is, + password = as_is, + port = as_is, + path = as_is, + query = as_is, + fragment = as_is) { if (!is_string(url) && !is_url(url)) { stop_input_type(url, "a string or parsed URL") } @@ -85,25 +97,25 @@ url_modify <- function(url, url <- url_parse(url) } - check_string(scheme, allow_null = TRUE) - check_string(hostname, allow_null = TRUE) - check_string(username, allow_null = TRUE) - check_string(password, allow_null = TRUE) - check_number_whole(port, min = 1, allow_null = TRUE) - check_string(path, allow_null = TRUE) - check_string(fragment, allow_null = TRUE) + if (!leave_as_is(scheme)) check_string(scheme, allow_null = TRUE) + if (!leave_as_is(hostname)) check_string(hostname, allow_null = TRUE) + if (!leave_as_is(username)) check_string(username, allow_null = TRUE) + if (!leave_as_is(password)) check_string(password, allow_null = TRUE) + if (!leave_as_is(port)) check_number_whole(port, min = 1, allow_null = TRUE) + if (!leave_as_is(path)) check_string(path, allow_null = TRUE) + if (!leave_as_is(fragment)) check_string(fragment, allow_null = TRUE) if (is_string(query)) { - query <- query_parse(query) - } else if (is.list(query) && (is_named(query) || length(query) == 0)) { + query <- url_query_parse(query) + } else if (is_named_list(query)) { for (nm in names(query)) { check_query_param(query[[nm]], paste0("query$", nm)) } - } else if (!is.null(query)) { + } else if (!is.null(query) && !leave_as_is(query)) { stop_input_type(query, "a character vector, named list, or NULL") } - new <- compact(list( + new <- list( scheme = scheme, hostname = hostname, username = username, @@ -112,9 +124,8 @@ url_modify <- function(url, path = path, query = query, fragment = fragment - )) - is_empty <- map_lgl(new, identical, "") - new[is_empty] <- list(NULL) + ) + new <- new[!map_lgl(new, leave_as_is)] url[names(new)] <- new if (string_url) { @@ -124,6 +135,71 @@ url_modify <- function(url, } } +as_is <- quote(as_is) +leave_as_is <- function(x) identical(x, as_is) + +#' @export +#' @rdname url_modify +#' @param relative_url A relative URL to append to the base URL. +url_modify_relative <- function(url, relative_url) { + string_url <- is_string(url) + if (!string_url) { + url <- url_build(url) + } + + new_url <- url_parse(relative_url, base_url = url) + + if (string_url) { + url_build(new_url) + } else { + new_url + } +} + +#' @export +#' @rdname url_modify +#' @param ... <[`dynamic-dots`][rlang::dyn-dots]> +#' Name-value pairs that define query parameters. Each value must be either +#' an atomic vector or `NULL` (which removes the corresponding parameters). +#' If you want to opt out of escaping, wrap strings in `I()`. +#' @param .multi Controls what happens when a value is a vector: +#' +#' * `"error"`, the default, throws an error. +#' * `"comma"`, separates values with a `,`, e.g. `?x=1,2`. +#' * `"pipe"`, separates values with a `|`, e.g. `?x=1|2`. +#' * `"explode"`, turns each element into its own parameter, e.g. `?x=1&x=2` +#' +#' If none of these options work for your needs, you can instead supply a +#' function that takes a character vector of argument values and returns a +#' a single string. +#' @param .space How should spaces in query params be escaped? The default, +#' "percent", uses standard percent encoding (i.e. `%20`), but you can opt-in +#' to "form" encoding, which uses `+` instead. +url_modify_query <- function( + url, + ..., + .multi = c("error", "comma", "pipe", "explode"), + .space = c("percent", "form")) { + if (!is_string(url) && !is_url(url)) { + stop_input_type(url, "a string or parsed URL") + } + string_url <- is_string(url) + if (string_url) { + url <- url_parse(url) + } + + new_query <- multi_dots(..., .multi = .multi, .space = .space) + if (length(new_query) > 0) { + url$query <- modify_list(url$query, !!!new_query) + } + + if (string_url) { + url_build(url) + } else { + url + } +} + is_url <- function(x) inherits(x, "httr2_url") #' @export @@ -175,7 +251,7 @@ url_build <- function(url) { } if (!is.null(url$query)) { - query <- query_build(url$query) + query <- url_query_build(url$query) } else { query <- NULL } @@ -213,9 +289,23 @@ url_build <- function(url) { ) } -query_parse <- function(x) { - x <- gsub("^\\?", "", x) # strip leading ?, if present - params <- parse_name_equals_value(parse_delim(x, "&")) +#' Parse query parameters and/or build a string +#' +#' `url_query_parse()` parses a query string into a named list; +#' `url_query_build()` builds a query string from a named list. +#' +#' @param query A string, when parsing; a named list when building. +#' @export +#' @examples +#' str(url_query_parse("a=1&b=2")) +#' +#' url_query_build(list(x = 1, y = "z")) +#' url_query_build(list(x = 1, y = 1:2), .multi = "explode") +url_query_parse <- function(query) { + check_string(query) + + query <- gsub("^\\?", "", query) # strip leading ?, if present + params <- parse_name_equals_value(parse_delim(query, "&")) if (length(params) == 0) { return(NULL) @@ -226,12 +316,20 @@ query_parse <- function(x) { out } -query_build <- function(x, error_call = caller_env()) { - elements_build(x, "Query", "&", error_call = error_call) +#' @export +#' @rdname url_query_parse +#' @inheritParams url_modify_query +url_query_build <- function(query, .multi = c("error", "comma", "pipe", "explode")) { + if (!is_named_list(query)) { + stop_input_type(query, "a named list") + } + + query <- multi_dots(!!!query, .multi = .multi, error_arg = "query") + elements_build(query, "Query", "&") } elements_build <- function(x, name, collapse, error_call = caller_env()) { - if (!is_list(x) || (!is_named(x) && length(x) > 0)) { + if (!is_named_list(x)) { cli::cli_abort("{name} must be a named list.", call = error_call) } diff --git a/R/utils.R b/R/utils.R index e65421d5..f57677f1 100644 --- a/R/utils.R +++ b/R/utils.R @@ -34,6 +34,8 @@ modify_list <- function(.x, ..., error_call = caller_env()) { ) } + + out <- .x[!names(.x) %in% names(dots)] out <- c(out, compact(dots)) @@ -326,3 +328,7 @@ slice <- function(vector, start = 1, end = length(vector) + 1) { vector[start:(end - 1)] } } + +is_named_list <- function(x) { + is_list(x) && (is_named(x) || length(x) == 0) +} diff --git a/man/req_body.Rd b/man/req_body.Rd index 6517608a..67224a88 100644 --- a/man/req_body.Rd +++ b/man/req_body.Rd @@ -64,8 +64,7 @@ and lists. \code{req_body_json()} uses this argument differently; it takes additional arguments passed on to \code{\link[jsonlite:fromJSON]{jsonlite::toJSON()}}.} -\item{.multi}{Controls what happens when an element of \code{...} is a vector -containing multiple values: +\item{.multi}{Controls what happens when a value is a vector: \itemize{ \item \code{"error"}, the default, throws an error. \item \code{"comma"}, separates values with a \verb{,}, e.g. \verb{?x=1,2}. diff --git a/man/req_url.Rd b/man/req_url.Rd index a213a4ff..9f5a50a7 100644 --- a/man/req_url.Rd +++ b/man/req_url.Rd @@ -36,8 +36,7 @@ If you want to opt out of escaping, wrap strings in \code{I()}. For \code{req_url_path()} and \code{req_url_path_append()}: A sequence of path components that will be combined with \code{/}.} -\item{.multi}{Controls what happens when an element of \code{...} is a vector -containing multiple values: +\item{.multi}{Controls what happens when a value is a vector: \itemize{ \item \code{"error"}, the default, throws an error. \item \code{"comma"}, separates values with a \verb{,}, e.g. \verb{?x=1,2}. @@ -63,6 +62,9 @@ A modified HTTP \link{request}. \item \code{req_url_path()} modifies the path \item \code{req_url_path_append()} adds to the path } + +Alternatively, to modify only a URL without creating a request, +you can instead use \code{\link[=url_modify]{url_modify()}} and friends. } \examples{ req <- request("http://example.com") diff --git a/man/url_modify.Rd b/man/url_modify.Rd index 2f19d1d7..9b00c8d0 100644 --- a/man/url_modify.Rd +++ b/man/url_modify.Rd @@ -2,18 +2,29 @@ % Please edit documentation in R/url.R \name{url_modify} \alias{url_modify} +\alias{url_modify_relative} +\alias{url_modify_query} \title{Modify a URL} \usage{ url_modify( url, - scheme = NULL, - hostname = NULL, - username = NULL, - password = NULL, - port = NULL, - path = NULL, - query = NULL, - fragment = NULL + scheme = as_is, + hostname = as_is, + username = as_is, + password = as_is, + port = as_is, + path = as_is, + query = as_is, + fragment = as_is +) + +url_modify_relative(url, relative_url) + +url_modify_query( + url, + ..., + .multi = c("error", "comma", "pipe", "explode"), + .space = c("percent", "form") ) } \arguments{ @@ -34,15 +45,42 @@ will be automatically added if omitted.} \item{query}{Either a query string or a named list of query components.} \item{fragment}{The fragment, e.g., \verb{#section-1}.} + +\item{relative_url}{A relative URL to append to the base URL.} + +\item{...}{<\code{\link[rlang:dyn-dots]{dynamic-dots}}> +Name-value pairs that define query parameters. Each value must be either +an atomic vector or \code{NULL} (which removes the corresponding parameters). +If you want to opt out of escaping, wrap strings in \code{I()}.} + +\item{.multi}{Controls what happens when a value is a vector: +\itemize{ +\item \code{"error"}, the default, throws an error. +\item \code{"comma"}, separates values with a \verb{,}, e.g. \verb{?x=1,2}. +\item \code{"pipe"}, separates values with a \code{|}, e.g. \code{?x=1|2}. +\item \code{"explode"}, turns each element into its own parameter, e.g. \code{?x=1&x=2} +} + +If none of these options work for your needs, you can instead supply a +function that takes a character vector of argument values and returns a +a single string.} + +\item{.space}{How should spaces in query params be escaped? The default, +"percent", uses standard percent encoding (i.e. \verb{\%20}), but you can opt-in +to "form" encoding, which uses \code{+} instead.} } \value{ An object of the same type as \code{url}. } \description{ -Modify components of a URL. The default value of each argument, \code{NULL}, -means leave the component as is. If you want to remove a component, -set it to \code{""}. Note that setting \code{scheme} or \code{hostname} to \code{""} will -create a relative URL. +Use \code{url_modify()} to modify any component of the URL, +\code{url_modify_relative()} to modify with a relative URL, +or \code{url_modify_query()} to modify individual query parameters. + +For \code{url_modify()}, components that aren't specified in the +function call will be left as is; components set to \code{NULL} will be removed, +and all other values will be updated. Note that removing \code{scheme} or +\code{hostname} will create a relative URL. } \examples{ url_modify("http://hadley.nz", path = "about") @@ -51,6 +89,14 @@ url_modify("http://hadley.nz/abc", path = "/cde") url_modify("http://hadley.nz/abc", path = "") url_modify("http://hadley.nz?a=1", query = "b=2") url_modify("http://hadley.nz?a=1", query = list(c = 3)) + +url_modify_query("http://hadley.nz?a=1&b=2", c = 3) +url_modify_query("http://hadley.nz?a=1&b=2", b = NULL) +url_modify_query("http://hadley.nz?a=1&b=2", a = 100) + +url_modify_relative("http://hadley.nz/a/b/c.html", "/d.html") +url_modify_relative("http://hadley.nz/a/b/c.html", "d.html") +url_modify_relative("http://hadley.nz/a/b/c.html", "../d.html") } \seealso{ Other URL manipulation: diff --git a/man/url_query_parse.Rd b/man/url_query_parse.Rd new file mode 100644 index 00000000..14e248e3 --- /dev/null +++ b/man/url_query_parse.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/url.R +\name{url_query_parse} +\alias{url_query_parse} +\alias{url_query_build} +\title{Parse query parameters and/or build a string} +\usage{ +url_query_parse(query) + +url_query_build(query, .multi = c("error", "comma", "pipe", "explode")) +} +\arguments{ +\item{query}{A string, when parsing; a named list when building.} + +\item{.multi}{Controls what happens when a value is a vector: +\itemize{ +\item \code{"error"}, the default, throws an error. +\item \code{"comma"}, separates values with a \verb{,}, e.g. \verb{?x=1,2}. +\item \code{"pipe"}, separates values with a \code{|}, e.g. \code{?x=1|2}. +\item \code{"explode"}, turns each element into its own parameter, e.g. \code{?x=1&x=2} +} + +If none of these options work for your needs, you can instead supply a +function that takes a character vector of argument values and returns a +a single string.} +} +\description{ +\code{url_query_parse()} parses a query string into a named list; +\code{url_query_build()} builds a query string from a named list. +} +\examples{ +str(url_query_parse("a=1&b=2")) + +url_query_build(list(x = 1, y = "z")) +url_query_build(list(x = 1, y = 1:2), .multi = "explode") +} diff --git a/tests/testthat/_snaps/req-url.md b/tests/testthat/_snaps/req-url.md index 90c05484..b67f376b 100644 --- a/tests/testthat/_snaps/req-url.md +++ b/tests/testthat/_snaps/req-url.md @@ -11,7 +11,7 @@ Code req_url_query_multi("error") Condition - Error in `req_url_query()`: + Error in `url_modify_query()`: ! All vector elements of `...` must be length 1. i Use `.multi` to choose a strategy for handling vectors. @@ -20,22 +20,22 @@ Code req %>% req_url_query(1) Condition - Error in `req_url_query()`: + Error in `url_modify_query()`: ! All components of `...` must be named. Code req %>% req_url_query(a = I(1)) Condition - Error in `req_url_query()`: + Error in `url_modify_query()`: ! Escaped query value `a` must be a single string, not the number 1. Code req %>% req_url_query(a = 1:2) Condition - Error in `req_url_query()`: + Error in `url_modify_query()`: ! All vector elements of `...` must be length 1. i Use `.multi` to choose a strategy for handling vectors. Code req %>% req_url_query(a = mean) Condition - Error in `req_url_query()`: + Error in `url_modify_query()`: ! All elements of `...` must be either an atomic vector or NULL. diff --git a/tests/testthat/_snaps/url.md b/tests/testthat/_snaps/url.md index 36e5c565..676102ff 100644 --- a/tests/testthat/_snaps/url.md +++ b/tests/testthat/_snaps/url.md @@ -96,15 +96,35 @@ # validates inputs Code - query_build(1:3) + url_modify_query(1) Condition - Error: - ! Query must be a named list. + Error in `url_modify_query()`: + ! `url` must be a string or parsed URL, not the number 1. Code - query_build(list(x = 1:2, y = 1:3)) + url_modify_query(url, 1) Condition - Error: - ! Query value `x` must be a length-1 atomic vector, not an integer vector. + Error in `url_modify_query()`: + ! All components of `...` must be named. + Code + url_modify_query(url, x = 1:2) + Condition + Error in `url_modify_query()`: + ! All vector elements of `...` must be length 1. + i Use `.multi` to choose a strategy for handling vectors. + +--- + + Code + url_query_build(1:3) + Condition + Error in `url_query_build()`: + ! `query` must be a named list, not an integer vector. + Code + url_query_build(list(x = 1:2, y = 1:3)) + Condition + Error in `url_query_build()`: + ! All vector elements of `query` must be length 1. + i Use `.multi` to choose a strategy for handling vectors. # can't opt out of escaping non strings diff --git a/tests/testthat/test-url.R b/tests/testthat/test-url.R index b99d7224..4cc97b99 100644 --- a/tests/testthat/test-url.R +++ b/tests/testthat/test-url.R @@ -91,29 +91,98 @@ test_that("checks various query formats", { test_that("path always starts with /", { expect_equal(url_modify("https://x.com/abc", path = "def"), "https://x.com/def") expect_equal(url_modify("https://x.com/abc", path = ""), "https://x.com/") + expect_equal(url_modify("https://x.com/abc", path = NULL), "https://x.com/") }) +# relative url ------------------------------------------------------------ + +test_that("can set relative urls", { + base <- "http://example.com/a/b/c/" + expect_equal(url_modify_relative(base, "d"), "http://example.com/a/b/c/d") + expect_equal(url_modify_relative(base, ".."), "http://example.com/a/b/") + expect_equal(url_modify_relative(base, "//archive.org"), "http://archive.org/") +}) + +test_that("is idempotent", { + string <- "http://example.com/" + url <- url_parse(string) + + expect_equal(url_modify_relative(string, "."), string) + expect_equal(url_modify_relative(url, "."), url) +}) + +# modify query ------------------------------------------------------------- + +test_that("can add, modify, and delete query components", { + expect_equal( + url_modify_query("http://test/path", new = "value"), + "http://test/path?new=value" + ) + expect_equal( + url_modify_query("http://test/path", new = "one", new = "two"), + "http://test/path?new=one&new=two" + ) + expect_equal( + url_modify_query("http://test/path?a=old&b=old", a = "new"), + "http://test/path?b=old&a=new" + ) + expect_equal( + url_modify_query("http://test/path?remove=me&keep=this", remove = NULL), + "http://test/path?keep=this" + ) +}) + +test_that("can control space formatting", { + expect_equal( + url_modify_query("http://test/path", new = "a b"), + "http://test/path?new=a%20b" + ) + expect_equal( + url_modify_query("http://test/path", new = "a b", .space = "form"), + "http://test/path?new=a+b" + ) +}) + +test_that("is idempotent", { + string <- "http://example.com/" + url <- url_parse(string) + + expect_equal(url_modify_query(string), string) + expect_equal(url_modify_query(url), url) +}) + +test_that("validates inputs", { + url <- "http://example.com/" + + expect_snapshot(error = TRUE, { + url_modify_query(1) + url_modify_query(url, 1) + url_modify_query(url, x = 1:2) + }) +}) + + # query ------------------------------------------------------------------- test_that("missing query values become empty strings", { - expect_equal(query_parse("?q="), list(q = "")) - expect_equal(query_parse("?q"), list(q = "")) - expect_equal(query_parse("?a&q"), list(a = "", q = "")) + expect_equal(url_query_parse("?q="), list(q = "")) + expect_equal(url_query_parse("?q"), list(q = "")) + expect_equal(url_query_parse("?a&q"), list(a = "", q = "")) }) test_that("handles equals in values", { - expect_equal(query_parse("?x==&y=="), list(x = "=", y = "=")) - }) + expect_equal(url_query_parse("?x==&y=="), list(x = "=", y = "=")) +}) test_that("empty queries become NULL", { - expect_equal(query_parse("?"), NULL) - expect_equal(query_parse(""), NULL) + expect_equal(url_query_parse("?"), NULL) + expect_equal(url_query_parse(""), NULL) }) test_that("validates inputs", { expect_snapshot(error = TRUE, { - query_build(1:3) - query_build(list(x = 1:2, y = 1:3)) + url_query_build(1:3) + url_query_build(list(x = 1:2, y = 1:3)) }) })