From c086015e9d636b4b157a32a291670e304a8f3376 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Mon, 23 Dec 2024 07:30:13 -0600 Subject: [PATCH] Implement user agent functionality (#34) * RStudio now gives project IDs. * Better user agent management. Better file organization. Closes #29. --- DESCRIPTION | 6 +- NAMESPACE | 4 +- R/aaa-shared.R | 63 ++++++- R/call.R | 64 ------- R/call_api.R | 62 +++++++ R/nectar-package.R | 1 + R/req.R | 101 ----------- R/{auth.R => req_auth_api_key.R} | 27 ++- R/req_body.R | 65 ------- R/req_init.R | 25 +++ R/req_method.R | 14 -- R/req_modify.R | 126 ++++++++++++++ R/req_path.R | 19 --- R/{perform.R => req_perform_opinionated.R} | 0 R/req_pkg_user_agent.R | 158 ++++++++++++++++++ R/req_prepare.R | 38 +++++ R/req_query.R | 15 -- R/{resp.R => resp_parse.R} | 14 +- R/utils.R | 33 +++- man/call_api.Rd | 85 +++++----- man/do_if_fn_defined.Rd | 8 +- man/dot-get_pkg_version.Rd | 25 +++ man/dot-httr2_default_user_agent.Rd | 15 ++ man/dot-lib_user_agent_append.Rd | 39 +++++ man/dot-lib_user_agent_string.Rd | 29 ++++ man/dot-nectar_user_agent_append.Rd | 31 ++++ man/dot-pkg_user_agent_append.Rd | 38 +++++ man/dot-req_auth_api_key_cookie.Rd | 11 +- man/dot-req_auth_api_key_header.Rd | 10 +- man/dot-req_auth_api_key_query.Rd | 10 +- man/dot-req_body_auto.Rd | 15 +- man/dot-req_method_apply.Rd | 8 +- man/dot-req_path_append.Rd | 12 +- man/dot-req_query_flatten.Rd | 12 +- man/dot-shared-parameters.Rd | 14 -- man/dot-shared-params.Rd | 94 +++++++++++ man/dot-user_agent_remove.Rd | 37 ++++ man/figures/lifecycle-deprecated.svg | 21 +++ man/figures/lifecycle-experimental.svg | 21 +++ man/figures/lifecycle-stable.svg | 29 ++++ man/figures/lifecycle-superseded.svg | 21 +++ man/get_pkg_name.Rd | 24 +++ man/req_auth_api_key.Rd | 14 +- man/req_init.Rd | 35 ++++ man/req_modify.Rd | 46 +++-- man/req_perform_opinionated.Rd | 2 +- man/req_pkg_user_agent.Rd | 41 +++++ man/req_prepare.Rd | 60 ++++--- man/req_setup.Rd | 34 ---- man/resp_parse.Rd | 28 ++-- man/stabilize_string.Rd | 2 +- nectar.Rproj | 1 + principles.md | 2 +- .../_snaps/{req_body.md => req_prepare.md} | 2 +- .../_snaps/{resp.md => resp_parse.md} | 0 tests/testthat/{ => fixtures}/img-test.png | Bin .../testthat/{test-call.R => test-call_api.R} | 13 +- tests/testthat/test-req.R | 58 ------- .../{test-auth.R => test-req_auth_api_key.R} | 0 tests/testthat/test-req_body.R | 29 ---- ...rform.R => test-req_perform_opinionated.R} | 0 tests/testthat/test-req_pkg_user_agent.R | 45 +++++ tests/testthat/test-req_prepare.R | 124 ++++++++++++++ tests/testthat/test-req_query.R | 45 ----- .../{test-resp.R => test-resp_parse.R} | 0 65 files changed, 1369 insertions(+), 656 deletions(-) delete mode 100644 R/call.R create mode 100644 R/call_api.R delete mode 100644 R/req.R rename R/{auth.R => req_auth_api_key.R} (69%) delete mode 100644 R/req_body.R create mode 100644 R/req_init.R delete mode 100644 R/req_method.R create mode 100644 R/req_modify.R delete mode 100644 R/req_path.R rename R/{perform.R => req_perform_opinionated.R} (100%) create mode 100644 R/req_pkg_user_agent.R create mode 100644 R/req_prepare.R delete mode 100644 R/req_query.R rename R/{resp.R => resp_parse.R} (74%) create mode 100644 man/dot-get_pkg_version.Rd create mode 100644 man/dot-httr2_default_user_agent.Rd create mode 100644 man/dot-lib_user_agent_append.Rd create mode 100644 man/dot-lib_user_agent_string.Rd create mode 100644 man/dot-nectar_user_agent_append.Rd create mode 100644 man/dot-pkg_user_agent_append.Rd delete mode 100644 man/dot-shared-parameters.Rd create mode 100644 man/dot-shared-params.Rd create mode 100644 man/dot-user_agent_remove.Rd create mode 100644 man/figures/lifecycle-deprecated.svg create mode 100644 man/figures/lifecycle-experimental.svg create mode 100644 man/figures/lifecycle-stable.svg create mode 100644 man/figures/lifecycle-superseded.svg create mode 100644 man/get_pkg_name.Rd create mode 100644 man/req_init.Rd create mode 100644 man/req_pkg_user_agent.Rd delete mode 100644 man/req_setup.Rd rename tests/testthat/_snaps/{req_body.md => req_prepare.md} (86%) rename tests/testthat/_snaps/{resp.md => resp_parse.md} (100%) rename tests/testthat/{ => fixtures}/img-test.png (100%) rename tests/testthat/{test-call.R => test-call_api.R} (79%) delete mode 100644 tests/testthat/test-req.R rename tests/testthat/{test-auth.R => test-req_auth_api_key.R} (100%) rename tests/testthat/{test-perform.R => test-req_perform_opinionated.R} (100%) create mode 100644 tests/testthat/test-req_pkg_user_agent.R create mode 100644 tests/testthat/test-req_prepare.R delete mode 100644 tests/testthat/test-req_query.R rename tests/testthat/{test-resp.R => test-resp_parse.R} (100%) diff --git a/DESCRIPTION b/DESCRIPTION index 61bc0b4..218a6e0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -20,16 +20,20 @@ Imports: glue, httr2 (>= 1.0.0), jsonlite, + lifecycle, purrr, rlang, stbl, + stringr, + utils, vctrs Suggests: covr, + stringi, testthat (>= 3.0.0) Remotes: jonthegeek/stbl Config/testthat/edition: 3 Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.2 diff --git a/NAMESPACE b/NAMESPACE index 91cbea0..b5a88a7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,10 +9,11 @@ export(call_api) export(compact_nested_list) export(do_if_fn_defined) export(req_auth_api_key) +export(req_init) export(req_modify) export(req_perform_opinionated) +export(req_pkg_user_agent) export(req_prepare) -export(req_setup) export(resp_parse) export(stabilize_string) export(url_normalize) @@ -21,4 +22,5 @@ importFrom(cli,cli_abort) importFrom(fs,path) importFrom(httr2,req_perform) importFrom(httr2,req_perform_iterative) +importFrom(lifecycle,deprecated) importFrom(rlang,":=") diff --git a/R/aaa-shared.R b/R/aaa-shared.R index 9206d9b..64fa605 100644 --- a/R/aaa-shared.R +++ b/R/aaa-shared.R @@ -2,10 +2,67 @@ #' #' Reused parameter definitions are gathered here for easier editing. #' -#' @param req A [httr2::request()] object. -#' @param x The object to update. +#' @param additional_user_agent (`length-1 character`) A string to identify +#' where a request is coming from. We automatically include information about +#' your package and nectar, but use this to provide additional details. +#' Default `NULL`. +#' @param api_key (`length-1 character`) The API key to use. +#' @param arg (`length-1 character`) An argument name as a string. This argument +#' will be mentioned in error messages as the input that is at the origin of a +#' problem. +#' @param auth_args (`list`) An optional list of arguments to the `auth_fn` +#' function. +#' @param auth_fn (`function`) A function to use to authenticate the request. By +#' default (`NULL`), no authentication is performed. +#' @param base_url (`length-1 character`) The part of the url that is shared by +#' all calls to the API. In some cases there may be a family of base URLs, +#' from which you will need to choose one. +#' @param body (multiple types) An object to use as the body of the request. If +#' any component of the body is a path, pass it through [fs::path()] or +#' otherwise give it the class "fs_path" to indicate that it is a path. +#' @param call (`environment`) The environment from which a function was +#' called, e.g. [rlang::caller_env()] (the default). The environment will be +#' mentioned in error messages as the source of the error. This argument is +#' particularly useful for functions that are intended to be called as +#' utilities inside other functions. +#' @param existing_user_agent (`length-1 character`, optional) An existing user +#' agent, such as the value of `req$options$useragent` in a [httr2::request()] +#' object. +#' @param method (`length-1 character`, optional) If the method is something +#' other than `GET` or `POST`, supply it. Case is ignored. +#' @param mime_type (`length-1 character`) The mime type of any files present in +#' the body. Some APIs allow you to leave this as NULL for them to guess. +#' @param name (`length-1 character`) The name of a package or other thing to +#' add to or remove from the user agent string. +#' @param pkg_name (`length-1 character`) The name of the calling package. This +#' will usually be automatically determined based on the source of the call. +#' @param pkg_url (`length-1 character`) A url for information about the calling +#' package (default `NULL`). +#' @param parameter_name (`length-1 character`) The name to use for the API key. +#' @param path (`character` or `list`) The route to an API endpoint. Optionally, +#' a list or character vector with the path as one or more unnamed arguments +#' (which will be concatenated with "/") plus named arguments to +#' [glue::glue()] into the path. +#' @param query (`character` or `list`) An optional list or character vector of +#' parameters to pass in the query portion of the request. Can also include a +#' `.multi` argument to pass to [httr2::req_url_query()] to control how +#' elements containing multiple values are handled. +#' @param req (`httr2_request`) A [httr2::request()] object. +#' @param resp (`httr2_response` or `list`) A single [httr2::response()] object +#' (as returned by [httr2::req_perform()]) or a list of such objects (as +#' returned by [httr2::req_perform_iterative()]). +#' @param response_parser (`function`) A function to parse the server response +#' (`resp`). Defaults to [httr2::resp_body_json()], since JSON responses are +#' common. Set this to `NULL` to return the raw response from +#' [httr2::req_perform()]. +#' @param response_parser_args (`list`) An optional list of arguments to pass to +#' the `response_parser` function (in addition to `resp`). +#' @param url (`length-1 character`) An optional url associated with `name`. +#' @param version (`length-1 character`) The version of `name`. +#' @param x (multiple types) The object to update. +#' @param ... These dots are for future extensions and must be empty. #' -#' @name .shared-parameters +#' @name .shared-params #' @keywords internal NULL diff --git a/R/call.R b/R/call.R deleted file mode 100644 index ff965b2..0000000 --- a/R/call.R +++ /dev/null @@ -1,64 +0,0 @@ -#' Send a request to an API -#' -#' This function implements an opinionated framework for making API calls. It is -#' intended to be used inside an API client package. It serves as a wrapper -#' around the `req_` family of functions, such as [httr2::request()], as well as -#' [httr2::req_perform()] and [httr2::req_perform_iterative()], and, by default, -#' [httr2::resp_body_json()]. -#' -#' @seealso [req_setup()], [req_modify()], [req_perform_opinionated()], -#' [resp_parse()], and [do_if_fn_defined()] for finer control of the process. -#' -#' @inheritParams rlang::args_dots_empty -#' @inheritParams req_setup -#' @inheritParams req_modify -#' @inheritParams req_perform_opinionated -#' @inheritParams resp_parse -#' @param security_fn A function to use to authenticate the request. By default -#' (`NULL`), no authentication is performed. -#' @param security_args An optional list of arguments to the `security_fn` -#' function. -#' @param response_parser_args An optional list of arguments to pass to the -#' `response_parser` function (in addition to `resp`). -#' -#' @return The response from the API, parsed by the `response_parser`. -#' @export -call_api <- function(base_url, - ..., - path = NULL, - query = NULL, - body = NULL, - mime_type = NULL, - method = NULL, - security_fn = NULL, - security_args = list(), - response_parser = httr2::resp_body_json, - response_parser_args = list(), - next_req = NULL, - max_reqs = Inf, - max_tries_per_req = 3, - user_agent = "nectar (https://nectar.api2r.org)") { - rlang::check_dots_empty() - req <- req_setup(base_url, user_agent = user_agent) - req <- req_modify( - req, - path = path, - query = query, - body = body, - mime_type = mime_type, - method = method - ) - req <- do_if_fn_defined(req, security_fn, !!!security_args) - resp <- req_perform_opinionated( - req, - next_req = next_req, - max_reqs = max_reqs, - max_tries_per_req = max_tries_per_req - ) - resp <- resp_parse( - resp, - response_parser = response_parser, - !!!response_parser_args - ) - return(resp) -} diff --git a/R/call_api.R b/R/call_api.R new file mode 100644 index 0000000..1779cba --- /dev/null +++ b/R/call_api.R @@ -0,0 +1,62 @@ +#' Send a request to an API +#' +#' @description `r lifecycle::badge("questioning")` +#' +#' This function implements an opinionated framework for making API calls. It +#' is intended to be used inside an API client package. It serves as a wrapper +#' around the `req_` family of functions, such as [httr2::request()], as well +#' as [httr2::req_perform()] and [httr2::req_perform_iterative()], and, by +#' default, [httr2::resp_body_json()]. +#' +#' @seealso [req_prepare()], [req_perform_opinionated()], and [resp_parse()] for +#' finer control of the process. +#' +#' @inheritParams rlang::args_dots_empty +#' @inheritParams req_prepare +#' @inheritParams req_perform_opinionated +#' @inheritParams resp_parse +#' @param response_parser_args (`list`) Additional arguments to pass to the +#' `response_parser`. +#' +#' @return The response from the API, parsed by the `response_parser`. +#' @export +call_api <- function(base_url, + ..., + path = NULL, + query = NULL, + body = NULL, + mime_type = NULL, + method = NULL, + auth_fn = NULL, + auth_args = list(), + response_parser = httr2::resp_body_json, + response_parser_args = list(), + next_req = NULL, + max_reqs = Inf, + max_tries_per_req = 3, + additional_user_agent = NULL) { + rlang::check_dots_empty() + req <- req_prepare( + base_url, + path = path, + query = query, + body = body, + mime_type = mime_type, + method = method, + additional_user_agent = additional_user_agent, + auth_fn = auth_fn, + auth_args = auth_args + ) + resp <- req_perform_opinionated( + req, + next_req = next_req, + max_reqs = max_reqs, + max_tries_per_req = max_tries_per_req + ) + resp <- resp_parse( + resp, + response_parser = response_parser, + !!!response_parser_args + ) + return(resp) +} diff --git a/R/nectar-package.R b/R/nectar-package.R index 0158528..7c7e26c 100644 --- a/R/nectar-package.R +++ b/R/nectar-package.R @@ -6,6 +6,7 @@ #' @importFrom fs path #' @importFrom httr2 req_perform #' @importFrom httr2 req_perform_iterative +#' @importFrom lifecycle deprecated #' @importFrom rlang := ## usethis namespace: end NULL diff --git a/R/req.R b/R/req.R deleted file mode 100644 index b61d2b3..0000000 --- a/R/req.R +++ /dev/null @@ -1,101 +0,0 @@ -#' Setup a basic API request -#' -#' For a given API, the `base_url` and `user_agent` will almost always be the -#' same. Use this function to prepare that piece of the request once for easy -#' reuse. -#' -#' @inheritParams rlang::args_dots_empty -#' @param base_url The part of the url that is shared by all calls to the API. -#' In some cases there may be a family of base URLs, from which you will need -#' to choose one. -#' @param user_agent A string to identify where this request is coming from. -#' It's polite to set the user agent to identify your package, such as -#' "MyPackage (https://mypackage.com)". -#' -#' @inherit .shared-request return -#' @export -#' -#' @examples -#' req_setup("https://example.com") -#' req_setup( -#' "https://example.com", -#' user_agent = "my_api_client (https://my.api.client)" -#' ) -req_setup <- function(base_url, - ..., - user_agent = "nectar (https://nectar.api2r.org)") { - req <- httr2::request(base_url) - req <- httr2::req_user_agent(req, user_agent) - return(req) -} - -#' Modify an API request for a particular endpoint -#' -#' Modify the basic request for an API by adding a path and any other -#' path-specific properties. -#' -#' @inheritParams rlang::args_dots_empty -#' @inheritParams .req_path_append -#' @inheritParams .req_body_auto -#' @inheritParams .req_method_apply -#' @inheritParams .req_query_flatten -#' -#' @inherit .shared-request return -#' @export -#' -#' @examples -#' req_base <- req_setup( -#' "https://example.com", -#' user_agent = "my_api_client (https://my.api.client)" -#' ) -#' req <- req_modify(req_base, path = c("specific/{path}", path = "endpoint")) -#' req -#' req <- req_modify(req, query = c("param1" = "value1", "param2" = "value2")) -#' req -req_modify <- function(req, - ..., - path = NULL, - query = NULL, - body = NULL, - mime_type = NULL, - method = NULL) { - rlang::check_dots_empty() - req <- .req_path_append(req, path) - req <- .req_query_flatten(req, query) - req <- .req_body_auto(req, body, mime_type) - req <- .req_method_apply(req, method) - return(req) -} - -#' Prepare a request for an API -#' -#' This function implements an opinionated framework for preparing an API -#' request. It is intended to be used inside an API client package. It serves as -#' a wrapper around the `req_` family of functions, such as [httr2::request()]. -#' -#' @inheritParams req_setup -#' @inheritParams req_modify -#' @inheritParams rlang::args_dots_empty -#' -#' @inherit .shared-request return -#' @export -req_prepare <- function(base_url, - ..., - path = NULL, - query = NULL, - body = NULL, - mime_type = NULL, - method = NULL, - user_agent = "nectar (https://nectar.api2r.org)") { - rlang::check_dots_empty() - req <- req_setup(base_url, user_agent = user_agent) - req <- req_modify( - req, - path = path, - query = query, - body = body, - mime_type = mime_type, - method = method - ) - return(req) -} diff --git a/R/auth.R b/R/req_auth_api_key.R similarity index 69% rename from R/auth.R rename to R/req_auth_api_key.R index c3dcceb..598da2e 100644 --- a/R/auth.R +++ b/R/req_auth_api_key.R @@ -4,14 +4,14 @@ #' often, provide other information about the user). This function helps to #' apply those keys to requests. #' -#' @inheritParams .shared-parameters -#' @param location Where the API key should be passed. One of `"header"` -#' (default), `"query"`, or `"cookie"`. +#' @inheritParams .shared-params +#' @param location (`length-1 character`) Where the API key should be passed. +#' One of `"header"` (default), `"query"`, or `"cookie"`. #' @param ... Additional parameters depending on the location of the API key. -#' * `parameter_name` ("header" or "query" only) The name of the parameter to +#' * `parameter_name` (`length-1 character`, "header" or "query" only) The name of the parameter to #' use in the header or query. -#' * `api_key` ("header" or "query" only) The API key to use. -#' * `path` ("cookie" only) The location of the cookie. +#' * `api_key` (`length-1 character`, "header" or "query" only) The API key to use. +#' * `path` (`length-1 character`, "cookie" only) The location of the cookie. #' #' @inherit .shared-request return #' @export @@ -27,9 +27,7 @@ req_auth_api_key <- function(req, #' Authenticate with an API key in the header of the request #' -#' @inheritParams .shared-parameters -#' @param parameter_name The name to use for the API key. -#' @param api_key The API key to use. +#' @inheritParams .shared-params #' #' @inherit .shared-request return #' @keywords internal @@ -47,9 +45,7 @@ req_auth_api_key <- function(req, #' Authenticate with an API key in the query of the request #' -#' @inheritParams .shared-parameters -#' @param parameter_name The name to use for the API key. -#' @param api_key The API key to use. +#' @inheritParams .shared-params #' #' @inherit .shared-request return #' @keywords internal @@ -63,12 +59,13 @@ req_auth_api_key <- function(req, #' Authenticate with an API key in a cookie #' -#' @inheritParams .shared-parameters -#' @param file_path The path to the cookie. +#' @inheritParams .shared-params +#' @param file_path (`length-1 character`, optional) The path to the cookie. If +#' no value is provided this function returns `req` unchanged. #' #' @inherit .shared-request return #' @keywords internal -.req_auth_api_key_cookie <- function(req, ..., file_path) { # nocov start +.req_auth_api_key_cookie <- function(req, ..., file_path = NULL) { # nocov start rlang::check_dots_empty() if (length(file_path) && nchar(file_path)) { req <- httr2::req_cookie_preserve(req, file_path) diff --git a/R/req_body.R b/R/req_body.R deleted file mode 100644 index 68658b5..0000000 --- a/R/req_body.R +++ /dev/null @@ -1,65 +0,0 @@ -.prepare_body <- function(body, - mime_type = NULL) { - body <- compact_nested_list(body) - if (length(body)) { - if (purrr::some(body, \(x) inherits(x, "fs_path"))) { - return(.prepare_body_path(body, mime_type)) - } - class(body) <- c("json", "list") - } - return(body) -} - -.prepare_body_path <- function(body, mime_type) { - body <- purrr::map(body, .prepare_body_part, mime_type) - class(body) <- c("multipart", "list") - return(body) -} - -.prepare_body_part <- function(body_part, mime_type = NULL) { - if (inherits(body_part, "fs_path")) { - return(curl::form_file(body_part, type = mime_type)) - } - return(curl::form_data( - jsonlite::toJSON(body_part, auto_unbox = TRUE), - type = "application/json" - )) -} - - -#' Send data in request body -#' -#' Automatically choose between [httr2::req_body_json()] and -#' [httr2::req_body_multipart()] based on the content of the body. This is -#' currently experimental and needs to be tested on more APIs. -#' -#' @inheritParams .shared-parameters -#' @param body An object to use as the body of the request. If any component of -#' the body is a path, pass it through [fs::path()] or otherwise give it the -#' class "fs_path" to indicate that it is a path. -#' @param mime_type A character scalar indicating the mime type of any files -#' present in the body. Some APIs allow you to leave this as NULL for them to -#' guess. -#' -#' @inherit httr2::req_body_json return -#' @keywords internal -.req_body_auto <- function(req, - body, - mime_type = NULL) { - body <- .prepare_body(body, mime_type) - .do_if_args_defined(req, .add_body, body = body) -} - -.add_body <- function(req, body) { - UseMethod(".add_body", body) -} - -#' @export -.add_body.multipart <- function(req, body) { - return(httr2::req_body_multipart(req, !!!unclass(body))) -} - -#' @export -.add_body.json <- function(req, body) { - return(httr2::req_body_json(req, data = unclass(body))) -} diff --git a/R/req_init.R b/R/req_init.R new file mode 100644 index 0000000..e48ea78 --- /dev/null +++ b/R/req_init.R @@ -0,0 +1,25 @@ +#' Setup a basic API request +#' +#' For a given API, the `base_url` and user agent will generally be the same for +#' every call to that API. Use this function to prepare that piece of the +#' request once for easy reuse. +#' +#' @inheritParams rlang::args_dots_empty +#' @inheritParams .shared-params +#' @inherit .shared-request return +#' @export +#' +#' @examples +#' req_init("https://example.com") +#' req_init( +#' "https://example.com", +#' additional_user_agent = "my_api_client (https://my.api.client)" +#' ) +req_init <- function(base_url, + ..., + additional_user_agent = NULL) { + req <- httr2::request(base_url) + req <- httr2::req_user_agent(req, additional_user_agent) + req <- req_pkg_user_agent(req) + return(req) +} diff --git a/R/req_method.R b/R/req_method.R deleted file mode 100644 index 2727ceb..0000000 --- a/R/req_method.R +++ /dev/null @@ -1,14 +0,0 @@ -#' Add a method if it is supplied -#' -#' [httr2::req_method()] errors if `method` is `NULL`, rather than using the -#' default rules. This function deals with that. -#' -#' @inheritParams .shared-parameters -#' @param method If the method is something other than GET or POST, supply it. -#' Case is ignored. -#' -#' @inherit .shared-request return -#' @keywords internal -.req_method_apply <- function(req, method) { - .do_if_args_defined(req, httr2::req_method, method = method) -} diff --git a/R/req_modify.R b/R/req_modify.R new file mode 100644 index 0000000..d7aba76 --- /dev/null +++ b/R/req_modify.R @@ -0,0 +1,126 @@ +#' Modify an API request for a particular endpoint +#' +#' Modify the basic request for an API by adding a path and any other +#' path-specific properties. +#' +#' @inheritParams rlang::args_dots_empty +#' @inheritParams .shared-params +#' +#' @inherit .shared-request return +#' @export +#' +#' @examples +#' req_base <- req_init("https://example.com") +#' req_modify(req_base, path = c("specific/{path}", path = "endpoint")) +#' req_modify(req_base, query = c("param1" = "value1", "param2" = "value2")) +req_modify <- function(req, + ..., + path = NULL, + query = NULL, + body = NULL, + mime_type = NULL, + method = NULL) { + rlang::check_dots_empty() + req <- .req_path_append(req, path) + req <- .req_query_flatten(req, query) + req <- .req_body_auto(req, body, mime_type) + req <- .req_method_apply(req, method) + return(req) +} + +#' Process a path with glue syntax and append it +#' +#' @inheritParams .shared-params +#' @inherit .shared-request return +#' @keywords internal +.req_path_append <- function(req, path) { + .do_if_args_defined(req, .req_path_append_impl, path = path) +} + +.req_path_append_impl <- function(req, path) { + path <- rlang::inject(glue::glue(!!!path, .sep = "/")) + path <- url_normalize(path) + req <- httr2::req_url_path_append(req, path) +} + +#' Add non-empty query elements to a request +#' +#' @inheritParams .shared-params +#' @inherit .shared-request return +#' @keywords internal +.req_query_flatten <- function(req, + query) { + query <- purrr::discard(query, is.null) + rlang::inject(httr2::req_url_query(req, !!!query)) +} + +#' Add a method if it is supplied +#' +#' [httr2::req_method()] errors if `method` is `NULL`, rather than using the +#' default rules. This function deals with that. +#' +#' @inheritParams .shared-params +#' @inherit .shared-request return +#' @keywords internal +.req_method_apply <- function(req, method) { + .do_if_args_defined(req, httr2::req_method, method = method) +} + +.prepare_body <- function(body, + mime_type = NULL) { + body <- compact_nested_list(body) + if (length(body)) { + if (purrr::some(body, \(x) inherits(x, "fs_path"))) { + return(.prepare_body_path(body, mime_type)) + } + class(body) <- c("json", "list") + } + return(body) +} + +.prepare_body_path <- function(body, mime_type) { + body <- purrr::map(body, .prepare_body_part, mime_type) + class(body) <- c("multipart", "list") + return(body) +} + +.prepare_body_part <- function(body_part, mime_type = NULL) { + if (inherits(body_part, "fs_path")) { + return(curl::form_file(body_part, type = mime_type)) + } + return(curl::form_data( + jsonlite::toJSON(body_part, auto_unbox = TRUE), + type = "application/json" + )) +} + + +#' Send data in request body +#' +#' Automatically choose between [httr2::req_body_json()] and +#' [httr2::req_body_multipart()] based on the content of the body. This is +#' currently experimental and needs to be tested on more APIs. +#' +#' @inheritParams .shared-params +#' @inherit httr2::req_body_json return +#' @keywords internal +.req_body_auto <- function(req, + body, + mime_type = NULL) { + body <- .prepare_body(body, mime_type) + .do_if_args_defined(req, .add_body, body = body) +} + +.add_body <- function(req, body) { + UseMethod(".add_body", body) +} + +#' @export +.add_body.multipart <- function(req, body) { + return(httr2::req_body_multipart(req, !!!unclass(body))) +} + +#' @export +.add_body.json <- function(req, body) { + return(httr2::req_body_json(req, data = unclass(body))) +} diff --git a/R/req_path.R b/R/req_path.R deleted file mode 100644 index 584ba5c..0000000 --- a/R/req_path.R +++ /dev/null @@ -1,19 +0,0 @@ -#' Process a path with glue syntax and append it -#' -#' @inheritParams .shared-parameters -#' @param path The route to an API endpoint. Optionally, a list or character -#' vector with the path as one or more unnamed arguments (which will be -#' concatenated with "/") plus named arguments to [glue::glue()] into the -#' path. -#' -#' @inherit .shared-request return -#' @keywords internal -.req_path_append <- function(req, path) { - .do_if_args_defined(req, .req_path_append_impl, path = path) -} - -.req_path_append_impl <- function(req, path) { - path <- rlang::inject(glue::glue(!!!path, .sep = "/")) - path <- url_normalize(path) - req <- httr2::req_url_path_append(req, path) -} diff --git a/R/perform.R b/R/req_perform_opinionated.R similarity index 100% rename from R/perform.R rename to R/req_perform_opinionated.R diff --git a/R/req_pkg_user_agent.R b/R/req_pkg_user_agent.R new file mode 100644 index 0000000..1f6507e --- /dev/null +++ b/R/req_pkg_user_agent.R @@ -0,0 +1,158 @@ +#' Append package information to user agent +#' +#' Add information about nectar and the calling package (if called from a +#' package) to the user agent string. +#' +#' @inheritParams .shared-params +#' @inherit .shared-request return +#' @export +#' @examples +#' req <- httr2::request("https://example.com") +#' req$options$useragent +#' req_pkg_user_agent(req)$options$useragent +#' req_pkg_user_agent(req, "stbl")$options$useragent +req_pkg_user_agent <- function(req, + pkg_name = get_pkg_name(call), + pkg_url = NULL, + call = rlang::caller_env()) { + # Always include nectar since we're the ones doing this. + user_agent_string <- .nectar_user_agent_append( + existing_user_agent = req$options$useragent %||% + .httr2_default_user_agent(), + call = call + ) + if (length(pkg_name) && pkg_name != "nectar") { + user_agent_string <- .pkg_user_agent_append( + existing_user_agent = user_agent_string, + pkg_name = pkg_name, + pkg_url = pkg_url, + call = call + ) + } + return( + httr2::req_user_agent(req, string = user_agent_string) + ) +} + +#' Append package user agent +#' +#' @inheritParams .shared-params +#' @returns A string to use as a user agent. Attach the agent to your request +#' with [httr2::req_user_agent()]. +#' @keywords internal +.pkg_user_agent_append <- function(existing_user_agent = NULL, + pkg_name = get_pkg_name(call), + pkg_url = NULL, + call = rlang::caller_env()) { + .lib_user_agent_append( + existing_user_agent, + name = pkg_name, + version = .get_pkg_version(pkg_name, call = call), + url = pkg_url, + call = call + ) +} + +#' Generate library user agent string +#' +#' @inheritParams .shared-params +#' @returns A user agent string for the library. +#' @keywords internal +.lib_user_agent_string <- function(name, + version, + url = NULL, + call = rlang::caller_env()) { + name <- stabilize_string(name, call = call) + agent <- glue::glue("{name}/{version}") + url <- stbl::to_chr_scalar(url, call = call) + if (length(url)) { + agent <- glue::glue("{agent} ({url})") + } + return(agent) +} + +#' Add a library and version to a user agent +#' +#' @inheritParams .shared-params +#' @returns A modified user agent string for the library. +#' @keywords internal +.lib_user_agent_append <- function(existing_user_agent, + name, + version, + url = NULL, + call = rlang::caller_env()) { + if (is.null(name)) { + return(existing_user_agent) + } + lib_string <- .lib_user_agent_string(name, version, url = url, call = call) + existing_user_agent <- .user_agent_remove( + existing_user_agent, + name = name, + url = url, + call = call + ) + return( + glue::glue_collapse(c(existing_user_agent, lib_string), sep = " ") + ) +} + +#' Create a nectar user agent string +#' +#' Create or modify a user agent string to identify that a request used the +#' nectar package. +#' +#' @inheritParams .shared-params +#' @returns A string to use as a user agent, with the nectar user agent +#' prepended exactly once. +#' @keywords internal +.nectar_user_agent_append <- function(existing_user_agent = NULL, + call = rlang::caller_env()) { + return( + .pkg_user_agent_append( + pkg_name = "nectar", + existing_user_agent = existing_user_agent, + pkg_url = "https://nectar.api2r.org", + call = call + ) + ) +} + +#' Extract the httr2 default user agent +#' +#' @returns The user agent string generated by the installed version of httr2. +#' @keywords internal +.httr2_default_user_agent <- function() { + httr2::req_user_agent(httr2::request(""))$options$useragent +} + +#' Remove package and version from user agent +#' +#' @inheritParams .shared-params +#' @returns A modified user agent string, minus `name`, any associated version, +#' and the `url`. +#' @keywords internal +.user_agent_remove <- function(existing_user_agent, + name, + url = NULL, + call = rlang::caller_env()) { + existing_user_agent <- stbl::to_chr_scalar(existing_user_agent, call = call) + if (!length(existing_user_agent)) { + return(existing_user_agent) + } + # Order of removal matters. Go from most specific to least specific. + if (length(url)) { + url <- stabilize_string(url, call = call) + existing_user_agent <- stringr::str_remove(existing_user_agent, url) + } + name <- stabilize_string(name, call = call) + existing_user_agent <- stringr::str_remove( + existing_user_agent, + paste0(name, "/[^ ]+") + ) + existing_user_agent <- stringr::str_remove(existing_user_agent, name) + existing_user_agent <- stringr::str_remove_all( + existing_user_agent, + "\\(\\s*\\)" + ) + return(stringr::str_squish(existing_user_agent)) +} diff --git a/R/req_prepare.R b/R/req_prepare.R new file mode 100644 index 0000000..08ee68f --- /dev/null +++ b/R/req_prepare.R @@ -0,0 +1,38 @@ +#' Prepare a request for an API +#' +#' This function implements an opinionated framework for preparing an API +#' request. It is intended to be used inside an API client package. It serves as +#' a wrapper around the `req_` family of functions, such as [httr2::request()]. +#' +#' @seealso [req_init()], [req_modify()], and [do_if_fn_defined()] for finer +#' control of the process. +#' +#' @inheritParams rlang::args_dots_empty +#' @inheritParams .shared-params +#' +#' @inherit .shared-request return +#' @export +req_prepare <- function(base_url, + ..., + path = NULL, + query = NULL, + body = NULL, + mime_type = NULL, + method = NULL, + additional_user_agent = NULL, + auth_fn = NULL, + auth_args = list()) { + rlang::check_dots_empty() + req <- req_init(base_url, additional_user_agent = additional_user_agent) + req <- req_modify( + req, + path = path, + query = query, + body = body, + mime_type = mime_type, + method = method + ) + req <- do_if_fn_defined(req, auth_fn, !!!auth_args) + class(req) <- c("nectar_request", class(req)) + return(req) +} diff --git a/R/req_query.R b/R/req_query.R deleted file mode 100644 index 921db87..0000000 --- a/R/req_query.R +++ /dev/null @@ -1,15 +0,0 @@ -#' Add non-empty query elements to a request -#' -#' @inheritParams .shared-parameters -#' @param query An optional list or character vector of parameters to pass in -#' the query portion of the request. Can also include a `.multi` argument to -#' pass to [httr2::req_url_query()] to control how elements containing -#' multiple values are handled. -#' -#' @inherit .shared-request return -#' @keywords internal -.req_query_flatten <- function(req, - query) { - query <- purrr::discard(query, is.null) - rlang::inject(httr2::req_url_query(req, !!!query)) -} diff --git a/R/resp.R b/R/resp_parse.R similarity index 74% rename from R/resp.R rename to R/resp_parse.R index 23c04a0..362f62f 100644 --- a/R/resp.R +++ b/R/resp_parse.R @@ -7,9 +7,7 @@ #' multiple responses have been returned, and parses the responses #' appropriately. #' -#' @param resp A single [httr2::response()] object (as returned by -#' [httr2::req_perform()]) or a list of such objects (as returned by -#' [httr2::req_perform_iterative()]). +#' @inheritParams .shared-params #' @param ... Additional arguments passed on to the `response_parser` function #' (in addition to `resp`). #' @@ -22,12 +20,8 @@ resp_parse <- function(resp, ...) { UseMethod("resp_parse") } +#' @inheritParams .shared-params #' @export -#' @param arg An argument name as a string. This argument will be mentioned in -#' error messages as the input that is at the origin of a problem. -#' @param call The execution environment of a currently running function, e.g. -#' caller_env(). The function will be mentioned in error messages as the -#' source of the error. See the call argument of abort() for more information. #' @rdname resp_parse resp_parse.default <- function(resp, ..., @@ -43,9 +37,7 @@ resp_parse.default <- function(resp, ) } -#' @param response_parser A function to parse the server response (`resp`). -#' Defaults to [httr2::resp_body_json()], since JSON responses are common. Set -#' this to `NULL` to return the raw response from [httr2::req_perform()]. +#' @inheritParams .shared-params #' @export #' @rdname resp_parse resp_parse.httr2_response <- function(resp, diff --git a/R/utils.R b/R/utils.R index b6a02db..616decf 100644 --- a/R/utils.R +++ b/R/utils.R @@ -96,7 +96,7 @@ url_normalize <- function(url) { #' #' When constructing API calls programmatically, you may encounter situations #' where an upstream task should indicate which function to apply. For example, -#' one endpoint might use a special security function that isn't used by other +#' one endpoint might use a special auth function that isn't used by other #' endpoints. This function exists to make coding such situations easier. #' #' @param x An object to potentially modify, such as a [httr2::request()] @@ -109,17 +109,17 @@ url_normalize <- function(url) { #' @export #' #' @examples -#' build_api_req <- function(endpoint, security_fn = NULL, ...) { +#' build_api_req <- function(endpoint, auth_fn = NULL, ...) { #' req <- httr2::request("https://example.com") #' req <- httr2::req_url_path_append(req, endpoint) -#' do_if_fn_defined(req, security_fn, ...) +#' do_if_fn_defined(req, auth_fn, ...) #' } #' #' # Most endpoints of this API do not require authentication. #' unsecure_req <- build_api_req("unsecure_endpoint") #' unsecure_req$headers #' -#' # But one endpoint requires +#' # But one endpoint requires authentication. #' secure_req <- build_api_req( #' "secure_endpoint", httr2::req_auth_bearer_token, "secret-token" #' ) @@ -153,3 +153,28 @@ do_if_fn_defined <- function(x, fn = NULL, ...) { } return(x) } + +# Calling package information -------------------------------------------------- + +#' Get calling package name +#' +#' @inheritParams .shared-params +#' @returns The package name, or `NULL` (invisibly). This function is intended +#' to be used in other nectar functions to automatically detect the calling +#' package name. +#' @keywords internal +get_pkg_name <- function(call = rlang::caller_env()) { + utils::packageName(env = call) +} + +#' Get package numeric version +#' +#' @inheritParams .shared-params +#' @returns The numeric version of the package. +#' @keywords internal +.get_pkg_version <- function(pkg_name = get_pkg_name(call), + call = rlang::caller_env()) { + pkg_name <- stabilize_string(pkg_name, call = call) + rlang::check_installed(pkg_name, "to find the package version.", call = call) + return(as.character(utils::packageVersion(pkg_name))) +} diff --git a/man/call_api.Rd b/man/call_api.Rd index de53be4..1200b89 100644 --- a/man/call_api.Rd +++ b/man/call_api.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/call.R +% Please edit documentation in R/call_api.R \name{call_api} \alias{call_api} \title{Send a request to an API} @@ -12,56 +12,56 @@ call_api( body = NULL, mime_type = NULL, method = NULL, - security_fn = NULL, - security_args = list(), + auth_fn = NULL, + auth_args = list(), response_parser = httr2::resp_body_json, response_parser_args = list(), next_req = NULL, max_reqs = Inf, max_tries_per_req = 3, - user_agent = "nectar (https://nectar.api2r.org)" + additional_user_agent = NULL ) } \arguments{ -\item{base_url}{The part of the url that is shared by all calls to the API. -In some cases there may be a family of base URLs, from which you will need -to choose one.} +\item{base_url}{(\verb{length-1 character}) The part of the url that is shared by +all calls to the API. In some cases there may be a family of base URLs, +from which you will need to choose one.} \item{...}{These dots are for future extensions and must be empty.} -\item{path}{The route to an API endpoint. Optionally, a list or character -vector with the path as one or more unnamed arguments (which will be -concatenated with "/") plus named arguments to \code{\link[glue:glue]{glue::glue()}} into the -path.} +\item{path}{(\code{character} or \code{list}) The route to an API endpoint. Optionally, +a list or character vector with the path as one or more unnamed arguments +(which will be concatenated with "/") plus named arguments to +\code{\link[glue:glue]{glue::glue()}} into the path.} -\item{query}{An optional list or character vector of parameters to pass in -the query portion of the request. Can also include a \code{.multi} argument to -pass to \code{\link[httr2:req_url]{httr2::req_url_query()}} to control how elements containing -multiple values are handled.} +\item{query}{(\code{character} or \code{list}) An optional list or character vector of +parameters to pass in the query portion of the request. Can also include a +\code{.multi} argument to pass to \code{\link[httr2:req_url]{httr2::req_url_query()}} to control how +elements containing multiple values are handled.} -\item{body}{An object to use as the body of the request. If any component of -the body is a path, pass it through \code{\link[fs:path]{fs::path()}} or otherwise give it the -class "fs_path" to indicate that it is a path.} +\item{body}{(multiple types) An object to use as the body of the request. If +any component of the body is a path, pass it through \code{\link[fs:path]{fs::path()}} or +otherwise give it the class "fs_path" to indicate that it is a path.} -\item{mime_type}{A character scalar indicating the mime type of any files -present in the body. Some APIs allow you to leave this as NULL for them to -guess.} +\item{mime_type}{(\verb{length-1 character}) The mime type of any files present in +the body. Some APIs allow you to leave this as NULL for them to guess.} -\item{method}{If the method is something other than GET or POST, supply it. -Case is ignored.} +\item{method}{(\verb{length-1 character}, optional) If the method is something +other than \code{GET} or \code{POST}, supply it. Case is ignored.} -\item{security_fn}{A function to use to authenticate the request. By default -(\code{NULL}), no authentication is performed.} +\item{auth_fn}{(\code{function}) A function to use to authenticate the request. By +default (\code{NULL}), no authentication is performed.} -\item{security_args}{An optional list of arguments to the \code{security_fn} +\item{auth_args}{(\code{list}) An optional list of arguments to the \code{auth_fn} function.} -\item{response_parser}{A function to parse the server response (\code{resp}). -Defaults to \code{\link[httr2:resp_body_raw]{httr2::resp_body_json()}}, since JSON responses are common. Set -this to \code{NULL} to return the raw response from \code{\link[httr2:req_perform]{httr2::req_perform()}}.} +\item{response_parser}{(\code{function}) A function to parse the server response +(\code{resp}). Defaults to \code{\link[httr2:resp_body_raw]{httr2::resp_body_json()}}, since JSON responses are +common. Set this to \code{NULL} to return the raw response from +\code{\link[httr2:req_perform]{httr2::req_perform()}}.} -\item{response_parser_args}{An optional list of arguments to pass to the -\code{response_parser} function (in addition to \code{resp}).} +\item{response_parser_args}{(\code{list}) Additional arguments to pass to the +\code{response_parser}.} \item{next_req}{An optional function that takes the previous response (\code{resp}) to generate the next request in a call to @@ -78,21 +78,24 @@ validate the function.} individual request. Passed to the \code{max_tries} argument of \code{\link[httr2:req_retry]{httr2::req_retry()}}.} -\item{user_agent}{A string to identify where this request is coming from. -It's polite to set the user agent to identify your package, such as -"MyPackage (https://mypackage.com)".} +\item{additional_user_agent}{(\verb{length-1 character}) A string to identify +where a request is coming from. We automatically include information about +your package and nectar, but use this to provide additional details. +Default \code{NULL}.} } \value{ The response from the API, parsed by the \code{response_parser}. } \description{ -This function implements an opinionated framework for making API calls. It is -intended to be used inside an API client package. It serves as a wrapper -around the \code{req_} family of functions, such as \code{\link[httr2:request]{httr2::request()}}, as well as -\code{\link[httr2:req_perform]{httr2::req_perform()}} and \code{\link[httr2:req_perform_iterative]{httr2::req_perform_iterative()}}, and, by default, -\code{\link[httr2:resp_body_raw]{httr2::resp_body_json()}}. +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#questioning}{\figure{lifecycle-questioning.svg}{options: alt='[Questioning]'}}}{\strong{[Questioning]}} + +This function implements an opinionated framework for making API calls. It +is intended to be used inside an API client package. It serves as a wrapper +around the \code{req_} family of functions, such as \code{\link[httr2:request]{httr2::request()}}, as well +as \code{\link[httr2:req_perform]{httr2::req_perform()}} and \code{\link[httr2:req_perform_iterative]{httr2::req_perform_iterative()}}, and, by +default, \code{\link[httr2:resp_body_raw]{httr2::resp_body_json()}}. } \seealso{ -\code{\link[=req_setup]{req_setup()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_perform_opinionated]{req_perform_opinionated()}}, -\code{\link[=resp_parse]{resp_parse()}}, and \code{\link[=do_if_fn_defined]{do_if_fn_defined()}} for finer control of the process. +\code{\link[=req_prepare]{req_prepare()}}, \code{\link[=req_perform_opinionated]{req_perform_opinionated()}}, and \code{\link[=resp_parse]{resp_parse()}} for +finer control of the process. } diff --git a/man/do_if_fn_defined.Rd b/man/do_if_fn_defined.Rd index aee53ff..bacfa70 100644 --- a/man/do_if_fn_defined.Rd +++ b/man/do_if_fn_defined.Rd @@ -21,21 +21,21 @@ The object, potentially modified. \description{ When constructing API calls programmatically, you may encounter situations where an upstream task should indicate which function to apply. For example, -one endpoint might use a special security function that isn't used by other +one endpoint might use a special auth function that isn't used by other endpoints. This function exists to make coding such situations easier. } \examples{ -build_api_req <- function(endpoint, security_fn = NULL, ...) { +build_api_req <- function(endpoint, auth_fn = NULL, ...) { req <- httr2::request("https://example.com") req <- httr2::req_url_path_append(req, endpoint) - do_if_fn_defined(req, security_fn, ...) + do_if_fn_defined(req, auth_fn, ...) } # Most endpoints of this API do not require authentication. unsecure_req <- build_api_req("unsecure_endpoint") unsecure_req$headers -# But one endpoint requires +# But one endpoint requires authentication. secure_req <- build_api_req( "secure_endpoint", httr2::req_auth_bearer_token, "secret-token" ) diff --git a/man/dot-get_pkg_version.Rd b/man/dot-get_pkg_version.Rd new file mode 100644 index 0000000..43be689 --- /dev/null +++ b/man/dot-get_pkg_version.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{.get_pkg_version} +\alias{.get_pkg_version} +\title{Get package numeric version} +\usage{ +.get_pkg_version(pkg_name = get_pkg_name(call), call = rlang::caller_env()) +} +\arguments{ +\item{pkg_name}{(\verb{length-1 character}) The name of the calling package. This +will usually be automatically determined based on the source of the call.} + +\item{call}{(\code{environment}) The environment from which a function was +called, e.g. \code{\link[rlang:stack]{rlang::caller_env()}} (the default). The environment will be +mentioned in error messages as the source of the error. This argument is +particularly useful for functions that are intended to be called as +utilities inside other functions.} +} +\value{ +The numeric version of the package. +} +\description{ +Get package numeric version +} +\keyword{internal} diff --git a/man/dot-httr2_default_user_agent.Rd b/man/dot-httr2_default_user_agent.Rd new file mode 100644 index 0000000..3cea73a --- /dev/null +++ b/man/dot-httr2_default_user_agent.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/req_pkg_user_agent.R +\name{.httr2_default_user_agent} +\alias{.httr2_default_user_agent} +\title{Extract the httr2 default user agent} +\usage{ +.httr2_default_user_agent() +} +\value{ +The user agent string generated by the installed version of httr2. +} +\description{ +Extract the httr2 default user agent +} +\keyword{internal} diff --git a/man/dot-lib_user_agent_append.Rd b/man/dot-lib_user_agent_append.Rd new file mode 100644 index 0000000..fb0b8de --- /dev/null +++ b/man/dot-lib_user_agent_append.Rd @@ -0,0 +1,39 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/req_pkg_user_agent.R +\name{.lib_user_agent_append} +\alias{.lib_user_agent_append} +\title{Add a library and version to a user agent} +\usage{ +.lib_user_agent_append( + existing_user_agent, + name, + version, + url = NULL, + call = rlang::caller_env() +) +} +\arguments{ +\item{existing_user_agent}{(\verb{length-1 character}, optional) An existing user +agent, such as the value of \code{req$options$useragent} in a \code{\link[httr2:request]{httr2::request()}} +object.} + +\item{name}{(\verb{length-1 character}) The name of a package or other thing to +add to or remove from the user agent string.} + +\item{version}{(\verb{length-1 character}) The version of \code{name}.} + +\item{url}{(\verb{length-1 character}) An optional url associated with \code{name}.} + +\item{call}{(\code{environment}) The environment from which a function was +called, e.g. \code{\link[rlang:stack]{rlang::caller_env()}} (the default). The environment will be +mentioned in error messages as the source of the error. This argument is +particularly useful for functions that are intended to be called as +utilities inside other functions.} +} +\value{ +A modified user agent string for the library. +} +\description{ +Add a library and version to a user agent +} +\keyword{internal} diff --git a/man/dot-lib_user_agent_string.Rd b/man/dot-lib_user_agent_string.Rd new file mode 100644 index 0000000..d876c2f --- /dev/null +++ b/man/dot-lib_user_agent_string.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/req_pkg_user_agent.R +\name{.lib_user_agent_string} +\alias{.lib_user_agent_string} +\title{Generate library user agent string} +\usage{ +.lib_user_agent_string(name, version, url = NULL, call = rlang::caller_env()) +} +\arguments{ +\item{name}{(\verb{length-1 character}) The name of a package or other thing to +add to or remove from the user agent string.} + +\item{version}{(\verb{length-1 character}) The version of \code{name}.} + +\item{url}{(\verb{length-1 character}) An optional url associated with \code{name}.} + +\item{call}{(\code{environment}) The environment from which a function was +called, e.g. \code{\link[rlang:stack]{rlang::caller_env()}} (the default). The environment will be +mentioned in error messages as the source of the error. This argument is +particularly useful for functions that are intended to be called as +utilities inside other functions.} +} +\value{ +A user agent string for the library. +} +\description{ +Generate library user agent string +} +\keyword{internal} diff --git a/man/dot-nectar_user_agent_append.Rd b/man/dot-nectar_user_agent_append.Rd new file mode 100644 index 0000000..6986a6f --- /dev/null +++ b/man/dot-nectar_user_agent_append.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/req_pkg_user_agent.R +\name{.nectar_user_agent_append} +\alias{.nectar_user_agent_append} +\title{Create a nectar user agent string} +\usage{ +.nectar_user_agent_append( + existing_user_agent = NULL, + call = rlang::caller_env() +) +} +\arguments{ +\item{existing_user_agent}{(\verb{length-1 character}, optional) An existing user +agent, such as the value of \code{req$options$useragent} in a \code{\link[httr2:request]{httr2::request()}} +object.} + +\item{call}{(\code{environment}) The environment from which a function was +called, e.g. \code{\link[rlang:stack]{rlang::caller_env()}} (the default). The environment will be +mentioned in error messages as the source of the error. This argument is +particularly useful for functions that are intended to be called as +utilities inside other functions.} +} +\value{ +A string to use as a user agent, with the nectar user agent +prepended exactly once. +} +\description{ +Create or modify a user agent string to identify that a request used the +nectar package. +} +\keyword{internal} diff --git a/man/dot-pkg_user_agent_append.Rd b/man/dot-pkg_user_agent_append.Rd new file mode 100644 index 0000000..69a9a08 --- /dev/null +++ b/man/dot-pkg_user_agent_append.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/req_pkg_user_agent.R +\name{.pkg_user_agent_append} +\alias{.pkg_user_agent_append} +\title{Append package user agent} +\usage{ +.pkg_user_agent_append( + existing_user_agent = NULL, + pkg_name = get_pkg_name(call), + pkg_url = NULL, + call = rlang::caller_env() +) +} +\arguments{ +\item{existing_user_agent}{(\verb{length-1 character}, optional) An existing user +agent, such as the value of \code{req$options$useragent} in a \code{\link[httr2:request]{httr2::request()}} +object.} + +\item{pkg_name}{(\verb{length-1 character}) The name of the calling package. This +will usually be automatically determined based on the source of the call.} + +\item{pkg_url}{(\verb{length-1 character}) A url for information about the calling +package (default \code{NULL}).} + +\item{call}{(\code{environment}) The environment from which a function was +called, e.g. \code{\link[rlang:stack]{rlang::caller_env()}} (the default). The environment will be +mentioned in error messages as the source of the error. This argument is +particularly useful for functions that are intended to be called as +utilities inside other functions.} +} +\value{ +A string to use as a user agent. Attach the agent to your request +with \code{\link[httr2:req_user_agent]{httr2::req_user_agent()}}. +} +\description{ +Append package user agent +} +\keyword{internal} diff --git a/man/dot-req_auth_api_key_cookie.Rd b/man/dot-req_auth_api_key_cookie.Rd index 08e2fc4..c7082cc 100644 --- a/man/dot-req_auth_api_key_cookie.Rd +++ b/man/dot-req_auth_api_key_cookie.Rd @@ -1,15 +1,18 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/auth.R +% Please edit documentation in R/req_auth_api_key.R \name{.req_auth_api_key_cookie} \alias{.req_auth_api_key_cookie} \title{Authenticate with an API key in a cookie} \usage{ -.req_auth_api_key_cookie(req, ..., file_path) +.req_auth_api_key_cookie(req, ..., file_path = NULL) } \arguments{ -\item{req}{A \code{\link[httr2:request]{httr2::request()}} object.} +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} -\item{file_path}{The path to the cookie.} +\item{...}{These dots are for future extensions and must be empty.} + +\item{file_path}{(\verb{length-1 character}, optional) The path to the cookie. If +no value is provided this function returns \code{req} unchanged.} } \value{ A \code{\link[httr2:request]{httr2::request()}} object. diff --git a/man/dot-req_auth_api_key_header.Rd b/man/dot-req_auth_api_key_header.Rd index 513ad1f..5a23ecc 100644 --- a/man/dot-req_auth_api_key_header.Rd +++ b/man/dot-req_auth_api_key_header.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/auth.R +% Please edit documentation in R/req_auth_api_key.R \name{.req_auth_api_key_header} \alias{.req_auth_api_key_header} \title{Authenticate with an API key in the header of the request} @@ -7,11 +7,13 @@ .req_auth_api_key_header(req, ..., parameter_name, api_key = NULL) } \arguments{ -\item{req}{A \code{\link[httr2:request]{httr2::request()}} object.} +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} -\item{parameter_name}{The name to use for the API key.} +\item{...}{These dots are for future extensions and must be empty.} -\item{api_key}{The API key to use.} +\item{parameter_name}{(\verb{length-1 character}) The name to use for the API key.} + +\item{api_key}{(\verb{length-1 character}) The API key to use.} } \value{ A \code{\link[httr2:request]{httr2::request()}} object. diff --git a/man/dot-req_auth_api_key_query.Rd b/man/dot-req_auth_api_key_query.Rd index 63de171..e15df73 100644 --- a/man/dot-req_auth_api_key_query.Rd +++ b/man/dot-req_auth_api_key_query.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/auth.R +% Please edit documentation in R/req_auth_api_key.R \name{.req_auth_api_key_query} \alias{.req_auth_api_key_query} \title{Authenticate with an API key in the query of the request} @@ -7,11 +7,13 @@ .req_auth_api_key_query(req, ..., parameter_name, api_key) } \arguments{ -\item{req}{A \code{\link[httr2:request]{httr2::request()}} object.} +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} -\item{parameter_name}{The name to use for the API key.} +\item{...}{These dots are for future extensions and must be empty.} -\item{api_key}{The API key to use.} +\item{parameter_name}{(\verb{length-1 character}) The name to use for the API key.} + +\item{api_key}{(\verb{length-1 character}) The API key to use.} } \value{ A \code{\link[httr2:request]{httr2::request()}} object. diff --git a/man/dot-req_body_auto.Rd b/man/dot-req_body_auto.Rd index 54d4a68..8c1e85e 100644 --- a/man/dot-req_body_auto.Rd +++ b/man/dot-req_body_auto.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/req_body.R +% Please edit documentation in R/req_modify.R \name{.req_body_auto} \alias{.req_body_auto} \title{Send data in request body} @@ -7,15 +7,14 @@ .req_body_auto(req, body, mime_type = NULL) } \arguments{ -\item{req}{A \code{\link[httr2:request]{httr2::request()}} object.} +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} -\item{body}{An object to use as the body of the request. If any component of -the body is a path, pass it through \code{\link[fs:path]{fs::path()}} or otherwise give it the -class "fs_path" to indicate that it is a path.} +\item{body}{(multiple types) An object to use as the body of the request. If +any component of the body is a path, pass it through \code{\link[fs:path]{fs::path()}} or +otherwise give it the class "fs_path" to indicate that it is a path.} -\item{mime_type}{A character scalar indicating the mime type of any files -present in the body. Some APIs allow you to leave this as NULL for them to -guess.} +\item{mime_type}{(\verb{length-1 character}) The mime type of any files present in +the body. Some APIs allow you to leave this as NULL for them to guess.} } \value{ A modified HTTP \link[httr2]{request}. diff --git a/man/dot-req_method_apply.Rd b/man/dot-req_method_apply.Rd index e01fbfd..1309775 100644 --- a/man/dot-req_method_apply.Rd +++ b/man/dot-req_method_apply.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/req_method.R +% Please edit documentation in R/req_modify.R \name{.req_method_apply} \alias{.req_method_apply} \title{Add a method if it is supplied} @@ -7,10 +7,10 @@ .req_method_apply(req, method) } \arguments{ -\item{req}{A \code{\link[httr2:request]{httr2::request()}} object.} +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} -\item{method}{If the method is something other than GET or POST, supply it. -Case is ignored.} +\item{method}{(\verb{length-1 character}, optional) If the method is something +other than \code{GET} or \code{POST}, supply it. Case is ignored.} } \value{ A \code{\link[httr2:request]{httr2::request()}} object. diff --git a/man/dot-req_path_append.Rd b/man/dot-req_path_append.Rd index 894c1dc..a05f4b4 100644 --- a/man/dot-req_path_append.Rd +++ b/man/dot-req_path_append.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/req_path.R +% Please edit documentation in R/req_modify.R \name{.req_path_append} \alias{.req_path_append} \title{Process a path with glue syntax and append it} @@ -7,12 +7,12 @@ .req_path_append(req, path) } \arguments{ -\item{req}{A \code{\link[httr2:request]{httr2::request()}} object.} +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} -\item{path}{The route to an API endpoint. Optionally, a list or character -vector with the path as one or more unnamed arguments (which will be -concatenated with "/") plus named arguments to \code{\link[glue:glue]{glue::glue()}} into the -path.} +\item{path}{(\code{character} or \code{list}) The route to an API endpoint. Optionally, +a list or character vector with the path as one or more unnamed arguments +(which will be concatenated with "/") plus named arguments to +\code{\link[glue:glue]{glue::glue()}} into the path.} } \value{ A \code{\link[httr2:request]{httr2::request()}} object. diff --git a/man/dot-req_query_flatten.Rd b/man/dot-req_query_flatten.Rd index 7aecf4f..d202d2a 100644 --- a/man/dot-req_query_flatten.Rd +++ b/man/dot-req_query_flatten.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/req_query.R +% Please edit documentation in R/req_modify.R \name{.req_query_flatten} \alias{.req_query_flatten} \title{Add non-empty query elements to a request} @@ -7,12 +7,12 @@ .req_query_flatten(req, query) } \arguments{ -\item{req}{A \code{\link[httr2:request]{httr2::request()}} object.} +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} -\item{query}{An optional list or character vector of parameters to pass in -the query portion of the request. Can also include a \code{.multi} argument to -pass to \code{\link[httr2:req_url]{httr2::req_url_query()}} to control how elements containing -multiple values are handled.} +\item{query}{(\code{character} or \code{list}) An optional list or character vector of +parameters to pass in the query portion of the request. Can also include a +\code{.multi} argument to pass to \code{\link[httr2:req_url]{httr2::req_url_query()}} to control how +elements containing multiple values are handled.} } \value{ A \code{\link[httr2:request]{httr2::request()}} object. diff --git a/man/dot-shared-parameters.Rd b/man/dot-shared-parameters.Rd deleted file mode 100644 index 0104bfd..0000000 --- a/man/dot-shared-parameters.Rd +++ /dev/null @@ -1,14 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/aaa-shared.R -\name{.shared-parameters} -\alias{.shared-parameters} -\title{Parameters used in multiple functions} -\arguments{ -\item{req}{A \code{\link[httr2:request]{httr2::request()}} object.} - -\item{x}{The object to update.} -} -\description{ -Reused parameter definitions are gathered here for easier editing. -} -\keyword{internal} diff --git a/man/dot-shared-params.Rd b/man/dot-shared-params.Rd new file mode 100644 index 0000000..5a801cd --- /dev/null +++ b/man/dot-shared-params.Rd @@ -0,0 +1,94 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/aaa-shared.R +\name{.shared-params} +\alias{.shared-params} +\title{Parameters used in multiple functions} +\arguments{ +\item{additional_user_agent}{(\verb{length-1 character}) A string to identify +where a request is coming from. We automatically include information about +your package and nectar, but use this to provide additional details. +Default \code{NULL}.} + +\item{api_key}{(\verb{length-1 character}) The API key to use.} + +\item{arg}{(\verb{length-1 character}) An argument name as a string. This argument +will be mentioned in error messages as the input that is at the origin of a +problem.} + +\item{auth_args}{(\code{list}) An optional list of arguments to the \code{auth_fn} +function.} + +\item{auth_fn}{(\code{function}) A function to use to authenticate the request. By +default (\code{NULL}), no authentication is performed.} + +\item{base_url}{(\verb{length-1 character}) The part of the url that is shared by +all calls to the API. In some cases there may be a family of base URLs, +from which you will need to choose one.} + +\item{body}{(multiple types) An object to use as the body of the request. If +any component of the body is a path, pass it through \code{\link[fs:path]{fs::path()}} or +otherwise give it the class "fs_path" to indicate that it is a path.} + +\item{call}{(\code{environment}) The environment from which a function was +called, e.g. \code{\link[rlang:stack]{rlang::caller_env()}} (the default). The environment will be +mentioned in error messages as the source of the error. This argument is +particularly useful for functions that are intended to be called as +utilities inside other functions.} + +\item{existing_user_agent}{(\verb{length-1 character}, optional) An existing user +agent, such as the value of \code{req$options$useragent} in a \code{\link[httr2:request]{httr2::request()}} +object.} + +\item{method}{(\verb{length-1 character}, optional) If the method is something +other than \code{GET} or \code{POST}, supply it. Case is ignored.} + +\item{mime_type}{(\verb{length-1 character}) The mime type of any files present in +the body. Some APIs allow you to leave this as NULL for them to guess.} + +\item{name}{(\verb{length-1 character}) The name of a package or other thing to +add to or remove from the user agent string.} + +\item{pkg_name}{(\verb{length-1 character}) The name of the calling package. This +will usually be automatically determined based on the source of the call.} + +\item{pkg_url}{(\verb{length-1 character}) A url for information about the calling +package (default \code{NULL}).} + +\item{parameter_name}{(\verb{length-1 character}) The name to use for the API key.} + +\item{path}{(\code{character} or \code{list}) The route to an API endpoint. Optionally, +a list or character vector with the path as one or more unnamed arguments +(which will be concatenated with "/") plus named arguments to +\code{\link[glue:glue]{glue::glue()}} into the path.} + +\item{query}{(\code{character} or \code{list}) An optional list or character vector of +parameters to pass in the query portion of the request. Can also include a +\code{.multi} argument to pass to \code{\link[httr2:req_url]{httr2::req_url_query()}} to control how +elements containing multiple values are handled.} + +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} + +\item{resp}{(\code{httr2_response} or \code{list}) A single \code{\link[httr2:response]{httr2::response()}} object +(as returned by \code{\link[httr2:req_perform]{httr2::req_perform()}}) or a list of such objects (as +returned by \code{\link[httr2:req_perform_iterative]{httr2::req_perform_iterative()}}).} + +\item{response_parser}{(\code{function}) A function to parse the server response +(\code{resp}). Defaults to \code{\link[httr2:resp_body_raw]{httr2::resp_body_json()}}, since JSON responses are +common. Set this to \code{NULL} to return the raw response from +\code{\link[httr2:req_perform]{httr2::req_perform()}}.} + +\item{response_parser_args}{(\code{list}) An optional list of arguments to pass to +the \code{response_parser} function (in addition to \code{resp}).} + +\item{url}{(\verb{length-1 character}) An optional url associated with \code{name}.} + +\item{version}{(\verb{length-1 character}) The version of \code{name}.} + +\item{x}{(multiple types) The object to update.} + +\item{...}{These dots are for future extensions and must be empty.} +} +\description{ +Reused parameter definitions are gathered here for easier editing. +} +\keyword{internal} diff --git a/man/dot-user_agent_remove.Rd b/man/dot-user_agent_remove.Rd new file mode 100644 index 0000000..404b4e0 --- /dev/null +++ b/man/dot-user_agent_remove.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/req_pkg_user_agent.R +\name{.user_agent_remove} +\alias{.user_agent_remove} +\title{Remove package and version from user agent} +\usage{ +.user_agent_remove( + existing_user_agent, + name, + url = NULL, + call = rlang::caller_env() +) +} +\arguments{ +\item{existing_user_agent}{(\verb{length-1 character}, optional) An existing user +agent, such as the value of \code{req$options$useragent} in a \code{\link[httr2:request]{httr2::request()}} +object.} + +\item{name}{(\verb{length-1 character}) The name of a package or other thing to +add to or remove from the user agent string.} + +\item{url}{(\verb{length-1 character}) An optional url associated with \code{name}.} + +\item{call}{(\code{environment}) The environment from which a function was +called, e.g. \code{\link[rlang:stack]{rlang::caller_env()}} (the default). The environment will be +mentioned in error messages as the source of the error. This argument is +particularly useful for functions that are intended to be called as +utilities inside other functions.} +} +\value{ +A modified user agent string, minus \code{name}, any associated version, +and the \code{url}. +} +\description{ +Remove package and version from user agent +} +\keyword{internal} diff --git a/man/figures/lifecycle-deprecated.svg b/man/figures/lifecycle-deprecated.svg new file mode 100644 index 0000000..b61c57c --- /dev/null +++ b/man/figures/lifecycle-deprecated.svg @@ -0,0 +1,21 @@ + + lifecycle: deprecated + + + + + + + + + + + + + + + lifecycle + + deprecated + + diff --git a/man/figures/lifecycle-experimental.svg b/man/figures/lifecycle-experimental.svg new file mode 100644 index 0000000..5d88fc2 --- /dev/null +++ b/man/figures/lifecycle-experimental.svg @@ -0,0 +1,21 @@ + + lifecycle: experimental + + + + + + + + + + + + + + + lifecycle + + experimental + + diff --git a/man/figures/lifecycle-stable.svg b/man/figures/lifecycle-stable.svg new file mode 100644 index 0000000..9bf21e7 --- /dev/null +++ b/man/figures/lifecycle-stable.svg @@ -0,0 +1,29 @@ + + lifecycle: stable + + + + + + + + + + + + + + + + lifecycle + + + + stable + + + diff --git a/man/figures/lifecycle-superseded.svg b/man/figures/lifecycle-superseded.svg new file mode 100644 index 0000000..db8d757 --- /dev/null +++ b/man/figures/lifecycle-superseded.svg @@ -0,0 +1,21 @@ + + lifecycle: superseded + + + + + + + + + + + + + + + lifecycle + + superseded + + diff --git a/man/get_pkg_name.Rd b/man/get_pkg_name.Rd new file mode 100644 index 0000000..789ad25 --- /dev/null +++ b/man/get_pkg_name.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{get_pkg_name} +\alias{get_pkg_name} +\title{Get calling package name} +\usage{ +get_pkg_name(call = rlang::caller_env()) +} +\arguments{ +\item{call}{(\code{environment}) The environment from which a function was +called, e.g. \code{\link[rlang:stack]{rlang::caller_env()}} (the default). The environment will be +mentioned in error messages as the source of the error. This argument is +particularly useful for functions that are intended to be called as +utilities inside other functions.} +} +\value{ +The package name, or \code{NULL} (invisibly). This function is intended +to be used in other nectar functions to automatically detect the calling +package name. +} +\description{ +Get calling package name +} +\keyword{internal} diff --git a/man/req_auth_api_key.Rd b/man/req_auth_api_key.Rd index 2b07e46..0c0125e 100644 --- a/man/req_auth_api_key.Rd +++ b/man/req_auth_api_key.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/auth.R +% Please edit documentation in R/req_auth_api_key.R \name{req_auth_api_key} \alias{req_auth_api_key} \title{Authenticate with an API key} @@ -7,18 +7,18 @@ req_auth_api_key(req, ..., location = "header") } \arguments{ -\item{req}{A \code{\link[httr2:request]{httr2::request()}} object.} +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} \item{...}{Additional parameters depending on the location of the API key. \itemize{ -\item \code{parameter_name} ("header" or "query" only) The name of the parameter to +\item \code{parameter_name} (\verb{length-1 character}, "header" or "query" only) The name of the parameter to use in the header or query. -\item \code{api_key} ("header" or "query" only) The API key to use. -\item \code{path} ("cookie" only) The location of the cookie. +\item \code{api_key} (\verb{length-1 character}, "header" or "query" only) The API key to use. +\item \code{path} (\verb{length-1 character}, "cookie" only) The location of the cookie. }} -\item{location}{Where the API key should be passed. One of \code{"header"} -(default), \code{"query"}, or \code{"cookie"}.} +\item{location}{(\verb{length-1 character}) Where the API key should be passed. +One of \code{"header"} (default), \code{"query"}, or \code{"cookie"}.} } \value{ A \code{\link[httr2:request]{httr2::request()}} object. diff --git a/man/req_init.Rd b/man/req_init.Rd new file mode 100644 index 0000000..855dba5 --- /dev/null +++ b/man/req_init.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/req_init.R +\name{req_init} +\alias{req_init} +\title{Setup a basic API request} +\usage{ +req_init(base_url, ..., additional_user_agent = NULL) +} +\arguments{ +\item{base_url}{(\verb{length-1 character}) The part of the url that is shared by +all calls to the API. In some cases there may be a family of base URLs, +from which you will need to choose one.} + +\item{...}{These dots are for future extensions and must be empty.} + +\item{additional_user_agent}{(\verb{length-1 character}) A string to identify +where a request is coming from. We automatically include information about +your package and nectar, but use this to provide additional details. +Default \code{NULL}.} +} +\value{ +A \code{\link[httr2:request]{httr2::request()}} object. +} +\description{ +For a given API, the \code{base_url} and user agent will generally be the same for +every call to that API. Use this function to prepare that piece of the +request once for easy reuse. +} +\examples{ +req_init("https://example.com") +req_init( + "https://example.com", + additional_user_agent = "my_api_client (https://my.api.client)" +) +} diff --git a/man/req_modify.Rd b/man/req_modify.Rd index c0fdca9..915d3f5 100644 --- a/man/req_modify.Rd +++ b/man/req_modify.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/req.R +% Please edit documentation in R/req_modify.R \name{req_modify} \alias{req_modify} \title{Modify an API request for a particular endpoint} @@ -15,30 +15,29 @@ req_modify( ) } \arguments{ -\item{req}{A \code{\link[httr2:request]{httr2::request()}} object.} +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} \item{...}{These dots are for future extensions and must be empty.} -\item{path}{The route to an API endpoint. Optionally, a list or character -vector with the path as one or more unnamed arguments (which will be -concatenated with "/") plus named arguments to \code{\link[glue:glue]{glue::glue()}} into the -path.} +\item{path}{(\code{character} or \code{list}) The route to an API endpoint. Optionally, +a list or character vector with the path as one or more unnamed arguments +(which will be concatenated with "/") plus named arguments to +\code{\link[glue:glue]{glue::glue()}} into the path.} -\item{query}{An optional list or character vector of parameters to pass in -the query portion of the request. Can also include a \code{.multi} argument to -pass to \code{\link[httr2:req_url]{httr2::req_url_query()}} to control how elements containing -multiple values are handled.} +\item{query}{(\code{character} or \code{list}) An optional list or character vector of +parameters to pass in the query portion of the request. Can also include a +\code{.multi} argument to pass to \code{\link[httr2:req_url]{httr2::req_url_query()}} to control how +elements containing multiple values are handled.} -\item{body}{An object to use as the body of the request. If any component of -the body is a path, pass it through \code{\link[fs:path]{fs::path()}} or otherwise give it the -class "fs_path" to indicate that it is a path.} +\item{body}{(multiple types) An object to use as the body of the request. If +any component of the body is a path, pass it through \code{\link[fs:path]{fs::path()}} or +otherwise give it the class "fs_path" to indicate that it is a path.} -\item{mime_type}{A character scalar indicating the mime type of any files -present in the body. Some APIs allow you to leave this as NULL for them to -guess.} +\item{mime_type}{(\verb{length-1 character}) The mime type of any files present in +the body. Some APIs allow you to leave this as NULL for them to guess.} -\item{method}{If the method is something other than GET or POST, supply it. -Case is ignored.} +\item{method}{(\verb{length-1 character}, optional) If the method is something +other than \code{GET} or \code{POST}, supply it. Case is ignored.} } \value{ A \code{\link[httr2:request]{httr2::request()}} object. @@ -48,12 +47,7 @@ Modify the basic request for an API by adding a path and any other path-specific properties. } \examples{ -req_base <- req_setup( - "https://example.com", - user_agent = "my_api_client (https://my.api.client)" -) -req <- req_modify(req_base, path = c("specific/{path}", path = "endpoint")) -req -req <- req_modify(req, query = c("param1" = "value1", "param2" = "value2")) -req +req_base <- req_init("https://example.com") +req_modify(req_base, path = c("specific/{path}", path = "endpoint")) +req_modify(req_base, query = c("param1" = "value1", "param2" = "value2")) } diff --git a/man/req_perform_opinionated.Rd b/man/req_perform_opinionated.Rd index 1c72263..c1c8b2a 100644 --- a/man/req_perform_opinionated.Rd +++ b/man/req_perform_opinionated.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/perform.R +% Please edit documentation in R/req_perform_opinionated.R \name{req_perform_opinionated} \alias{req_perform_opinionated} \title{Perform a request with opinionated defaults} diff --git a/man/req_pkg_user_agent.Rd b/man/req_pkg_user_agent.Rd new file mode 100644 index 0000000..dd2b913 --- /dev/null +++ b/man/req_pkg_user_agent.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/req_pkg_user_agent.R +\name{req_pkg_user_agent} +\alias{req_pkg_user_agent} +\title{Append package information to user agent} +\usage{ +req_pkg_user_agent( + req, + pkg_name = get_pkg_name(call), + pkg_url = NULL, + call = rlang::caller_env() +) +} +\arguments{ +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} + +\item{pkg_name}{(\verb{length-1 character}) The name of the calling package. This +will usually be automatically determined based on the source of the call.} + +\item{pkg_url}{(\verb{length-1 character}) A url for information about the calling +package (default \code{NULL}).} + +\item{call}{(\code{environment}) The environment from which a function was +called, e.g. \code{\link[rlang:stack]{rlang::caller_env()}} (the default). The environment will be +mentioned in error messages as the source of the error. This argument is +particularly useful for functions that are intended to be called as +utilities inside other functions.} +} +\value{ +A \code{\link[httr2:request]{httr2::request()}} object. +} +\description{ +Add information about nectar and the calling package (if called from a +package) to the user agent string. +} +\examples{ +req <- httr2::request("https://example.com") +req$options$useragent +req_pkg_user_agent(req)$options$useragent +req_pkg_user_agent(req, "stbl")$options$useragent +} diff --git a/man/req_prepare.Rd b/man/req_prepare.Rd index 7fd3c67..01cd2f5 100644 --- a/man/req_prepare.Rd +++ b/man/req_prepare.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/req.R +% Please edit documentation in R/req_prepare.R \name{req_prepare} \alias{req_prepare} \title{Prepare a request for an API} @@ -12,40 +12,48 @@ req_prepare( body = NULL, mime_type = NULL, method = NULL, - user_agent = "nectar (https://nectar.api2r.org)" + additional_user_agent = NULL, + auth_fn = NULL, + auth_args = list() ) } \arguments{ -\item{base_url}{The part of the url that is shared by all calls to the API. -In some cases there may be a family of base URLs, from which you will need -to choose one.} +\item{base_url}{(\verb{length-1 character}) The part of the url that is shared by +all calls to the API. In some cases there may be a family of base URLs, +from which you will need to choose one.} \item{...}{These dots are for future extensions and must be empty.} -\item{path}{The route to an API endpoint. Optionally, a list or character -vector with the path as one or more unnamed arguments (which will be -concatenated with "/") plus named arguments to \code{\link[glue:glue]{glue::glue()}} into the -path.} +\item{path}{(\code{character} or \code{list}) The route to an API endpoint. Optionally, +a list or character vector with the path as one or more unnamed arguments +(which will be concatenated with "/") plus named arguments to +\code{\link[glue:glue]{glue::glue()}} into the path.} -\item{query}{An optional list or character vector of parameters to pass in -the query portion of the request. Can also include a \code{.multi} argument to -pass to \code{\link[httr2:req_url]{httr2::req_url_query()}} to control how elements containing -multiple values are handled.} +\item{query}{(\code{character} or \code{list}) An optional list or character vector of +parameters to pass in the query portion of the request. Can also include a +\code{.multi} argument to pass to \code{\link[httr2:req_url]{httr2::req_url_query()}} to control how +elements containing multiple values are handled.} -\item{body}{An object to use as the body of the request. If any component of -the body is a path, pass it through \code{\link[fs:path]{fs::path()}} or otherwise give it the -class "fs_path" to indicate that it is a path.} +\item{body}{(multiple types) An object to use as the body of the request. If +any component of the body is a path, pass it through \code{\link[fs:path]{fs::path()}} or +otherwise give it the class "fs_path" to indicate that it is a path.} -\item{mime_type}{A character scalar indicating the mime type of any files -present in the body. Some APIs allow you to leave this as NULL for them to -guess.} +\item{mime_type}{(\verb{length-1 character}) The mime type of any files present in +the body. Some APIs allow you to leave this as NULL for them to guess.} -\item{method}{If the method is something other than GET or POST, supply it. -Case is ignored.} +\item{method}{(\verb{length-1 character}, optional) If the method is something +other than \code{GET} or \code{POST}, supply it. Case is ignored.} -\item{user_agent}{A string to identify where this request is coming from. -It's polite to set the user agent to identify your package, such as -"MyPackage (https://mypackage.com)".} +\item{additional_user_agent}{(\verb{length-1 character}) A string to identify +where a request is coming from. We automatically include information about +your package and nectar, but use this to provide additional details. +Default \code{NULL}.} + +\item{auth_fn}{(\code{function}) A function to use to authenticate the request. By +default (\code{NULL}), no authentication is performed.} + +\item{auth_args}{(\code{list}) An optional list of arguments to the \code{auth_fn} +function.} } \value{ A \code{\link[httr2:request]{httr2::request()}} object. @@ -55,3 +63,7 @@ This function implements an opinionated framework for preparing an API request. It is intended to be used inside an API client package. It serves as a wrapper around the \code{req_} family of functions, such as \code{\link[httr2:request]{httr2::request()}}. } +\seealso{ +\code{\link[=req_init]{req_init()}}, \code{\link[=req_modify]{req_modify()}}, and \code{\link[=do_if_fn_defined]{do_if_fn_defined()}} for finer +control of the process. +} diff --git a/man/req_setup.Rd b/man/req_setup.Rd deleted file mode 100644 index f4603ff..0000000 --- a/man/req_setup.Rd +++ /dev/null @@ -1,34 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/req.R -\name{req_setup} -\alias{req_setup} -\title{Setup a basic API request} -\usage{ -req_setup(base_url, ..., user_agent = "nectar (https://nectar.api2r.org)") -} -\arguments{ -\item{base_url}{The part of the url that is shared by all calls to the API. -In some cases there may be a family of base URLs, from which you will need -to choose one.} - -\item{...}{These dots are for future extensions and must be empty.} - -\item{user_agent}{A string to identify where this request is coming from. -It's polite to set the user agent to identify your package, such as -"MyPackage (https://mypackage.com)".} -} -\value{ -A \code{\link[httr2:request]{httr2::request()}} object. -} -\description{ -For a given API, the \code{base_url} and \code{user_agent} will almost always be the -same. Use this function to prepare that piece of the request once for easy -reuse. -} -\examples{ -req_setup("https://example.com") -req_setup( - "https://example.com", - user_agent = "my_api_client (https://my.api.client)" -) -} diff --git a/man/resp_parse.Rd b/man/resp_parse.Rd index 15103d4..6f2fd92 100644 --- a/man/resp_parse.Rd +++ b/man/resp_parse.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/resp.R +% Please edit documentation in R/resp_parse.R \name{resp_parse} \alias{resp_parse} \alias{resp_parse.default} @@ -18,23 +18,27 @@ resp_parse(resp, ...) \method{resp_parse}{httr2_response}(resp, ..., response_parser = httr2::resp_body_json) } \arguments{ -\item{resp}{A single \code{\link[httr2:response]{httr2::response()}} object (as returned by -\code{\link[httr2:req_perform]{httr2::req_perform()}}) or a list of such objects (as returned by -\code{\link[httr2:req_perform_iterative]{httr2::req_perform_iterative()}}).} +\item{resp}{(\code{httr2_response} or \code{list}) A single \code{\link[httr2:response]{httr2::response()}} object +(as returned by \code{\link[httr2:req_perform]{httr2::req_perform()}}) or a list of such objects (as +returned by \code{\link[httr2:req_perform_iterative]{httr2::req_perform_iterative()}}).} \item{...}{Additional arguments passed on to the \code{response_parser} function (in addition to \code{resp}).} -\item{arg}{An argument name as a string. This argument will be mentioned in -error messages as the input that is at the origin of a problem.} +\item{arg}{(\verb{length-1 character}) An argument name as a string. This argument +will be mentioned in error messages as the input that is at the origin of a +problem.} -\item{call}{The execution environment of a currently running function, e.g. -caller_env(). The function will be mentioned in error messages as the -source of the error. See the call argument of abort() for more information.} +\item{call}{(\code{environment}) The environment from which a function was +called, e.g. \code{\link[rlang:stack]{rlang::caller_env()}} (the default). The environment will be +mentioned in error messages as the source of the error. This argument is +particularly useful for functions that are intended to be called as +utilities inside other functions.} -\item{response_parser}{A function to parse the server response (\code{resp}). -Defaults to \code{\link[httr2:resp_body_raw]{httr2::resp_body_json()}}, since JSON responses are common. Set -this to \code{NULL} to return the raw response from \code{\link[httr2:req_perform]{httr2::req_perform()}}.} +\item{response_parser}{(\code{function}) A function to parse the server response +(\code{resp}). Defaults to \code{\link[httr2:resp_body_raw]{httr2::resp_body_json()}}, since JSON responses are +common. Set this to \code{NULL} to return the raw response from +\code{\link[httr2:req_perform]{httr2::req_perform()}}.} } \value{ The response parsed by the \code{response_parser}. If \code{resp} was a list, diff --git a/man/stabilize_string.Rd b/man/stabilize_string.Rd index 645270d..1bd2035 100644 --- a/man/stabilize_string.Rd +++ b/man/stabilize_string.Rd @@ -16,7 +16,7 @@ stabilize_string( \item{x}{The argument to stabilize.} \item{...}{ - Arguments passed on to \code{\link[stbl:stabilize_chr_scalar]{stbl::stabilize_chr_scalar}} + Arguments passed on to \code{\link[stbl:stabilize_chr]{stbl::stabilize_chr_scalar}} \describe{ \item{\code{x_class}}{Character. The class name of \code{x} to use in error messages. Use this if you remove a special class from \code{x} before checking its coercion, diff --git a/nectar.Rproj b/nectar.Rproj index 69fafd4..d0863d9 100644 --- a/nectar.Rproj +++ b/nectar.Rproj @@ -1,4 +1,5 @@ Version: 1.0 +ProjectId: 1e1e41c7-34b1-473a-a2f3-6936f90d6ed3 RestoreWorkspace: No SaveWorkspace: No diff --git a/principles.md b/principles.md index 0bcba36..74c0958 100644 --- a/principles.md +++ b/principles.md @@ -21,6 +21,6 @@ Function and argument names in this package reflect a tug-of-war between convent - All exported functions must be documented. - Ideally, internal functions should also be documented, with `@keywords internal`. They do not need a full description, but include a one-line description in the "Title" area, and document their parameters. Most of the time, parameters will be documented in internal functions, and then inherited in the calling exported function (see below). Exception: `_impl()` functions do not need their own documentation; the parent function's documentation covers the details in most or all cases. - Use `@inheritParams` liberally. Every parameter should be defined in exactly one place. This makes it much easier to maintain definitions. If you add a new parameter, globally search (ctrl-shift-F in RStudio) for `@param {name}` to make sure it isn't already defined. -- If a parameter is reused but doesn't have a clear "home" (eg, `req` is used in several unrelated functions), define it in the `.shared-parameters` block of `aaa-shared.R`. +- If a parameter is reused but doesn't have a clear "home" (eg, `req` is used in several unrelated functions), define it in the `.shared-params` block of `aaa-shared.R`. - Likewise, if a return value is reused but doesn't have a clear "home" (eg the `httr2::request` objects returned by several functions), define it once in `aaa-shared.R` in a block with `@name .shared-{name}`, where `{name}` concisely describes the return value. - If a choice contradicts with the choice in {httr2}, explain and justify it in documentation. Make sure it is extremely clear when a default is different. diff --git a/tests/testthat/_snaps/req_body.md b/tests/testthat/_snaps/req_prepare.md similarity index 86% rename from tests/testthat/_snaps/req_body.md rename to tests/testthat/_snaps/req_prepare.md index eac192b..b24686d 100644 --- a/tests/testthat/_snaps/req_body.md +++ b/tests/testthat/_snaps/req_prepare.md @@ -2,7 +2,7 @@ Code test_result <- req_prepare(base_url = "https://example.com", body = list(foo = "bar", - baz = fs::path(test_path("img-test.png"))), user_agent = NULL) + baz = fs::path(test_path("fixtures", "img-test.png")))) test_result$body Output $data diff --git a/tests/testthat/_snaps/resp.md b/tests/testthat/_snaps/resp_parse.md similarity index 100% rename from tests/testthat/_snaps/resp.md rename to tests/testthat/_snaps/resp_parse.md diff --git a/tests/testthat/img-test.png b/tests/testthat/fixtures/img-test.png similarity index 100% rename from tests/testthat/img-test.png rename to tests/testthat/fixtures/img-test.png diff --git a/tests/testthat/test-call.R b/tests/testthat/test-call_api.R similarity index 79% rename from tests/testthat/test-call.R rename to tests/testthat/test-call_api.R index 8edb0de..b51b132 100644 --- a/tests/testthat/test-call.R +++ b/tests/testthat/test-call_api.R @@ -6,24 +6,20 @@ test_that("call_api() calls an API", { } ) expect_no_error({ - test_result <- call_api( - base_url = "https://example.com", - user_agent = NULL - ) + test_result <- call_api(base_url = "https://example.com") }) expect_true(test_result) }) -test_that("call_api() applies security", { +test_that("call_api() applies auth", { local_mocked_bindings( req_perform_opinionated = function(req, ...) req, resp_parse = function(resp, ...) resp ) test_result <- call_api( base_url = "https://example.com", - user_agent = NULL, - security_fn = httr2::req_url_query, - security_args = list( + auth_fn = httr2::req_url_query, + auth_args = list( security = "set" ) ) @@ -44,7 +40,6 @@ test_that("call_api() uses response_parser", { } test_result <- call_api( base_url = "https://example.com", - user_agent = NULL, response_parser = parser ) expect_true(test_result) diff --git a/tests/testthat/test-req.R b/tests/testthat/test-req.R deleted file mode 100644 index 78f7078..0000000 --- a/tests/testthat/test-req.R +++ /dev/null @@ -1,58 +0,0 @@ -test_that("req_prepare() deals with paths.", { - test_result <- req_prepare( - base_url = "https://example.com", - path = "foo/bar", - user_agent = NULL - ) - expect_identical( - test_result$url, - "https://example.com/foo/bar" - ) - - test_result <- req_prepare( - base_url = "https://example.com", - path = list( - "foo/{bar}", - bar = "baz" - ), - user_agent = NULL - ) - expect_identical( - test_result$url, - "https://example.com/foo/baz" - ) -}) - -test_that("req_prepare() applies methods", { - test_result <- req_prepare( - base_url = "https://example.com", - method = "PATCH", - user_agent = NULL - ) - expect_identical( - test_result$method, - "PATCH" - ) - test_result <- req_prepare( - base_url = "https://example.com", - user_agent = NULL - ) - expect_null(test_result$method) - test_result <- req_prepare( - base_url = "https://example.com", - body = list(a = 1), - user_agent = NULL - ) - expect_null(test_result$method) -}) - -test_that("req_prepare() applies user_agent", { - test_result <- req_prepare( - base_url = "https://example.com", - user_agent = "foo" - ) - expect_identical( - test_result$options$useragent, - "foo" - ) -}) diff --git a/tests/testthat/test-auth.R b/tests/testthat/test-req_auth_api_key.R similarity index 100% rename from tests/testthat/test-auth.R rename to tests/testthat/test-req_auth_api_key.R diff --git a/tests/testthat/test-req_body.R b/tests/testthat/test-req_body.R index 8fafacd..e69de29 100644 --- a/tests/testthat/test-req_body.R +++ b/tests/testthat/test-req_body.R @@ -1,29 +0,0 @@ -test_that("req_prepare() uses body parameters", { - test_result <- req_prepare( - base_url = "https://example.com", - body = list( - foo = "bar", - baz = "qux" - ), - user_agent = NULL - ) - expect_identical( - test_result$body$data, - list(foo = "bar", baz = "qux") - ) -}) - -test_that("bodies with paths are handled properly", { - expect_snapshot({ - test_result <- req_prepare( - base_url = "https://example.com", - body = list( - foo = "bar", - baz = fs::path(test_path("img-test.png")) - ), - user_agent = NULL - ) - test_result$body - }) - expect_identical(test_result$body$type, "multipart") -}) diff --git a/tests/testthat/test-perform.R b/tests/testthat/test-req_perform_opinionated.R similarity index 100% rename from tests/testthat/test-perform.R rename to tests/testthat/test-req_perform_opinionated.R diff --git a/tests/testthat/test-req_pkg_user_agent.R b/tests/testthat/test-req_pkg_user_agent.R new file mode 100644 index 0000000..7e433bb --- /dev/null +++ b/tests/testthat/test-req_pkg_user_agent.R @@ -0,0 +1,45 @@ +test_that("req_pkg_user_agent adds a package agent", { + req <- httr2::request("https://example.com") + test_result <- req_pkg_user_agent(req, pkg_name = "stbl") + # Scrub versions but make sure they're there. + test_agent <- stringr::str_replace_all( + test_result$options$useragent, + r"(\d+(\.\d+){2,3})", + "x.x.x" + ) + expect_identical( + test_agent, + "httr2/x.x.x r-curl/x.x.x libcurl/x.x.x nectar/x.x.x (https://nectar.api2r.org) stbl/x.x.x" + ) +}) + +test_that("req_pkg_user_agent adds a package agent and url", { + req <- httr2::request("https://example.com") + test_result <- req_pkg_user_agent( + req, + pkg_name = "stbl", + pkg_url = "https://stbl.api2r.org" + ) + # Scrub versions but make sure they're there. + test_agent <- stringr::str_replace_all( + test_result$options$useragent, + r"(\d+(\.\d+){2,3})", + "x.x.x" + ) + expect_identical( + test_agent, + "httr2/x.x.x r-curl/x.x.x libcurl/x.x.x nectar/x.x.x (https://nectar.api2r.org) stbl/x.x.x (https://stbl.api2r.org)" + ) +}) + +test_that("Corner case of no supplied name works.", { + existing <- stringi::stri_rand_strings(1, 10) + expect_identical( + .lib_user_agent_append(existing, name = NULL, version = "x"), + existing + ) +}) + +test_that("Corner case of no supplied agent works.", { + expect_null(.user_agent_remove(NULL, NULL)) +}) diff --git a/tests/testthat/test-req_prepare.R b/tests/testthat/test-req_prepare.R new file mode 100644 index 0000000..ecd43c7 --- /dev/null +++ b/tests/testthat/test-req_prepare.R @@ -0,0 +1,124 @@ +test_that("req_prepare() applies user agent", { + test_result <- req_prepare( + base_url = "https://example.com", + additional_user_agent = "foo" + ) + expect_identical( + test_result$options$useragent, + "foo nectar/0.0.0.9003 (https://nectar.api2r.org)" + ) +}) + +test_that("req_prepare() deals with paths.", { + test_result <- req_prepare( + base_url = "https://example.com", + path = "foo/bar" + ) + expect_identical( + test_result$url, + "https://example.com/foo/bar" + ) + + test_result <- req_prepare( + base_url = "https://example.com", + path = list( + "foo/{bar}", + bar = "baz" + ) + ) + expect_identical( + test_result$url, + "https://example.com/foo/baz" + ) +}) + +test_that("req_prepare() uses query parameters", { + test_result <- req_prepare( + base_url = "https://example.com", + query = list( + foo = "bar", + baz = "qux" + ) + ) + expect_identical( + url_normalize(test_result$url), + "https://example.com/?foo=bar&baz=qux" + ) +}) + +test_that("req_prepare() uses the .multi arg", { + test_result <- req_prepare( + base_url = "https://example.com", + query = list( + foo = "bar", + baz = c("qux", "quux"), + .multi = "comma" + ) + ) + expect_identical( + url_normalize(test_result$url), + "https://example.com/?foo=bar&baz=qux%2Cquux" + ) +}) + +test_that("req_prepare() removes empty query parameters", { + test_result <- req_prepare( + base_url = "https://example.com", + query = list( + foo = NULL, + bar = "baz" + ) + ) + expect_identical( + url_normalize(test_result$url), + "https://example.com/?bar=baz" + ) +}) + +test_that("req_prepare() uses body parameters", { + test_result <- req_prepare( + base_url = "https://example.com", + body = list( + foo = "bar", + baz = "qux" + ) + ) + expect_identical( + test_result$body$data, + list(foo = "bar", baz = "qux") + ) +}) + +test_that("bodies with paths are handled properly", { + expect_snapshot({ + test_result <- req_prepare( + base_url = "https://example.com", + body = list( + foo = "bar", + baz = fs::path(test_path("fixtures", "img-test.png")) + ) + ) + test_result$body + }) + expect_identical(test_result$body$type, "multipart") +}) + +test_that("req_prepare() applies methods", { + test_result <- req_prepare( + base_url = "https://example.com", + method = "PATCH" + ) + expect_identical( + test_result$method, + "PATCH" + ) + test_result <- req_prepare( + base_url = "https://example.com" + ) + expect_null(test_result$method) + test_result <- req_prepare( + base_url = "https://example.com", + body = list(a = 1) + ) + expect_null(test_result$method) +}) diff --git a/tests/testthat/test-req_query.R b/tests/testthat/test-req_query.R deleted file mode 100644 index d860467..0000000 --- a/tests/testthat/test-req_query.R +++ /dev/null @@ -1,45 +0,0 @@ -test_that("req_prepare() uses query parameters", { - test_result <- req_prepare( - base_url = "https://example.com", - query = list( - foo = "bar", - baz = "qux" - ), - user_agent = NULL - ) - expect_identical( - url_normalize(test_result$url), - "https://example.com/?foo=bar&baz=qux" - ) -}) - -test_that("req_prepare() uses the .multi arg", { - test_result <- req_prepare( - base_url = "https://example.com", - query = list( - foo = "bar", - baz = c("qux", "quux"), - .multi = "comma" - ), - user_agent = NULL - ) - expect_identical( - url_normalize(test_result$url), - "https://example.com/?foo=bar&baz=qux%2Cquux" - ) -}) - -test_that("req_prepare() removes empty query parameters", { - test_result <- req_prepare( - base_url = "https://example.com", - query = list( - foo = NULL, - bar = "baz" - ), - user_agent = NULL - ) - expect_identical( - url_normalize(test_result$url), - "https://example.com/?bar=baz" - ) -}) diff --git a/tests/testthat/test-resp.R b/tests/testthat/test-resp_parse.R similarity index 100% rename from tests/testthat/test-resp.R rename to tests/testthat/test-resp_parse.R