diff --git a/NAMESPACE b/NAMESPACE index d50f786..b8b3b4d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,11 +1,14 @@ # Generated by roxygen2: do not edit by hand export("%>%") +export(best_pool) export(can_api) export(can_pool) +export(could_pool) export(get_cr_mailto) export(get_cr_token) export(is_email_address) +export(pools) export(req_cr) export(resp_cr_pool) export(was_pool) diff --git a/R/api.R b/R/api.R index 9bded15..88b8e01 100644 --- a/R/api.R +++ b/R/api.R @@ -1,27 +1,45 @@ #' Use the [Crossref REST API](https://api.crossref.org/swagger-ui/index.html) #' +#' @details +#' Will always try to authenticate into the most performant API pool possible, +#' but gracefully downgrades to less performant pools, +#' when `mailto` or `token` cannot be found. +#' See [req_auth_pool()] for details. +#' +#' @inheritParams req_auth_pool #' @family api functions #' @export -req_cr <- function() { - httr2::request("https://api.crossref.org/") %>% - req_user_agent_cr() +req_cr <- function(mailto = get_cr_mailto()) { + httr2::request("https://api.crossref.org/works") %>% + req_user_agent_cr(mailto = NULL) %>% + # below overwrites above ua, duplication is necessary + # because only req_auth_pool() gives feedback + req_auth_pool(mailto = mailto) } #' Set a user agent #' -#' To set a `mailto`, see [polite()]. -#' +#' @param mailto a character scalar giving a valid email address. #' @inheritParams httr2::req_user_agent -#' @noRd -req_user_agent_cr <- function(req) { - httr2::req_user_agent( - req = req, - string = paste0( +#' @keywords internal +req_user_agent_cr <- function(req, mailto = NULL) { + if (is.null(mailto)) { + ua <- paste0( "crlite/", utils::packageVersion("crlite"), "(https://github.com/subugoe/crlite/)" ) - ) + } else { + ua <- paste0( + "crlite/", + utils::packageVersion("crlite"), + "(https://github.com/subugoe/crlite/; ", + "mailto:", + mailto, + ")" + ) + } + httr2::req_user_agent(req = req, string = ua) } #' Test whether API can be reached diff --git a/R/pools.R b/R/pools.R index 49025ff..630aa69 100644 --- a/R/pools.R +++ b/R/pools.R @@ -1,46 +1,108 @@ # testing ==== -#' Diagnosing API pools -#' @name pools +#' Accessing different Crossref API pools +#' +#' In increasing order of performance: `r pools()`. +#' #' @family api pool access functions -NULL +#' @export +pools <- function() c("public", "polite", "plus") + +#' @describeIn pools Try to authenticate into the highest performing pool +#' +#' Function checks whether a higher performance pool is possible, +#' using [could_pool()]. +#' Only when that test passes, is the request authenticated. +#' The function thus gracefully recovers from bad `mailto` and `token`s. +#' The function emits intermittent messages about the chosen API pool. +#' +#' @inheritParams req_user_agent_cr +req_auth_pool <- function(req, mailto = get_cr_mailto()) { + if (could_pool("polite")) { + inform_pool("polite") + req <- req_user_agent_cr(req, mailto = mailto) + } else { + inform_pool("public") + } + req +} + +#' Inform user about email source +#' @noRd +inform_pool <- function(pool) { + rlang::inform( + c( + "i" = paste( + "Requesting the", pool, "API pool." + ) + ), + .frequency = "once", + .frequency_id = "inform_pool" + ) +} + #' @describeIn pools Show the API pool which served a request #' @inheritParams httr2::resp_header #' @family api pool access functions #' @export -resp_cr_pool <- function(resp) { - httr2::resp_header(resp, "x-api-pool") -} +resp_cr_pool <- function(resp) httr2::resp_header(resp, "x-api-pool") #' @describeIn pools Was the request served by the expected pool? -#' @param pool giving the name of the expected pool. -#' Listed in order of increasing performance. +#' Inspects the response header "x-api-pool" of an *existing* response +#' to see if it matches the expected pool. +#' @param pool giving the name of the expected pool #' @export -was_pool <- function(resp, pool = c("public", "polite", "plus")) { +was_pool <- function(resp, pool = pools()) { pool <- rlang::arg_match(pool) resp_cr_pool(resp) == pool } -#' @describeIn pools Can the request be served by an expected pool? +#' @describeIn pools Can requests be served by an expected pool? +#' Performs a request to the API and inspects the response header. #' @export -can_pool <- function(pool = c("public", "polite", "plus")) { +can_pool <- function(pool = pools()) { pool <- rlang::arg_match(pool) resp_cr_pool(httr2::req_perform(req_head_cr())) == pool } +#' @describeIn pools Could the request be served by an expected pool? +#' Checks whether the necessary credentials can be found +#' for higher performance pools. +#' @export +could_pool <- function(pool = pools()) { + pool <- rlang::arg_match(pool) + expected_rank <- match(pool, pools()) + actual_rank <- match(best_pool(), pools()) + expected_rank <= actual_rank +} + +#' @describeIn pools What is the highest performing pool available? +#' Checks the necessary credentials for the highest performing pool. +#' @export +best_pool <- function(mailto = get_cr_mailto()) { + res <- "public" + mailto <- try(mailto, silent = TRUE) + if (!inherits(mailto, "try-error") && is_email_address(mailto)) { + res <- "polite" + } + res +} + # mailto for polite ==== #' Accessing the polite API pool #' @name polite #' @family api pool access functions NULL + #' @describeIn polite #' Get the email address to authenticate into the polite pool #' #' Whenever possible, API calls should be identified by an email address #' to reach the human responsible for making the call. -#' In this order, returns the first hit of: +#' In this order, returns the first valid email found in: #' +#' 1. The `crlite.mailto` option. #' 1. The `CR_MD_MAILTO` environment variable #' (recommended only for secure environment variables in the cloud). #' @@ -56,38 +118,44 @@ NULL #' 1. The git user email address for the repo at the working directory. #' This #' -#' Or errors out. +#' Otherwise, the function errors out. #' @export get_cr_mailto <- function() { + mailto <- getOption("crlite.mailto") + if (!is.null(mailto)) { + inform_mailto_source(mailto = mailto, from = "`crlite.mailto` option") + return(mailto) + } mailto <- Sys.getenv("CR_MD_MAILTO") - if (is_email_address(mailto)) { - rlang::inform(c( - "i" = paste( - "Using", - mailto, - "from environment variable `CR_MD_MAILTO` as a Crossref user." - ) - )) + if (mailto != "") { + inform_mailto_source(mailto = mailto, from = "`CR_MD_MAILTO` env var") return(mailto) } if (requireNamespace("gert", quietly = TRUE)) { - mailto <- try(gert::git_signature_parse(gert::git_signature_default - ())$email) - if (is_email_address(mailto)) { - rlang::inform( - c( - "i" = paste( - "Using", mailto, "from your git config as a Crossref user." - ) - ), - .frequency = "once", - .frequency_id = "get_cr_mailto" - ) + mailto <- try( + gert::git_signature_parse(gert::git_signature_default())$email, + silent = TRUE + ) + if (!inherits(mailto, "try-error")) { + inform_mailto_source(mailto = mailto, from = "your git config") return(mailto) } - } else { - rlang::abort(c("x" = "Could not find a Crossref user.")) } + rlang::abort(c("x" = "Could not find a Crossref user.")) +} + +#' Inform user about email source +#' @noRd +inform_mailto_source <- function(mailto, from) { + rlang::inform( + c( + "i" = paste( + "Using", mailto, "from", from, "as a Crossref user." + ) + ), + .frequency = "once", + .frequency_id = "get_cr_mailto" + ) } # token for plus ==== diff --git a/man/plus.Rd b/man/plus.Rd index ef02d8f..e0f0914 100644 --- a/man/plus.Rd +++ b/man/plus.Rd @@ -31,6 +31,6 @@ In this order, returns the first hit of: \seealso{ Other api pool access functions: \code{\link{polite}}, -\code{\link{pools}} +\code{\link{pools}()} } \concept{api pool access functions} diff --git a/man/polite.Rd b/man/polite.Rd index 97bcd90..f8de06c 100644 --- a/man/polite.Rd +++ b/man/polite.Rd @@ -17,8 +17,9 @@ Accessing the polite API pool Whenever possible, API calls should be identified by an email address to reach the human responsible for making the call. -In this order, returns the first hit of: +In this order, returns the first valid email found in: \enumerate{ +\item The \code{crlite.mailto} option. \item The \code{CR_MD_MAILTO} environment variable (recommended only for secure environment variables in the cloud). @@ -32,12 +33,12 @@ thus:\if{html}{\out{
}}\preformatted{env: This } -Or errors out. +Otherwise, the function errors out. }}} \seealso{ Other api pool access functions: \code{\link{plus}}, -\code{\link{pools}} +\code{\link{pools}()} } \concept{api pool access functions} diff --git a/man/pools.Rd b/man/pools.Rd index 1841d6e..1d1710b 100644 --- a/man/pools.Rd +++ b/man/pools.Rd @@ -2,36 +2,71 @@ % Please edit documentation in R/pools.R \name{pools} \alias{pools} +\alias{req_auth_pool} \alias{resp_cr_pool} \alias{was_pool} \alias{can_pool} -\title{Diagnosing API pools} +\alias{could_pool} +\alias{best_pool} +\title{Accessing different Crossref API pools} \usage{ +pools() + +req_auth_pool(req, mailto = get_cr_mailto()) + resp_cr_pool(resp) -was_pool(resp, pool = c("public", "polite", "plus")) +was_pool(resp, pool = pools()) + +can_pool(pool = pools()) + +could_pool(pool = pools()) -can_pool(pool = c("public", "polite", "plus")) +best_pool(mailto = get_cr_mailto()) } \arguments{ +\item{req}{A \link[httr2]{request}.} + +\item{mailto}{a character scalar giving a valid email address.} + \item{resp}{An HTTP response object, as created by \code{\link[httr2:req_perform]{req_perform()}}.} -\item{pool}{giving the name of the expected pool. -Listed in order of increasing performance.} +\item{pool}{giving the name of the expected pool} } \description{ -Diagnosing API pools +In increasing order of performance: public, polite, plus. } \section{Related Functions and Methods}{ \subsection{Functions}{ \itemize{ +\item \code{req_auth_pool}: Try to authenticate into the highest performing pool + +Function checks whether a higher performance pool is possible, +using \code{\link[=could_pool]{could_pool()}}. +Only when that test passes, is the request authenticated. +The function thus gracefully recovers from bad \code{mailto} and \code{token}s. +The function emits intermittent messages about the chosen API pool. +} +\itemize{ \item \code{resp_cr_pool}: Show the API pool which served a request } \itemize{ \item \code{was_pool}: Was the request served by the expected pool? +Inspects the response header "x-api-pool" of an \emph{existing} response +to see if it matches the expected pool. +} +\itemize{ +\item \code{can_pool}: Can requests be served by an expected pool? +Performs a request to the API and inspects the response header. +} +\itemize{ +\item \code{could_pool}: Could the request be served by an expected pool? +Checks whether the necessary credentials can be found +for higher performance pools. } \itemize{ -\item \code{can_pool}: Can the request be served by an expected pool? +\item \code{best_pool}: What is the highest performing pool available? +Checks the necessary credentials for the highest performing pool. }}} \seealso{ diff --git a/man/req_cr.Rd b/man/req_cr.Rd index 7d6a36e..8be4bde 100644 --- a/man/req_cr.Rd +++ b/man/req_cr.Rd @@ -4,11 +4,20 @@ \alias{req_cr} \title{Use the \href{https://api.crossref.org/swagger-ui/index.html}{Crossref REST API}} \usage{ -req_cr() +req_cr(mailto = get_cr_mailto()) +} +\arguments{ +\item{mailto}{a character scalar giving a valid email address.} } \description{ Use the \href{https://api.crossref.org/swagger-ui/index.html}{Crossref REST API} } +\details{ +Will always try to authenticate into the most performant API pool possible, +but gracefully downgrades to less performant pools, +when \code{mailto} or \code{token} cannot be found. +See \code{\link[=req_auth_pool]{req_auth_pool()}} for details. +} \seealso{ Other api functions: \code{\link{can_api}()} diff --git a/man/req_user_agent_cr.Rd b/man/req_user_agent_cr.Rd new file mode 100644 index 0000000..3cd9648 --- /dev/null +++ b/man/req_user_agent_cr.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/api.R +\name{req_user_agent_cr} +\alias{req_user_agent_cr} +\title{Set a user agent} +\usage{ +req_user_agent_cr(req, mailto = NULL) +} +\arguments{ +\item{req}{A \link[httr2]{request}.} + +\item{mailto}{a character scalar giving a valid email address.} +} +\description{ +Set a user agent +} +\keyword{internal} diff --git a/tests/testthat/test-api.R b/tests/testthat/test-api.R index 3cae601..bd6994a 100644 --- a/tests/testthat/test-api.R +++ b/tests/testthat/test-api.R @@ -1,6 +1,8 @@ # unit tests ==== test_that("user agent is properly set", { + # knock out mailto + withr::local_options(crlite.mailto = "") expect_equal( req_cr()$options$useragent, "crlite/0.0.0.9000(https://github.com/subugoe/crlite/)" @@ -9,6 +11,7 @@ test_that("user agent is properly set", { # integration tests ==== + test_that("api can be reached", { skip_if_offline() expect_true(can_api()) diff --git a/tests/testthat/test-pools.R b/tests/testthat/test-pools.R index c14fbec..8d5e459 100644 --- a/tests/testthat/test-pools.R +++ b/tests/testthat/test-pools.R @@ -8,10 +8,6 @@ test_that("email from env var can be found", { ) expect_true(is_email_address(suppressMessages(get_cr_mailto()))) }) -test_that("email from env var can be found on github actions", { - skip_if_not(Sys.getenv("GITHUB_ACTIONS") == "true") - expect_true(is_email_address(suppressMessages(get_cr_mailto()))) -}) # integration tests ==== @@ -21,10 +17,11 @@ test_that("by default, public pool is used", { withr::local_options(crlite.mailto = NULL) # knock out mdplus token withr::local_envvar(c("CR_MDPLUS_TOKEN" = NA)) + # knock out git email + withr::local_dir(tempdir()) expect_true(can_pool("public")) }) test_that("with mailto, polite pool is used", { - skip("in dev") # knock out mdplus token withr::local_envvar(c("CR_MDPLUS_TOKEN" = NA)) withr::local_options(