From ed09b79b4a0e7e7e75644768e735f6df1277349a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 13:22:13 +0000 Subject: [PATCH 1/5] Initial plan From b33892b8edb72417fe49e7b11798a3d6fa3339fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 13:28:35 +0000 Subject: [PATCH 2/5] feat: add reusable auth_prepare/auth_api_key and req_prepare auth arg Agent-Logs-Url: https://github.com/api2r/nectar/sessions/455572f3-7618-454f-a623-887f0fdd8f83 Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> --- NAMESPACE | 2 + R/aaa-shared_params.R | 6 +-- R/auth_prepare.R | 39 ++++++++++++++++++ R/req_auth_api_key.R | 43 +++++++++++++++++++- R/req_prepare.R | 6 +-- man/auth_api_key.Rd | 55 ++++++++++++++++++++++++++ man/auth_prepare.Rd | 40 +++++++++++++++++++ man/dot-shared-params.Rd | 7 +--- man/req_auth_api_key.Rd | 3 +- man/req_init.Rd | 2 + man/req_modify.Rd | 2 + man/req_pagination_policy.Rd | 2 + man/req_prepare.Rd | 12 +++--- man/req_tidy_policy.Rd | 2 + tests/testthat/test-req_auth_api_key.R | 24 +++++++++++ tests/testthat/test-req_prepare.R | 21 ++++++++++ vignettes/nectar.Rmd | 10 ++--- 17 files changed, 249 insertions(+), 27 deletions(-) create mode 100644 R/auth_prepare.R create mode 100644 man/auth_api_key.Rd create mode 100644 man/auth_prepare.Rd diff --git a/NAMESPACE b/NAMESPACE index feedca9..685ad2e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -12,6 +12,8 @@ S3method(resp_tidy,default) S3method(resp_tidy,httr2_response) S3method(resp_tidy,list) S3method(resp_tidy,nectar_responses) +export(auth_api_key) +export(auth_prepare) export(choose_pagination_fn) export(compact_nested_list) export(do_if_fn_defined) diff --git a/R/aaa-shared_params.R b/R/aaa-shared_params.R index b82cd1e..6d64f4c 100644 --- a/R/aaa-shared_params.R +++ b/R/aaa-shared_params.R @@ -10,10 +10,8 @@ #' @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 auth (`nectar_auth` or `NULL`) Authentication prepared with +#' [auth_prepare()]. 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. diff --git a/R/auth_prepare.R b/R/auth_prepare.R new file mode 100644 index 0000000..89c42cb --- /dev/null +++ b/R/auth_prepare.R @@ -0,0 +1,39 @@ +#' Prepare authentication independent of a request +#' +#' This constructor stores an authentication function and arguments so the same +#' authentication strategy can be reused across requests. +#' +#' @inheritParams .shared-params +#' @inheritParams rlang::args_dots_empty +#' @param auth_fn (`function`) A function to use to authenticate a request. +#' @returns A list with class `"nectar_auth"` and elements `auth_fn` and +#' `auth_args`. +#' @family opinionated request functions +#' @export +#' +#' @examples +#' auth_prepare(req_auth_api_key, "X-API-Key", api_key = "my-api-key") +auth_prepare <- function(auth_fn, ..., call = rlang::caller_env()) { + auth_fn <- rlang::as_function(auth_fn, call = call) + structure( + list(auth_fn = auth_fn, auth_args = rlang::list2(...)), + class = "nectar_auth" + ) +} + +.as_nectar_auth <- function(auth, call = rlang::caller_env()) { + if (is.null(auth)) { + return(list(auth_fn = NULL, auth_args = list())) + } + if (inherits(auth, "nectar_auth")) { + return(auth) + } + .nectar_abort( + c( + "{.arg {auth}} must be `NULL` or a {.cls nectar_auth}.", + x = "{.arg {auth}} is {.obj_type_friendly {auth}}." + ), + subclass = "unsupported_auth_class", + call = call + ) +} diff --git a/R/req_auth_api_key.R b/R/req_auth_api_key.R index 839a88d..19644b1 100644 --- a/R/req_auth_api_key.R +++ b/R/req_auth_api_key.R @@ -10,7 +10,8 @@ #' in the header, query, or cookie. #' @param api_key (`length-1 character` or `NULL`) The API key to use. If this #' value is `NULL`, the key will be removed from the request. If this value is -#' `NA` or an empty string, `req` is returned unchanged. +#' `NA` or an empty string, the request is returned unchanged when the +#' prepared auth is applied. #' @param location (`length-1 character`) Where the API key should be passed. #' One of `"header"` (default), `"query"`, or `"cookie"`. #' @@ -60,3 +61,43 @@ req_auth_api_key <- function( req <- rlang::exec(req_api_key_set, req, !!parameter_name := api_key) return(req) } + +#' Prepare API key authentication independent of a request +#' +#' This helper creates a reusable authentication object that can be passed to +#' [req_prepare()] via `auth`. +#' +#' @inheritParams .shared-params +#' @inheritParams rlang::args_dots_empty +#' @param parameter_name (`length-1 character`) The name of the parameter to use +#' in the header, query, or cookie. +#' @param api_key (`length-1 character` or `NULL`) The API key to use. If this +#' value is `NULL`, the key will be removed from the request. If this value is +#' `NA` or an empty string, the request is returned unchanged when the +#' prepared auth is applied. +#' @param location (`length-1 character`) Where the API key should be passed. +#' One of `"header"` (default), `"query"`, or `"cookie"`. +#' @returns A list with class `"nectar_auth"` and elements `auth_fn` and +#' `auth_args`. +#' @family opinionated request functions +#' @export +#' +#' @examples +#' auth_api_key("X-API-Key", api_key = "my-api-key") +auth_api_key <- function( + parameter_name, + ..., + api_key = NULL, + location = c("header", "query", "cookie"), + call = rlang::caller_env() +) { + rlang::check_dots_empty(call = call) + location <- rlang::arg_match(location, error_call = call) + auth_prepare( + req_auth_api_key, + parameter_name = parameter_name, + api_key = api_key, + location = location, + call = call + ) +} diff --git a/R/req_prepare.R b/R/req_prepare.R index 0144768..f716050 100644 --- a/R/req_prepare.R +++ b/R/req_prepare.R @@ -26,8 +26,7 @@ req_prepare <- function( mime_type = NULL, method = NULL, additional_user_agent = NULL, - auth_fn = NULL, - auth_args = list(), + auth = NULL, tidy_fn = NULL, tidy_args = list(), pagination_fn = NULL, @@ -48,7 +47,8 @@ req_prepare <- function( method = method, call = call ) - req <- do_if_fn_defined(req, auth_fn, !!!auth_args, call = call) + auth <- .as_nectar_auth(auth, call = call) + req <- do_if_fn_defined(req, auth$auth_fn, !!!auth$auth_args, call = call) if (length(pagination_fn)) { req <- req_pagination_policy(req, pagination_fn, call = call) } diff --git a/man/auth_api_key.Rd b/man/auth_api_key.Rd new file mode 100644 index 0000000..79441dd --- /dev/null +++ b/man/auth_api_key.Rd @@ -0,0 +1,55 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/req_auth_api_key.R +\name{auth_api_key} +\alias{auth_api_key} +\title{Prepare API key authentication independent of a request} +\usage{ +auth_api_key( + parameter_name, + ..., + api_key = NULL, + location = c("header", "query", "cookie"), + call = rlang::caller_env() +) +} +\arguments{ +\item{parameter_name}{(\verb{length-1 character}) The name of the parameter to use +in the header, query, or cookie.} + +\item{...}{These dots are for future extensions and must be empty.} + +\item{api_key}{(\verb{length-1 character} or \code{NULL}) The API key to use. If this +value is \code{NULL}, the key will be removed from the request. If this value is +\code{NA} or an empty string, the request is returned unchanged when the +prepared auth is applied.} + +\item{location}{(\verb{length-1 character}) Where the API key should be passed. +One of \code{"header"} (default), \code{"query"}, or \code{"cookie"}.} + +\item{call}{(\code{environment}) The environment from which a function was called, +e.g. \code{\link[rlang:caller_env]{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 list with class \code{"nectar_auth"} and elements \code{auth_fn} and +\code{auth_args}. +} +\description{ +This helper creates a reusable authentication object that can be passed to +\code{\link[=req_prepare]{req_prepare()}} via \code{auth}. +} +\examples{ +auth_api_key("X-API-Key", api_key = "my-api-key") +} +\seealso{ +Other opinionated request functions: +\code{\link[=auth_prepare]{auth_prepare()}}, +\code{\link[=req_init]{req_init()}}, +\code{\link[=req_modify]{req_modify()}}, +\code{\link[=req_pagination_policy]{req_pagination_policy()}}, +\code{\link[=req_prepare]{req_prepare()}}, +\code{\link[=req_tidy_policy]{req_tidy_policy()}} +} +\concept{opinionated request functions} diff --git a/man/auth_prepare.Rd b/man/auth_prepare.Rd new file mode 100644 index 0000000..369e103 --- /dev/null +++ b/man/auth_prepare.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/auth_prepare.R +\name{auth_prepare} +\alias{auth_prepare} +\title{Prepare authentication independent of a request} +\usage{ +auth_prepare(auth_fn, ..., call = rlang::caller_env()) +} +\arguments{ +\item{auth_fn}{(\code{function}) A function to use to authenticate a request.} + +\item{...}{These dots are for future extensions and must be empty.} + +\item{call}{(\code{environment}) The environment from which a function was called, +e.g. \code{\link[rlang:caller_env]{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 list with class \code{"nectar_auth"} and elements \code{auth_fn} and +\code{auth_args}. +} +\description{ +This constructor stores an authentication function and arguments so the same +authentication strategy can be reused across requests. +} +\examples{ +auth_prepare(req_auth_api_key, "X-API-Key", api_key = "my-api-key") +} +\seealso{ +Other opinionated request functions: +\code{\link[=auth_api_key]{auth_api_key()}}, +\code{\link[=req_init]{req_init()}}, +\code{\link[=req_modify]{req_modify()}}, +\code{\link[=req_pagination_policy]{req_pagination_policy()}}, +\code{\link[=req_prepare]{req_prepare()}}, +\code{\link[=req_tidy_policy]{req_tidy_policy()}} +} +\concept{opinionated request functions} diff --git a/man/dot-shared-params.Rd b/man/dot-shared-params.Rd index ecb9631..b617565 100644 --- a/man/dot-shared-params.Rd +++ b/man/dot-shared-params.Rd @@ -15,11 +15,8 @@ Default \code{NULL}.} 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{auth}{(\code{nectar_auth} or \code{NULL}) Authentication prepared with +\code{\link[=auth_prepare]{auth_prepare()}}. 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, diff --git a/man/req_auth_api_key.Rd b/man/req_auth_api_key.Rd index 98371af..43d118f 100644 --- a/man/req_auth_api_key.Rd +++ b/man/req_auth_api_key.Rd @@ -23,7 +23,8 @@ in the header, query, or cookie.} \item{api_key}{(\verb{length-1 character} or \code{NULL}) The API key to use. If this value is \code{NULL}, the key will be removed from the request. If this value is -\code{NA} or an empty string, \code{req} is returned unchanged.} +\code{NA} or an empty string, the request is returned unchanged when the +prepared auth is applied.} \item{location}{(\verb{length-1 character}) Where the API key should be passed. One of \code{"header"} (default), \code{"query"}, or \code{"cookie"}.} diff --git a/man/req_init.Rd b/man/req_init.Rd index f30dfbc..bd03e2e 100644 --- a/man/req_init.Rd +++ b/man/req_init.Rd @@ -46,6 +46,8 @@ req_init( } \seealso{ Other opinionated request functions: +\code{\link[=auth_api_key]{auth_api_key()}}, +\code{\link[=auth_prepare]{auth_prepare()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, \code{\link[=req_prepare]{req_prepare()}}, diff --git a/man/req_modify.Rd b/man/req_modify.Rd index 3b4dfd8..be7bfc1 100644 --- a/man/req_modify.Rd +++ b/man/req_modify.Rd @@ -60,6 +60,8 @@ req_modify(req_base, query = c("param1" = "value1", "param2" = "value2")) } \seealso{ Other opinionated request functions: +\code{\link[=auth_api_key]{auth_api_key()}}, +\code{\link[=auth_prepare]{auth_prepare()}}, \code{\link[=req_init]{req_init()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, \code{\link[=req_prepare]{req_prepare()}}, diff --git a/man/req_pagination_policy.Rd b/man/req_pagination_policy.Rd index 41bfdb6..14ed28c 100644 --- a/man/req_pagination_policy.Rd +++ b/man/req_pagination_policy.Rd @@ -40,6 +40,8 @@ req_pagination_policy(req, httr2::iterate_with_offset("page")) } \seealso{ Other opinionated request functions: +\code{\link[=auth_api_key]{auth_api_key()}}, +\code{\link[=auth_prepare]{auth_prepare()}}, \code{\link[=req_init]{req_init()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_prepare]{req_prepare()}}, diff --git a/man/req_prepare.Rd b/man/req_prepare.Rd index cd1a159..fb41496 100644 --- a/man/req_prepare.Rd +++ b/man/req_prepare.Rd @@ -13,8 +13,7 @@ req_prepare( mime_type = NULL, method = NULL, additional_user_agent = NULL, - auth_fn = NULL, - auth_args = list(), + auth = NULL, tidy_fn = NULL, tidy_args = list(), pagination_fn = NULL, @@ -53,11 +52,8 @@ 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.} +\item{auth}{(\code{nectar_auth} or \code{NULL}) Authentication prepared with +\code{\link[=auth_prepare]{auth_prepare()}}. By default (\code{NULL}), no authentication is performed.} \item{tidy_fn}{(\code{function}) A function that will be invoked by \code{\link[=resp_tidy]{resp_tidy()}} to tidy the response.} @@ -97,6 +93,8 @@ req_prepare( } \seealso{ Other opinionated request functions: +\code{\link[=auth_api_key]{auth_api_key()}}, +\code{\link[=auth_prepare]{auth_prepare()}}, \code{\link[=req_init]{req_init()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, diff --git a/man/req_tidy_policy.Rd b/man/req_tidy_policy.Rd index 5bf17af..fac0dde 100644 --- a/man/req_tidy_policy.Rd +++ b/man/req_tidy_policy.Rd @@ -40,6 +40,8 @@ req_tidy_policy(req, httr2::resp_body_json, list(simplifyVector = TRUE)) } \seealso{ Other opinionated request functions: +\code{\link[=auth_api_key]{auth_api_key()}}, +\code{\link[=auth_prepare]{auth_prepare()}}, \code{\link[=req_init]{req_init()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, diff --git a/tests/testthat/test-req_auth_api_key.R b/tests/testthat/test-req_auth_api_key.R index 31d7145..f6e92c6 100644 --- a/tests/testthat/test-req_auth_api_key.R +++ b/tests/testthat/test-req_auth_api_key.R @@ -12,6 +12,30 @@ test_that("req_auth_api_key errors informatively with unused arguments", { ) }) +test_that("auth_prepare() constructs nectar_auth objects", { + test_result <- auth_prepare(req_auth_api_key, "parm", api_key = "my_key") + expect_s3_class(test_result, "nectar_auth") + expect_identical(test_result$auth_fn, req_auth_api_key) + expect_identical( + test_result$auth_args, + list("parm", api_key = "my_key") + ) +}) + +test_that("auth_api_key() prepares req_auth_api_key auth", { + test_result <- auth_api_key( + parameter_name = "parm", + api_key = "my_key", + location = "query" + ) + expect_s3_class(test_result, "nectar_auth") + expect_identical(test_result$auth_fn, req_auth_api_key) + expect_identical( + test_result$auth_args[c("parameter_name", "api_key", "location")], + list(parameter_name = "parm", api_key = "my_key", location = "query") + ) +}) + test_that("req_auth_api_key returns req unchanged if api_key is NA or empty (#76)", { req <- httr2::request("https://example.com") expect_identical(req, req_auth_api_key(req, "parm", api_key = NA_character_)) diff --git a/tests/testthat/test-req_prepare.R b/tests/testthat/test-req_prepare.R index bde0bfd..c3746f6 100644 --- a/tests/testthat/test-req_prepare.R +++ b/tests/testthat/test-req_prepare.R @@ -153,6 +153,27 @@ test_that("req_prepare() applies tidying", { ) }) +test_that("req_prepare() applies prepared auth", { + test_result <- req_prepare( + base_url = "https://example.com", + auth = auth_prepare(req_auth_api_key, "parm", api_key = "my_key") + ) + expect_in( + names(test_result$headers), + "parm" + ) +}) + +test_that("req_prepare() errors for unsupported auth objects", { + expect_error( + req_prepare( + base_url = "https://example.com", + auth = list(auth_fn = req_auth_api_key, auth_args = list("parm")) + ), + class = "nectar-error-unsupported_auth_class" + ) +}) + test_that(".as_nectar_request() fails gracefully for non-reqs", { test_obj <- 1 expect_nectar_error_snapshot( diff --git a/vignettes/nectar.Rmd b/vignettes/nectar.Rmd index ff90426..85d8231 100644 --- a/vignettes/nectar.Rmd +++ b/vignettes/nectar.Rmd @@ -27,9 +27,9 @@ The main entry point in nectar is `req_prepare()`. It wraps `httr2::request()` a Here we prepare a request to the Crossref `/works` endpoint. We ask for ten results per page (`rows = 10`), select only the "publisher" and "DOI" fields, tell `{httr2}` to concatenate the `select` parameter with commas (`.multi`), and set the `cursor` parameter to `"*"` to trigger cursor-based pagination: -## Authentication with `req_auth_api_key()` +## Authentication with `auth_api_key()` -Many APIs accept an optional key (or, as in Crossref's case, an email address) to identify your application and gain access to a higher rate limit. nectar provides `req_auth_api_key()` for this purpose. You can pass it through `req_prepare()` via the `auth_fn` and `auth_args` arguments: +Many APIs accept an optional key (or, as in Crossref's case, an email address) to identify your application and gain access to a higher rate limit. nectar provides `auth_api_key()` to prepare this authentication and pass it through `req_prepare()` via the `auth` argument: ```{r auth, eval = FALSE} req <- req_prepare( @@ -37,8 +37,7 @@ req <- req_prepare( query = list( rows = 10, cursor = "*", select = c("publisher", "DOI"), .multi = "comma" ), - auth_fn = req_auth_api_key, - auth_args = list("mailto", api_key = "your@email.com", location = "query") + auth = auth_api_key("mailto", api_key = "your@email.com", location = "query") ) ``` @@ -171,8 +170,7 @@ works <- function( req_prepare( "https://api.crossref.org/works", query = list(rows = rows, cursor = "*", select = select), - auth_fn = req_auth_api_key, - auth_args = list("mailto", api_key = mailto, location = "query"), + auth = auth_api_key("mailto", api_key = mailto, location = "query"), tidy_fn = resp_tidy_json, tidy_args = list(subset_path = c("message", "items")), pagination_fn = iterate_with_json_cursor( From 6fb88f8a27b1220f3234586cda6a75e6edde2452 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 09:47:46 -0500 Subject: [PATCH 3/5] Add reusable auth objects and `auth` arg to `req_prepare()` (#84) * Initial plan * fix: address all PR #83 review comments - Refactor .as_nectar_auth() to S3 generic with methods for nectar_auth, NULL, list, and default; auth_prepare() delegates to .as_nectar_auth() - Change @family to 'opinionated auth functions' in auth_prepare(), auth_api_key(), and req_auth_api_key() - Move parameter_name, api_key, location param docs to aaa-shared_params.R and use @inheritParams .shared-params in both req_auth_api_key() and auth_api_key() - Move auth_prepare() test to new test-auth_prepare.R with (#81) tag - Add (#81) to auth_api_key() test name in test-req_auth_api_key.R - Add (#81) to both req_prepare() auth tests in test-req_prepare.R - Update unsupported-auth error test to use a genuinely unsupported type Agent-Logs-Url: https://github.com/api2r/nectar/sessions/c74d2ebb-3b2a-49b0-95fc-30fac727276d Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> * fix: req_auth_api_key in both opinionated auth and request function families Agent-Logs-Url: https://github.com/api2r/nectar/sessions/77aaa079-c761-4263-ab39-3001b526e1b3 Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> * Update R/auth_prepare.R Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Signed-off-by: Jon Harmon --------- Signed-off-by: Jon Harmon Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> Co-authored-by: Jon Harmon Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- NAMESPACE | 4 +++ R/aaa-shared_params.R | 10 +++++-- R/auth_prepare.R | 39 ++++++++++++++++++++++---- R/req_auth_api_key.R | 19 ++----------- man/auth_api_key.Rd | 11 ++------ man/auth_prepare.Rd | 11 ++------ man/dot-shared-params.Rd | 11 ++++++-- man/req_auth_api_key.Rd | 1 + man/req_init.Rd | 2 -- man/req_modify.Rd | 2 -- man/req_pagination_policy.Rd | 2 -- man/req_prepare.Rd | 2 -- man/req_tidy_policy.Rd | 2 -- tests/testthat/test-auth_prepare.R | 9 ++++++ tests/testthat/test-req_auth_api_key.R | 12 +------- tests/testthat/test-req_prepare.R | 6 ++-- 16 files changed, 76 insertions(+), 67 deletions(-) create mode 100644 tests/testthat/test-auth_prepare.R diff --git a/NAMESPACE b/NAMESPACE index 685ad2e..debb939 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,6 +2,10 @@ S3method(.add_body,json) S3method(.add_body,multipart) +S3method(.as_nectar_auth,"NULL") +S3method(.as_nectar_auth,default) +S3method(.as_nectar_auth,list) +S3method(.as_nectar_auth,nectar_auth) S3method(.as_nectar_request,default) S3method(.as_nectar_request,httr2_request) S3method(.as_nectar_request,nectar_request) diff --git a/R/aaa-shared_params.R b/R/aaa-shared_params.R index 6d64f4c..6cb7e53 100644 --- a/R/aaa-shared_params.R +++ b/R/aaa-shared_params.R @@ -6,7 +6,10 @@ #' 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 api_key (`length-1 character` or `NULL`) The API key to use. If this +#' value is `NULL`, the key will be removed from the request. If this value is +#' `NA` or an empty string, the request is returned unchanged when the +#' prepared auth is applied. #' @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. @@ -33,6 +36,8 @@ #' 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 location (`length-1 character`) Where the API key should be passed. +#' One of `"header"` (default), `"query"`, or `"cookie"`. #' @param name (`length-1 character`) The name of a package or other thing to #' add to or remove from the user agent string. #' @param pagination_fn (`function`) A function that takes the previous response @@ -42,7 +47,8 @@ #' [httr2::iterate_with_offset()]. This function will be extracted from the #' request by [req_perform_opinionated()] and passed on as `next_req` to #' [httr2::req_perform_iterative()]. -#' @param parameter_name (`length-1 character`) The name to use for the API key. +#' @param parameter_name (`length-1 character`) The name of the parameter to use +#' in the header, query, or cookie. #' @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 diff --git a/R/auth_prepare.R b/R/auth_prepare.R index 89c42cb..2627b75 100644 --- a/R/auth_prepare.R +++ b/R/auth_prepare.R @@ -8,26 +8,53 @@ #' @param auth_fn (`function`) A function to use to authenticate a request. #' @returns A list with class `"nectar_auth"` and elements `auth_fn` and #' `auth_args`. -#' @family opinionated request functions +#' @family opinionated auth functions #' @export #' #' @examples #' auth_prepare(req_auth_api_key, "X-API-Key", api_key = "my-api-key") auth_prepare <- function(auth_fn, ..., call = rlang::caller_env()) { auth_fn <- rlang::as_function(auth_fn, call = call) - structure( + .as_nectar_auth( list(auth_fn = auth_fn, auth_args = rlang::list2(...)), - class = "nectar_auth" + call = call ) } .as_nectar_auth <- function(auth, call = rlang::caller_env()) { - if (is.null(auth)) { - return(list(auth_fn = NULL, auth_args = list())) + UseMethod(".as_nectar_auth") +} + +#' @export +.as_nectar_auth.nectar_auth <- function(auth, call = rlang::caller_env()) { + return(auth) +} + +#' @export +.as_nectar_auth.NULL <- function(auth, call = rlang::caller_env()) { + return(list(auth_fn = NULL, auth_args = list())) +} + +#' @export +.as_nectar_auth.list <- function(auth, call = rlang::caller_env()) { + if (!("auth_fn" %in% names(auth))) { + return(NextMethod()) } - if (inherits(auth, "nectar_auth")) { + if (setequal(names(auth), c("auth_fn", "auth_args"))) { + class(auth) <- "nectar_auth" return(auth) } + structure( + list( + auth_fn = auth$auth_fn, + auth_args = auth[setdiff(names(auth), "auth_fn")] + ), + class = "nectar_auth" + ) +} + +#' @export +.as_nectar_auth.default <- function(auth, call = rlang::caller_env()) { .nectar_abort( c( "{.arg {auth}} must be `NULL` or a {.cls nectar_auth}.", diff --git a/R/req_auth_api_key.R b/R/req_auth_api_key.R index 19644b1..b384b15 100644 --- a/R/req_auth_api_key.R +++ b/R/req_auth_api_key.R @@ -6,16 +6,9 @@ #' #' @inheritParams .shared-params #' @inheritParams rlang::args_dots_empty -#' @param parameter_name (`length-1 character`) The name of the parameter to use -#' in the header, query, or cookie. -#' @param api_key (`length-1 character` or `NULL`) The API key to use. If this -#' value is `NULL`, the key will be removed from the request. If this value is -#' `NA` or an empty string, the request is returned unchanged when the -#' prepared auth is applied. -#' @param location (`length-1 character`) Where the API key should be passed. -#' One of `"header"` (default), `"query"`, or `"cookie"`. #' #' @inherit .shared-request return +#' @family opinionated auth functions, opinionated request functions #' @export #' #' @examples @@ -69,17 +62,9 @@ req_auth_api_key <- function( #' #' @inheritParams .shared-params #' @inheritParams rlang::args_dots_empty -#' @param parameter_name (`length-1 character`) The name of the parameter to use -#' in the header, query, or cookie. -#' @param api_key (`length-1 character` or `NULL`) The API key to use. If this -#' value is `NULL`, the key will be removed from the request. If this value is -#' `NA` or an empty string, the request is returned unchanged when the -#' prepared auth is applied. -#' @param location (`length-1 character`) Where the API key should be passed. -#' One of `"header"` (default), `"query"`, or `"cookie"`. #' @returns A list with class `"nectar_auth"` and elements `auth_fn` and #' `auth_args`. -#' @family opinionated request functions +#' @family opinionated auth functions #' @export #' #' @examples diff --git a/man/auth_api_key.Rd b/man/auth_api_key.Rd index 79441dd..f1e5377 100644 --- a/man/auth_api_key.Rd +++ b/man/auth_api_key.Rd @@ -44,12 +44,7 @@ This helper creates a reusable authentication object that can be passed to auth_api_key("X-API-Key", api_key = "my-api-key") } \seealso{ -Other opinionated request functions: -\code{\link[=auth_prepare]{auth_prepare()}}, -\code{\link[=req_init]{req_init()}}, -\code{\link[=req_modify]{req_modify()}}, -\code{\link[=req_pagination_policy]{req_pagination_policy()}}, -\code{\link[=req_prepare]{req_prepare()}}, -\code{\link[=req_tidy_policy]{req_tidy_policy()}} +Other opinionated auth functions: +\code{\link[=auth_prepare]{auth_prepare()}} } -\concept{opinionated request functions} +\concept{opinionated auth functions} diff --git a/man/auth_prepare.Rd b/man/auth_prepare.Rd index 369e103..f62f587 100644 --- a/man/auth_prepare.Rd +++ b/man/auth_prepare.Rd @@ -29,12 +29,7 @@ authentication strategy can be reused across requests. auth_prepare(req_auth_api_key, "X-API-Key", api_key = "my-api-key") } \seealso{ -Other opinionated request functions: -\code{\link[=auth_api_key]{auth_api_key()}}, -\code{\link[=req_init]{req_init()}}, -\code{\link[=req_modify]{req_modify()}}, -\code{\link[=req_pagination_policy]{req_pagination_policy()}}, -\code{\link[=req_prepare]{req_prepare()}}, -\code{\link[=req_tidy_policy]{req_tidy_policy()}} +Other opinionated auth functions: +\code{\link[=auth_api_key]{auth_api_key()}} } -\concept{opinionated request functions} +\concept{opinionated auth functions} diff --git a/man/dot-shared-params.Rd b/man/dot-shared-params.Rd index b617565..bd2458c 100644 --- a/man/dot-shared-params.Rd +++ b/man/dot-shared-params.Rd @@ -9,7 +9,10 @@ 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{api_key}{(\verb{length-1 character} or \code{NULL}) The API key to use. If this +value is \code{NULL}, the key will be removed from the request. If this value is +\code{NA} or an empty string, the request is returned unchanged when the +prepared auth is applied.} \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 @@ -46,6 +49,9 @@ 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{location}{(\verb{length-1 character}) Where the API key should be passed. +One of \code{"header"} (default), \code{"query"}, or \code{"cookie"}.} + \item{name}{(\verb{length-1 character}) The name of a package or other thing to add to or remove from the user agent string.} @@ -57,7 +63,8 @@ using one of the iteration helpers described in request by \code{\link[=req_perform_opinionated]{req_perform_opinionated()}} and passed on as \code{next_req} to \code{\link[httr2:req_perform_iterative]{httr2::req_perform_iterative()}}.} -\item{parameter_name}{(\verb{length-1 character}) The name to use for the API key.} +\item{parameter_name}{(\verb{length-1 character}) The name of the parameter to use +in the header, query, or cookie.} \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 diff --git a/man/req_auth_api_key.Rd b/man/req_auth_api_key.Rd index 43d118f..cb97fc5 100644 --- a/man/req_auth_api_key.Rd +++ b/man/req_auth_api_key.Rd @@ -55,3 +55,4 @@ req_auth_api_key(req, "api_key", api_key = "my-api-key", location = "query") # If `api_key` is NULL, the key is removed from the request req_auth_api_key(req, "X-API-Key", api_key = NULL) } +\concept{opinionated auth functions, opinionated request functions} diff --git a/man/req_init.Rd b/man/req_init.Rd index bd03e2e..f30dfbc 100644 --- a/man/req_init.Rd +++ b/man/req_init.Rd @@ -46,8 +46,6 @@ req_init( } \seealso{ Other opinionated request functions: -\code{\link[=auth_api_key]{auth_api_key()}}, -\code{\link[=auth_prepare]{auth_prepare()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, \code{\link[=req_prepare]{req_prepare()}}, diff --git a/man/req_modify.Rd b/man/req_modify.Rd index be7bfc1..3b4dfd8 100644 --- a/man/req_modify.Rd +++ b/man/req_modify.Rd @@ -60,8 +60,6 @@ req_modify(req_base, query = c("param1" = "value1", "param2" = "value2")) } \seealso{ Other opinionated request functions: -\code{\link[=auth_api_key]{auth_api_key()}}, -\code{\link[=auth_prepare]{auth_prepare()}}, \code{\link[=req_init]{req_init()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, \code{\link[=req_prepare]{req_prepare()}}, diff --git a/man/req_pagination_policy.Rd b/man/req_pagination_policy.Rd index 14ed28c..41bfdb6 100644 --- a/man/req_pagination_policy.Rd +++ b/man/req_pagination_policy.Rd @@ -40,8 +40,6 @@ req_pagination_policy(req, httr2::iterate_with_offset("page")) } \seealso{ Other opinionated request functions: -\code{\link[=auth_api_key]{auth_api_key()}}, -\code{\link[=auth_prepare]{auth_prepare()}}, \code{\link[=req_init]{req_init()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_prepare]{req_prepare()}}, diff --git a/man/req_prepare.Rd b/man/req_prepare.Rd index fb41496..01e95a5 100644 --- a/man/req_prepare.Rd +++ b/man/req_prepare.Rd @@ -93,8 +93,6 @@ req_prepare( } \seealso{ Other opinionated request functions: -\code{\link[=auth_api_key]{auth_api_key()}}, -\code{\link[=auth_prepare]{auth_prepare()}}, \code{\link[=req_init]{req_init()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, diff --git a/man/req_tidy_policy.Rd b/man/req_tidy_policy.Rd index fac0dde..5bf17af 100644 --- a/man/req_tidy_policy.Rd +++ b/man/req_tidy_policy.Rd @@ -40,8 +40,6 @@ req_tidy_policy(req, httr2::resp_body_json, list(simplifyVector = TRUE)) } \seealso{ Other opinionated request functions: -\code{\link[=auth_api_key]{auth_api_key()}}, -\code{\link[=auth_prepare]{auth_prepare()}}, \code{\link[=req_init]{req_init()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, diff --git a/tests/testthat/test-auth_prepare.R b/tests/testthat/test-auth_prepare.R new file mode 100644 index 0000000..ee4734c --- /dev/null +++ b/tests/testthat/test-auth_prepare.R @@ -0,0 +1,9 @@ +test_that("auth_prepare() constructs nectar_auth objects (#81)", { + test_result <- auth_prepare(req_auth_api_key, "parm", api_key = "my_key") + expect_s3_class(test_result, "nectar_auth") + expect_identical(test_result$auth_fn, req_auth_api_key) + expect_identical( + test_result$auth_args, + list("parm", api_key = "my_key") + ) +}) diff --git a/tests/testthat/test-req_auth_api_key.R b/tests/testthat/test-req_auth_api_key.R index f6e92c6..03e00aa 100644 --- a/tests/testthat/test-req_auth_api_key.R +++ b/tests/testthat/test-req_auth_api_key.R @@ -12,17 +12,7 @@ test_that("req_auth_api_key errors informatively with unused arguments", { ) }) -test_that("auth_prepare() constructs nectar_auth objects", { - test_result <- auth_prepare(req_auth_api_key, "parm", api_key = "my_key") - expect_s3_class(test_result, "nectar_auth") - expect_identical(test_result$auth_fn, req_auth_api_key) - expect_identical( - test_result$auth_args, - list("parm", api_key = "my_key") - ) -}) - -test_that("auth_api_key() prepares req_auth_api_key auth", { +test_that("auth_api_key() prepares req_auth_api_key auth (#81)", { test_result <- auth_api_key( parameter_name = "parm", api_key = "my_key", diff --git a/tests/testthat/test-req_prepare.R b/tests/testthat/test-req_prepare.R index c3746f6..ebdf931 100644 --- a/tests/testthat/test-req_prepare.R +++ b/tests/testthat/test-req_prepare.R @@ -153,7 +153,7 @@ test_that("req_prepare() applies tidying", { ) }) -test_that("req_prepare() applies prepared auth", { +test_that("req_prepare() applies prepared auth (#81)", { test_result <- req_prepare( base_url = "https://example.com", auth = auth_prepare(req_auth_api_key, "parm", api_key = "my_key") @@ -164,11 +164,11 @@ test_that("req_prepare() applies prepared auth", { ) }) -test_that("req_prepare() errors for unsupported auth objects", { +test_that("req_prepare() errors for unsupported auth objects (#81)", { expect_error( req_prepare( base_url = "https://example.com", - auth = list(auth_fn = req_auth_api_key, auth_args = list("parm")) + auth = "not_auth" ), class = "nectar-error-unsupported_auth_class" ) From de383e572cdbcc37c4ecb3b1d483afb258e46e63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 15:09:54 +0000 Subject: [PATCH 4/5] fix: address review feedback for auth docs and list coercion Agent-Logs-Url: https://github.com/api2r/nectar/sessions/5caf0c9e-1fef-4ff3-b2ef-c054f0052f07 Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> --- R/auth_prepare.R | 16 ++++++++++++++-- R/req_auth_api_key.R | 3 ++- man/auth_api_key.Rd | 3 ++- man/auth_prepare.Rd | 5 +++-- man/req_auth_api_key.Rd | 15 ++++++++++++++- man/req_init.Rd | 1 + man/req_modify.Rd | 1 + man/req_pagination_policy.Rd | 1 + man/req_prepare.Rd | 1 + man/req_tidy_policy.Rd | 1 + tests/testthat/test-auth_prepare.R | 21 +++++++++++++++++++++ tests/testthat/test-req_prepare.R | 4 ++-- 12 files changed, 63 insertions(+), 9 deletions(-) diff --git a/R/auth_prepare.R b/R/auth_prepare.R index 2627b75..5c70c7b 100644 --- a/R/auth_prepare.R +++ b/R/auth_prepare.R @@ -4,8 +4,8 @@ #' authentication strategy can be reused across requests. #' #' @inheritParams .shared-params -#' @inheritParams rlang::args_dots_empty #' @param auth_fn (`function`) A function to use to authenticate a request. +#' @param ... (`any`) Arguments to pass to `auth_fn`. #' @returns A list with class `"nectar_auth"` and elements `auth_fn` and #' `auth_args`. #' @family opinionated auth functions @@ -40,14 +40,26 @@ auth_prepare <- function(auth_fn, ..., call = rlang::caller_env()) { if (!("auth_fn" %in% names(auth))) { return(NextMethod()) } + auth_args <- auth$auth_args %||% list() + if (!is.list(auth_args)) { + .nectar_abort( + c( + "{.arg auth$auth_args} must be a list.", + x = "{.arg auth$auth_args} is {.obj_type_friendly {auth_args}}." + ), + subclass = "bad_auth_args", + call = call + ) + } if (setequal(names(auth), c("auth_fn", "auth_args"))) { + auth$auth_args <- auth_args class(auth) <- "nectar_auth" return(auth) } structure( list( auth_fn = auth$auth_fn, - auth_args = auth[setdiff(names(auth), "auth_fn")] + auth_args = c(auth_args, auth[setdiff(names(auth), c("auth_fn", "auth_args"))]) ), class = "nectar_auth" ) diff --git a/R/req_auth_api_key.R b/R/req_auth_api_key.R index b384b15..2239de5 100644 --- a/R/req_auth_api_key.R +++ b/R/req_auth_api_key.R @@ -8,7 +8,8 @@ #' @inheritParams rlang::args_dots_empty #' #' @inherit .shared-request return -#' @family opinionated auth functions, opinionated request functions +#' @family opinionated auth functions +#' @family opinionated request functions #' @export #' #' @examples diff --git a/man/auth_api_key.Rd b/man/auth_api_key.Rd index f1e5377..d98937a 100644 --- a/man/auth_api_key.Rd +++ b/man/auth_api_key.Rd @@ -45,6 +45,7 @@ auth_api_key("X-API-Key", api_key = "my-api-key") } \seealso{ Other opinionated auth functions: -\code{\link[=auth_prepare]{auth_prepare()}} +\code{\link[=auth_prepare]{auth_prepare()}}, +\code{\link[=req_auth_api_key]{req_auth_api_key()}} } \concept{opinionated auth functions} diff --git a/man/auth_prepare.Rd b/man/auth_prepare.Rd index f62f587..353233f 100644 --- a/man/auth_prepare.Rd +++ b/man/auth_prepare.Rd @@ -9,7 +9,7 @@ auth_prepare(auth_fn, ..., call = rlang::caller_env()) \arguments{ \item{auth_fn}{(\code{function}) A function to use to authenticate a request.} -\item{...}{These dots are for future extensions and must be empty.} +\item{...}{(\code{any}) Arguments to pass to \code{auth_fn}.} \item{call}{(\code{environment}) The environment from which a function was called, e.g. \code{\link[rlang:caller_env]{rlang::caller_env()}} (the default). The environment will be mentioned @@ -30,6 +30,7 @@ auth_prepare(req_auth_api_key, "X-API-Key", api_key = "my-api-key") } \seealso{ Other opinionated auth functions: -\code{\link[=auth_api_key]{auth_api_key()}} +\code{\link[=auth_api_key]{auth_api_key()}}, +\code{\link[=req_auth_api_key]{req_auth_api_key()}} } \concept{opinionated auth functions} diff --git a/man/req_auth_api_key.Rd b/man/req_auth_api_key.Rd index cb97fc5..234152a 100644 --- a/man/req_auth_api_key.Rd +++ b/man/req_auth_api_key.Rd @@ -55,4 +55,17 @@ req_auth_api_key(req, "api_key", api_key = "my-api-key", location = "query") # If `api_key` is NULL, the key is removed from the request req_auth_api_key(req, "X-API-Key", api_key = NULL) } -\concept{opinionated auth functions, opinionated request functions} +\seealso{ +Other opinionated auth functions: +\code{\link[=auth_api_key]{auth_api_key()}}, +\code{\link[=auth_prepare]{auth_prepare()}} + +Other opinionated request functions: +\code{\link[=req_init]{req_init()}}, +\code{\link[=req_modify]{req_modify()}}, +\code{\link[=req_pagination_policy]{req_pagination_policy()}}, +\code{\link[=req_prepare]{req_prepare()}}, +\code{\link[=req_tidy_policy]{req_tidy_policy()}} +} +\concept{opinionated auth functions} +\concept{opinionated request functions} diff --git a/man/req_init.Rd b/man/req_init.Rd index f30dfbc..4ff141b 100644 --- a/man/req_init.Rd +++ b/man/req_init.Rd @@ -46,6 +46,7 @@ req_init( } \seealso{ Other opinionated request functions: +\code{\link[=req_auth_api_key]{req_auth_api_key()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, \code{\link[=req_prepare]{req_prepare()}}, diff --git a/man/req_modify.Rd b/man/req_modify.Rd index 3b4dfd8..5d0ebde 100644 --- a/man/req_modify.Rd +++ b/man/req_modify.Rd @@ -60,6 +60,7 @@ req_modify(req_base, query = c("param1" = "value1", "param2" = "value2")) } \seealso{ Other opinionated request functions: +\code{\link[=req_auth_api_key]{req_auth_api_key()}}, \code{\link[=req_init]{req_init()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, \code{\link[=req_prepare]{req_prepare()}}, diff --git a/man/req_pagination_policy.Rd b/man/req_pagination_policy.Rd index 41bfdb6..346810f 100644 --- a/man/req_pagination_policy.Rd +++ b/man/req_pagination_policy.Rd @@ -40,6 +40,7 @@ req_pagination_policy(req, httr2::iterate_with_offset("page")) } \seealso{ Other opinionated request functions: +\code{\link[=req_auth_api_key]{req_auth_api_key()}}, \code{\link[=req_init]{req_init()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_prepare]{req_prepare()}}, diff --git a/man/req_prepare.Rd b/man/req_prepare.Rd index 01e95a5..05b4eaf 100644 --- a/man/req_prepare.Rd +++ b/man/req_prepare.Rd @@ -93,6 +93,7 @@ req_prepare( } \seealso{ Other opinionated request functions: +\code{\link[=req_auth_api_key]{req_auth_api_key()}}, \code{\link[=req_init]{req_init()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, diff --git a/man/req_tidy_policy.Rd b/man/req_tidy_policy.Rd index 5bf17af..a3d9bbf 100644 --- a/man/req_tidy_policy.Rd +++ b/man/req_tidy_policy.Rd @@ -40,6 +40,7 @@ req_tidy_policy(req, httr2::resp_body_json, list(simplifyVector = TRUE)) } \seealso{ Other opinionated request functions: +\code{\link[=req_auth_api_key]{req_auth_api_key()}}, \code{\link[=req_init]{req_init()}}, \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, diff --git a/tests/testthat/test-auth_prepare.R b/tests/testthat/test-auth_prepare.R index ee4734c..78422b3 100644 --- a/tests/testthat/test-auth_prepare.R +++ b/tests/testthat/test-auth_prepare.R @@ -7,3 +7,24 @@ test_that("auth_prepare() constructs nectar_auth objects (#81)", { list("parm", api_key = "my_key") ) }) + +test_that(".as_nectar_auth() merges auth_args with additional auth fields (#81)", { + test_result <- .as_nectar_auth(list( + auth_fn = req_auth_api_key, + auth_args = list(parameter_name = "parm"), + api_key = "my_key", + location = "query" + )) + expect_s3_class(test_result, "nectar_auth") + expect_identical( + test_result$auth_args, + list(parameter_name = "parm", api_key = "my_key", location = "query") + ) +}) + +test_that(".as_nectar_auth() errors for non-list auth_args (#81)", { + expect_error( + .as_nectar_auth(list(auth_fn = req_auth_api_key, auth_args = "not-a-list")), + class = "nectar-error-bad_auth_args" + ) +}) diff --git a/tests/testthat/test-req_prepare.R b/tests/testthat/test-req_prepare.R index ebdf931..189f295 100644 --- a/tests/testthat/test-req_prepare.R +++ b/tests/testthat/test-req_prepare.R @@ -159,8 +159,8 @@ test_that("req_prepare() applies prepared auth (#81)", { auth = auth_prepare(req_auth_api_key, "parm", api_key = "my_key") ) expect_in( - names(test_result$headers), - "parm" + "parm", + names(test_result$headers) ) }) From b7de24f8574a06eb8cde874f370324aa0747721f Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Sun, 10 May 2026 10:57:48 -0500 Subject: [PATCH 5/5] Manual tweaks --- NAMESPACE | 1 + R/auth_prepare.R | 22 ++++++++--------- tests/testthat/test-auth_prepare.R | 39 +++++++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index debb939..2b9d1a1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,7 @@ S3method(.add_body,json) S3method(.add_body,multipart) S3method(.as_nectar_auth,"NULL") +S3method(.as_nectar_auth,"function") S3method(.as_nectar_auth,default) S3method(.as_nectar_auth,list) S3method(.as_nectar_auth,nectar_auth) diff --git a/R/auth_prepare.R b/R/auth_prepare.R index 5c70c7b..9aab3bb 100644 --- a/R/auth_prepare.R +++ b/R/auth_prepare.R @@ -35,22 +35,17 @@ auth_prepare <- function(auth_fn, ..., call = rlang::caller_env()) { return(list(auth_fn = NULL, auth_args = list())) } +#' @export +.as_nectar_auth.function <- function(auth, call = rlang::caller_env()) { + .as_nectar_auth(list(auth_fn = auth), call = call) +} + #' @export .as_nectar_auth.list <- function(auth, call = rlang::caller_env()) { if (!("auth_fn" %in% names(auth))) { return(NextMethod()) } - auth_args <- auth$auth_args %||% list() - if (!is.list(auth_args)) { - .nectar_abort( - c( - "{.arg auth$auth_args} must be a list.", - x = "{.arg auth$auth_args} is {.obj_type_friendly {auth_args}}." - ), - subclass = "bad_auth_args", - call = call - ) - } + auth_args <- stbl::to_lst(auth$auth_args) %||% list() if (setequal(names(auth), c("auth_fn", "auth_args"))) { auth$auth_args <- auth_args class(auth) <- "nectar_auth" @@ -59,7 +54,10 @@ auth_prepare <- function(auth_fn, ..., call = rlang::caller_env()) { structure( list( auth_fn = auth$auth_fn, - auth_args = c(auth_args, auth[setdiff(names(auth), c("auth_fn", "auth_args"))]) + auth_args = c( + auth_args, + auth[setdiff(names(auth), c("auth_fn", "auth_args"))] + ) ), class = "nectar_auth" ) diff --git a/tests/testthat/test-auth_prepare.R b/tests/testthat/test-auth_prepare.R index 78422b3..4af138a 100644 --- a/tests/testthat/test-auth_prepare.R +++ b/tests/testthat/test-auth_prepare.R @@ -8,6 +8,36 @@ test_that("auth_prepare() constructs nectar_auth objects (#81)", { ) }) +test_that(".as_nectar_auth() returns nectar_auth objects unchanged (#81)", { + test_input <- auth_prepare( + auth_fn = req_auth_api_key, + auth_args = list(parameter_name = "parm") + ) + test_result <- .as_nectar_auth(test_input) + expect_identical(test_result, test_input) +}) + +test_that(".as_nectar_auth() returns NULL auth_fn and empty auth_args for NULL input (#81)", { + test_result <- .as_nectar_auth(NULL) + expect_identical(test_result$auth_fn, NULL) + expect_identical(test_result$auth_args, list()) +}) + +test_that(".as_nectar_auth() errors for list without auth_fn (#81)", { + stbl::expect_pkg_error_classes( + .as_nectar_auth(list(auth_args = list(parameter_name = "parm"))), + "nectar", + class = "unsupported_auth_class" + ) +}) + +test_that(".as_nectar_auth() converts function input to nectar_auth (#81)", { + test_result <- .as_nectar_auth(req_auth_api_key) + expect_s3_class(test_result, "nectar_auth") + expect_identical(test_result$auth_fn, req_auth_api_key) + expect_identical(test_result$auth_args, list()) +}) + test_that(".as_nectar_auth() merges auth_args with additional auth fields (#81)", { test_result <- .as_nectar_auth(list( auth_fn = req_auth_api_key, @@ -22,9 +52,10 @@ test_that(".as_nectar_auth() merges auth_args with additional auth fields (#81)" ) }) -test_that(".as_nectar_auth() errors for non-list auth_args (#81)", { - expect_error( - .as_nectar_auth(list(auth_fn = req_auth_api_key, auth_args = "not-a-list")), - class = "nectar-error-bad_auth_args" +test_that(".as_nectar_auth() errors for non-listable auth_args (#81)", { + stbl::expect_pkg_error_classes( + .as_nectar_auth(list(auth_fn = req_auth_api_key, auth_args = mean)), + "stbl", + class = "bad_function" ) })