diff --git a/NAMESPACE b/NAMESPACE index 2b9d1a1..972b467 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,6 +10,11 @@ S3method(.as_nectar_auth,nectar_auth) S3method(.as_nectar_request,default) S3method(.as_nectar_request,httr2_request) S3method(.as_nectar_request,nectar_request) +S3method(.as_nectar_tidy_policy,"NULL") +S3method(.as_nectar_tidy_policy,"function") +S3method(.as_nectar_tidy_policy,default) +S3method(.as_nectar_tidy_policy,list) +S3method(.as_nectar_tidy_policy,nectar_tidy_policy) S3method(resp_parse,default) S3method(resp_parse,httr2_response) S3method(resp_parse,list) @@ -39,6 +44,10 @@ export(resp_parse) export(resp_tidy) export(resp_tidy_json) export(resp_tidy_unknown) +export(tidy_policy_body_auto) +export(tidy_policy_json) +export(tidy_policy_prepare) +export(tidy_policy_unknown) export(url_normalize) export(url_path_append) importFrom(fs,path) diff --git a/R/aaa-shared_params.R b/R/aaa-shared_params.R index 6cb7e53..7e00694 100644 --- a/R/aaa-shared_params.R +++ b/R/aaa-shared_params.R @@ -76,10 +76,24 @@ #' [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 tidy_fn (`function`) A function that will be invoked by [resp_tidy()] -#' to tidy the response. -#' @param tidy_args (`list`) A list of additional arguments to pass to -#' `tidy_fn`. +#' @param spec (`tspec` or `NULL`) A specification used by +#' [tibblify::tibblify()] to parse the extracted body of `resp`. When `spec` +#' is `NULL` (the default), [tibblify::tibblify()] will attempt to guess a +#' spec. +#' @param tidy_policy (`nectar_tidy_policy` or `NULL`) A tidying policy prepared +#' with [tidy_policy_prepare()]. By default, [tidy_policy_body_auto()] is used +#' to automatically apply [resp_body_auto()] to responses. +#' @param unspecified (`length-1 character`) A string that describes what +#' happens if the extracted body of `resp` contains fields that are not +#' specified in `spec`. While [tibblify::tibblify()] defaults to `NULL` for +#' this value, we set it to `list` so that the body will still parse when +#' `resp` contains extra data without throwing errors. +#' @param subset_path (`character`) An optional vector indicating the path to +#' the "real" object within the body of `resp`. For example, many APIs return +#' a body with information about the status of the response, cache +#' information, perhaps pagination information, and then the actual data in a +#' field such as `data`. If the desired part of the response body is in +#' `data$objects`, the value of this argument should be `c("data", "object")`. #' @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. diff --git a/R/auth_prepare.R b/R/auth_prepare.R index 9aab3bb..6b6a527 100644 --- a/R/auth_prepare.R +++ b/R/auth_prepare.R @@ -46,11 +46,6 @@ auth_prepare <- function(auth_fn, ..., call = rlang::caller_env()) { return(NextMethod()) } 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" - return(auth) - } structure( list( auth_fn = auth$auth_fn, diff --git a/R/req_prepare.R b/R/req_prepare.R index f716050..f00991e 100644 --- a/R/req_prepare.R +++ b/R/req_prepare.R @@ -27,8 +27,7 @@ req_prepare <- function( method = NULL, additional_user_agent = NULL, auth = NULL, - tidy_fn = NULL, - tidy_args = list(), + tidy_policy = tidy_policy_body_auto(), pagination_fn = NULL, call = rlang::caller_env() ) { @@ -52,8 +51,8 @@ req_prepare <- function( if (length(pagination_fn)) { req <- req_pagination_policy(req, pagination_fn, call = call) } - if (length(tidy_fn)) { - req <- req_tidy_policy(req, tidy_fn, tidy_args = tidy_args, call = call) + if (length(tidy_policy)) { + req <- req_tidy_policy(req, tidy_policy = tidy_policy, call = call) } return(.as_nectar_request(req)) } diff --git a/R/req_tidy_policy.R b/R/req_tidy_policy.R index a5b5145..4c411f7 100644 --- a/R/req_tidy_policy.R +++ b/R/req_tidy_policy.R @@ -7,21 +7,24 @@ #' @inheritParams .shared-params #' @inherit .shared-request return #' @family opinionated request functions +#' @family opinionated response parsers #' @export #' #' @examples #' req <- httr2::request("https://example.com") -#' req_tidy_policy(req, httr2::resp_body_json, list(simplifyVector = TRUE)) +#' req_tidy_policy( +#' req, +#' tidy_policy_json() +#' ) req_tidy_policy <- function( req, - tidy_fn = resp_body_auto, - tidy_args = list(), + tidy_policy = tidy_policy_body_auto(), call = rlang::caller_env() ) { - tidy_fn <- rlang::as_function(tidy_fn, call = call) + tidy_policy <- .as_nectar_tidy_policy(tidy_policy, call = call) .req_policy( req, - resp_tidy = list(tidy_fn = tidy_fn, tidy_args = tidy_args), + resp_tidy = tidy_policy, call = call ) } diff --git a/R/resp_body_auto.R b/R/resp_body_auto.R index 103407b..f339049 100644 --- a/R/resp_body_auto.R +++ b/R/resp_body_auto.R @@ -34,6 +34,21 @@ resp_body_auto <- function(resp) { ) } +#' A policy to automatically parse a response body +#' +#' Create a reusable tidy policy that applies [resp_body_auto()]. +#' +#' @returns A list with class `"nectar_tidy_policy"` and elements `tidy_fn` and +#' `tidy_args`. +#' @family opinionated response parsers +#' @export +#' +#' @examples +#' tidy_policy_body_auto() +tidy_policy_body_auto <- function() { + tidy_policy_prepare(resp_body_auto) +} + #' Automatically choose more body parsers #' #' This helper function exists to find somewhat variable content types and diff --git a/R/resp_tidy.R b/R/resp_tidy.R index e193c11..8ea7329 100644 --- a/R/resp_tidy.R +++ b/R/resp_tidy.R @@ -18,6 +18,7 @@ #' httr2 response parsers, and [resp_parse()] for an alternative approach to #' dealing with responses (particularly useful if the request does not include #' a `resp_tidy` policy). +#' @family opinionated response parsers #' @export #' #' @examples @@ -28,7 +29,7 @@ #' # With a tidy policy, resp_tidy() uses the policy's tidy function. #' req <- req_tidy_policy( #' httr2::request("https://example.com"), -#' httr2::resp_body_json +#' tidy_policy_prepare(httr2::resp_body_json) #' ) #' # In practice, the request is attached automatically when the response is #' # fetched with httr2::req_perform() or req_perform_opinionated(). diff --git a/R/resp_tidy_json.R b/R/resp_tidy_json.R index 083e80a..143eee1 100644 --- a/R/resp_tidy_json.R +++ b/R/resp_tidy_json.R @@ -4,23 +4,9 @@ #' subset of that body, and tidy the result with [tibblify::tibblify()]. #' #' @inheritParams .shared-params -#' @param spec (`tspec` or `NULL`) A specification used by -#' [tibblify::tibblify()] to parse the extracted body of `resp`. When `spec` -#' is `NULL` (the default), [tibblify::tibblify()] will attempt to guess a -#' spec. -#' @param unspecified (`length-1 character`) A string that describes what -#' happens if the extracted body of `resp` contains fields that are not -#' specified in `spec`. While [tibblify::tibblify()] defaults to `NULL` for -#' this value, we set it to `list` so that the body will still parse when -#' `resp` contains extra data without throwing errors. -#' @param subset_path (`character`) An optional vector indicating the path to -#' the "real" object within the body of `resp`. For example, many APIs return -#' a body with information about the status of the response, cache -#' information, perhaps pagination information, and then the actual data in a -#' field such as `data`. If the desired part of the response body is in -#' `data$objects`, the value of this argument should be `c("data", "object")`. #' #' @returns The tibblified response body. +#' @family opinionated response parsers #' @export #' #' @examplesIf rlang::is_installed("tibblify") @@ -59,3 +45,28 @@ resp_tidy_json <- function( } return(NULL) } + +#' A policy to parse a response body as JSON +#' +#' Create a reusable tidy policy that applies [resp_tidy_json()]. +#' +#' @inheritParams .shared-params +#' @returns A list with class `"nectar_tidy_policy"` and elements `tidy_fn` and +#' `tidy_args`. +#' @family opinionated response parsers +#' @export +#' +#' @examplesIf rlang::is_installed("tibblify") +#' tidy_policy_json(subset_path = "data") +tidy_policy_json <- function( + spec = NULL, + unspecified = "list", + subset_path = NULL +) { + tidy_policy_prepare( + resp_tidy_json, + spec = spec, + unspecified = unspecified, + subset_path = subset_path + ) +} diff --git a/R/resp_tidy_unknown.R b/R/resp_tidy_unknown.R index 6a38f16..d405f3b 100644 --- a/R/resp_tidy_unknown.R +++ b/R/resp_tidy_unknown.R @@ -7,6 +7,7 @@ #' #' @returns This function always throws an error. The error lists the names of #' the response pieces after parsing with [resp_body_auto()]. +#' @family opinionated response parsers #' @export #' #' @examples @@ -25,3 +26,19 @@ resp_tidy_unknown <- function(resp, call = rlang::caller_env()) { call = call ) } + +#' A policy to error for unknown response bodies +#' +#' Create a reusable tidy policy that applies [resp_tidy_unknown()], signaling +#' an informative error. +#' +#' @returns A list with class `"nectar_tidy_policy"` and elements `tidy_fn` and +#' `tidy_args`. +#' @family opinionated response parsers +#' @export +#' +#' @examples +#' tidy_policy_unknown() +tidy_policy_unknown <- function() { + tidy_policy_prepare(resp_tidy_unknown) +} diff --git a/R/tidy_policy_prepare.R b/R/tidy_policy_prepare.R new file mode 100644 index 0000000..08a0da4 --- /dev/null +++ b/R/tidy_policy_prepare.R @@ -0,0 +1,85 @@ +#' Prepare tidying independent of a request +#' +#' This constructor stores a response tidying function and arguments so the same +#' tidying strategy can be reused across requests. +#' +#' @param tidy_fn (`function`) A function that will be invoked by [resp_tidy()] +#' to tidy a response. +#' @param ... (`any`) Arguments to pass to `tidy_fn`. +#' @returns A list with class `"nectar_tidy_policy"` and elements `tidy_fn` and +#' `tidy_args`. +#' @family opinionated response parsers +#' @export +#' +#' @examples +#' tidy_policy_prepare(httr2::resp_body_json, simplifyVector = TRUE) +tidy_policy_prepare <- function(tidy_fn, ...) { + tidy_fn <- rlang::as_function(tidy_fn, call = rlang::caller_env()) + .as_nectar_tidy_policy( + list(tidy_fn = tidy_fn, tidy_args = rlang::list2(...)) + ) +} + +.as_nectar_tidy_policy <- function(tidy_policy, call = rlang::caller_env()) { + UseMethod(".as_nectar_tidy_policy") +} + +#' @export +.as_nectar_tidy_policy.nectar_tidy_policy <- function( + tidy_policy, + call = rlang::caller_env() +) { + return(tidy_policy) +} + +#' @export +.as_nectar_tidy_policy.NULL <- function( + tidy_policy, + call = rlang::caller_env() +) { + return(NULL) +} + +#' @export +.as_nectar_tidy_policy.function <- function( + tidy_policy, + call = rlang::caller_env() +) { + .as_nectar_tidy_policy(list(tidy_fn = tidy_policy), call = call) +} + +#' @export +.as_nectar_tidy_policy.list <- function( + tidy_policy, + call = rlang::caller_env() +) { + if (!("tidy_fn" %in% names(tidy_policy))) { + return(NextMethod()) + } + tidy_args <- stbl::to_lst(tidy_policy$tidy_args) %||% list() + structure( + list( + tidy_fn = tidy_policy$tidy_fn, + tidy_args = c( + tidy_args, + tidy_policy[setdiff(names(tidy_policy), c("tidy_fn", "tidy_args"))] + ) + ), + class = "nectar_tidy_policy" + ) +} + +#' @export +.as_nectar_tidy_policy.default <- function( + tidy_policy, + call = rlang::caller_env() +) { + .nectar_abort( + c( + "{.arg {tidy_policy}} must be `NULL` or a {.cls nectar_tidy_policy}.", + x = "{.arg {tidy_policy}} is {.obj_type_friendly {tidy_policy}}." + ), + subclass = "unsupported_tidy_policy_class", + call = call + ) +} diff --git a/README.Rmd b/README.Rmd index 07a419c..06c1e06 100644 --- a/README.Rmd +++ b/README.Rmd @@ -46,8 +46,7 @@ req <- req_prepare( query = list( rows = 10, cursor = "*", select = c("publisher", "DOI"), .multi = "comma" ), - tidy_fn = resp_tidy_json, - tidy_args = list(subset_path = c("message", "items")), + tidy_policy = tidy_policy_json(subset_path = c("message", "items")), pagination_fn = iterate_with_json_cursor( param_name = "cursor", next_cursor_path = c("message", "next-cursor") diff --git a/README.md b/README.md index ae87e8e..5f06ccb 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,7 @@ req <- req_prepare( query = list( rows = 10, cursor = "*", select = c("publisher", "DOI"), .multi = "comma" ), - tidy_fn = resp_tidy_json, - tidy_args = list(subset_path = c("message", "items")), + tidy_policy = tidy_policy_json(subset_path = c("message", "items")), pagination_fn = iterate_with_json_cursor( param_name = "cursor", next_cursor_path = c("message", "next-cursor") diff --git a/man/dot-shared-params.Rd b/man/dot-shared-params.Rd index bd2458c..d2f75cf 100644 --- a/man/dot-shared-params.Rd +++ b/man/dot-shared-params.Rd @@ -103,11 +103,27 @@ common. Set this to \code{NULL} to return the raw response from \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{tidy_fn}{(\code{function}) A function that will be invoked by \code{\link[=resp_tidy]{resp_tidy()}} -to tidy the response.} - -\item{tidy_args}{(\code{list}) A list of additional arguments to pass to -\code{tidy_fn}.} +\item{spec}{(\code{tspec} or \code{NULL}) A specification used by +\code{\link[tibblify:tibblify]{tibblify::tibblify()}} to parse the extracted body of \code{resp}. When \code{spec} +is \code{NULL} (the default), \code{\link[tibblify:tibblify]{tibblify::tibblify()}} will attempt to guess a +spec.} + +\item{tidy_policy}{(\code{nectar_tidy_policy} or \code{NULL}) A tidying policy prepared +with \code{\link[=tidy_policy_prepare]{tidy_policy_prepare()}}. By default, \code{\link[=tidy_policy_body_auto]{tidy_policy_body_auto()}} is used +to automatically apply \code{\link[=resp_body_auto]{resp_body_auto()}} to responses.} + +\item{unspecified}{(\verb{length-1 character}) A string that describes what +happens if the extracted body of \code{resp} contains fields that are not +specified in \code{spec}. While \code{\link[tibblify:tibblify]{tibblify::tibblify()}} defaults to \code{NULL} for +this value, we set it to \code{list} so that the body will still parse when +\code{resp} contains extra data without throwing errors.} + +\item{subset_path}{(\code{character}) An optional vector indicating the path to +the "real" object within the body of \code{resp}. For example, many APIs return +a body with information about the status of the response, cache +information, perhaps pagination information, and then the actual data in a +field such as \code{data}. If the desired part of the response body is in +\code{data$objects}, the value of this argument should be \code{c("data", "object")}.} \item{url}{(\verb{length-1 character}) An optional url associated with \code{name}.} diff --git a/man/req_prepare.Rd b/man/req_prepare.Rd index 05b4eaf..4631de1 100644 --- a/man/req_prepare.Rd +++ b/man/req_prepare.Rd @@ -14,8 +14,7 @@ req_prepare( method = NULL, additional_user_agent = NULL, auth = NULL, - tidy_fn = NULL, - tidy_args = list(), + tidy_policy = tidy_policy_body_auto(), pagination_fn = NULL, call = rlang::caller_env() ) @@ -55,11 +54,9 @@ Default \code{NULL}.} \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.} - -\item{tidy_args}{(\code{list}) A list of additional arguments to pass to -\code{tidy_fn}.} +\item{tidy_policy}{(\code{nectar_tidy_policy} or \code{NULL}) A tidying policy prepared +with \code{\link[=tidy_policy_prepare]{tidy_policy_prepare()}}. By default, \code{\link[=tidy_policy_body_auto]{tidy_policy_body_auto()}} is used +to automatically apply \code{\link[=resp_body_auto]{resp_body_auto()}} to responses.} \item{pagination_fn}{(\code{function}) A function that takes the previous response (\code{resp}) to generate the next request in a call to diff --git a/man/req_tidy_policy.Rd b/man/req_tidy_policy.Rd index a3d9bbf..4fee397 100644 --- a/man/req_tidy_policy.Rd +++ b/man/req_tidy_policy.Rd @@ -6,19 +6,16 @@ \usage{ req_tidy_policy( req, - tidy_fn = resp_body_auto, - tidy_args = list(), + tidy_policy = tidy_policy_body_auto(), call = rlang::caller_env() ) } \arguments{ \item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} -\item{tidy_fn}{(\code{function}) A function that will be invoked by \code{\link[=resp_tidy]{resp_tidy()}} -to tidy the response.} - -\item{tidy_args}{(\code{list}) A list of additional arguments to pass to -\code{tidy_fn}.} +\item{tidy_policy}{(\code{nectar_tidy_policy} or \code{NULL}) A tidying policy prepared +with \code{\link[=tidy_policy_prepare]{tidy_policy_prepare()}}. By default, \code{\link[=tidy_policy_body_auto]{tidy_policy_body_auto()}} is used +to automatically apply \code{\link[=resp_body_auto]{resp_body_auto()}} to responses.} \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 @@ -36,7 +33,10 @@ portion of a response and wrangle it into a desired format. } \examples{ req <- httr2::request("https://example.com") -req_tidy_policy(req, httr2::resp_body_json, list(simplifyVector = TRUE)) +req_tidy_policy( + req, + tidy_policy_json() +) } \seealso{ Other opinionated request functions: @@ -45,5 +45,15 @@ Other opinionated request functions: \code{\link[=req_modify]{req_modify()}}, \code{\link[=req_pagination_policy]{req_pagination_policy()}}, \code{\link[=req_prepare]{req_prepare()}} + +Other opinionated response parsers: +\code{\link[=resp_tidy]{resp_tidy()}}, +\code{\link[=resp_tidy_json]{resp_tidy_json()}}, +\code{\link[=resp_tidy_unknown]{resp_tidy_unknown()}}, +\code{\link[=tidy_policy_body_auto]{tidy_policy_body_auto()}}, +\code{\link[=tidy_policy_json]{tidy_policy_json()}}, +\code{\link[=tidy_policy_prepare]{tidy_policy_prepare()}}, +\code{\link[=tidy_policy_unknown]{tidy_policy_unknown()}} } \concept{opinionated request functions} +\concept{opinionated response parsers} diff --git a/man/resp_tidy.Rd b/man/resp_tidy.Rd index a79a711..96133a8 100644 --- a/man/resp_tidy.Rd +++ b/man/resp_tidy.Rd @@ -31,7 +31,7 @@ resp_tidy(resp) # With a tidy policy, resp_tidy() uses the policy's tidy function. req <- req_tidy_policy( httr2::request("https://example.com"), - httr2::resp_body_json + tidy_policy_prepare(httr2::resp_body_json) ) # In practice, the request is attached automatically when the response is # fetched with httr2::req_perform() or req_perform_opinionated(). @@ -46,4 +46,14 @@ response content type, \code{\link[httr2:resp_body_raw]{httr2::resp_body_raw()}} httr2 response parsers, and \code{\link[=resp_parse]{resp_parse()}} for an alternative approach to dealing with responses (particularly useful if the request does not include a \code{resp_tidy} policy). + +Other opinionated response parsers: +\code{\link[=req_tidy_policy]{req_tidy_policy()}}, +\code{\link[=resp_tidy_json]{resp_tidy_json()}}, +\code{\link[=resp_tidy_unknown]{resp_tidy_unknown()}}, +\code{\link[=tidy_policy_body_auto]{tidy_policy_body_auto()}}, +\code{\link[=tidy_policy_json]{tidy_policy_json()}}, +\code{\link[=tidy_policy_prepare]{tidy_policy_prepare()}}, +\code{\link[=tidy_policy_unknown]{tidy_policy_unknown()}} } +\concept{opinionated response parsers} diff --git a/man/resp_tidy_json.Rd b/man/resp_tidy_json.Rd index 94270a2..a4cf377 100644 --- a/man/resp_tidy_json.Rd +++ b/man/resp_tidy_json.Rd @@ -49,3 +49,14 @@ resp_nested <- httr2::response_json( resp_tidy_json(resp_nested, subset_path = "data") \dontshow{\}) # examplesIf} } +\seealso{ +Other opinionated response parsers: +\code{\link[=req_tidy_policy]{req_tidy_policy()}}, +\code{\link[=resp_tidy]{resp_tidy()}}, +\code{\link[=resp_tidy_unknown]{resp_tidy_unknown()}}, +\code{\link[=tidy_policy_body_auto]{tidy_policy_body_auto()}}, +\code{\link[=tidy_policy_json]{tidy_policy_json()}}, +\code{\link[=tidy_policy_prepare]{tidy_policy_prepare()}}, +\code{\link[=tidy_policy_unknown]{tidy_policy_unknown()}} +} +\concept{opinionated response parsers} diff --git a/man/resp_tidy_unknown.Rd b/man/resp_tidy_unknown.Rd index 3d1e0ba..38ffbe4 100644 --- a/man/resp_tidy_unknown.Rd +++ b/man/resp_tidy_unknown.Rd @@ -30,3 +30,14 @@ try( resp_tidy_unknown(resp) ) } +\seealso{ +Other opinionated response parsers: +\code{\link[=req_tidy_policy]{req_tidy_policy()}}, +\code{\link[=resp_tidy]{resp_tidy()}}, +\code{\link[=resp_tidy_json]{resp_tidy_json()}}, +\code{\link[=tidy_policy_body_auto]{tidy_policy_body_auto()}}, +\code{\link[=tidy_policy_json]{tidy_policy_json()}}, +\code{\link[=tidy_policy_prepare]{tidy_policy_prepare()}}, +\code{\link[=tidy_policy_unknown]{tidy_policy_unknown()}} +} +\concept{opinionated response parsers} diff --git a/man/tidy_policy_body_auto.Rd b/man/tidy_policy_body_auto.Rd new file mode 100644 index 0000000..7f11382 --- /dev/null +++ b/man/tidy_policy_body_auto.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/resp_body_auto.R +\name{tidy_policy_body_auto} +\alias{tidy_policy_body_auto} +\title{A policy to automatically parse a response body} +\usage{ +tidy_policy_body_auto() +} +\value{ +A list with class \code{"nectar_tidy_policy"} and elements \code{tidy_fn} and +\code{tidy_args}. +} +\description{ +Create a reusable tidy policy that applies \code{\link[=resp_body_auto]{resp_body_auto()}}. +} +\examples{ +tidy_policy_body_auto() +} +\seealso{ +Other opinionated response parsers: +\code{\link[=req_tidy_policy]{req_tidy_policy()}}, +\code{\link[=resp_tidy]{resp_tidy()}}, +\code{\link[=resp_tidy_json]{resp_tidy_json()}}, +\code{\link[=resp_tidy_unknown]{resp_tidy_unknown()}}, +\code{\link[=tidy_policy_json]{tidy_policy_json()}}, +\code{\link[=tidy_policy_prepare]{tidy_policy_prepare()}}, +\code{\link[=tidy_policy_unknown]{tidy_policy_unknown()}} +} +\concept{opinionated response parsers} diff --git a/man/tidy_policy_json.Rd b/man/tidy_policy_json.Rd new file mode 100644 index 0000000..064c085 --- /dev/null +++ b/man/tidy_policy_json.Rd @@ -0,0 +1,50 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/resp_tidy_json.R +\name{tidy_policy_json} +\alias{tidy_policy_json} +\title{A policy to parse a response body as JSON} +\usage{ +tidy_policy_json(spec = NULL, unspecified = "list", subset_path = NULL) +} +\arguments{ +\item{spec}{(\code{tspec} or \code{NULL}) A specification used by +\code{\link[tibblify:tibblify]{tibblify::tibblify()}} to parse the extracted body of \code{resp}. When \code{spec} +is \code{NULL} (the default), \code{\link[tibblify:tibblify]{tibblify::tibblify()}} will attempt to guess a +spec.} + +\item{unspecified}{(\verb{length-1 character}) A string that describes what +happens if the extracted body of \code{resp} contains fields that are not +specified in \code{spec}. While \code{\link[tibblify:tibblify]{tibblify::tibblify()}} defaults to \code{NULL} for +this value, we set it to \code{list} so that the body will still parse when +\code{resp} contains extra data without throwing errors.} + +\item{subset_path}{(\code{character}) An optional vector indicating the path to +the "real" object within the body of \code{resp}. For example, many APIs return +a body with information about the status of the response, cache +information, perhaps pagination information, and then the actual data in a +field such as \code{data}. If the desired part of the response body is in +\code{data$objects}, the value of this argument should be \code{c("data", "object")}.} +} +\value{ +A list with class \code{"nectar_tidy_policy"} and elements \code{tidy_fn} and +\code{tidy_args}. +} +\description{ +Create a reusable tidy policy that applies \code{\link[=resp_tidy_json]{resp_tidy_json()}}. +} +\examples{ +\dontshow{if (rlang::is_installed("tibblify")) withAutoprint(\{ # examplesIf} +tidy_policy_json(subset_path = "data") +\dontshow{\}) # examplesIf} +} +\seealso{ +Other opinionated response parsers: +\code{\link[=req_tidy_policy]{req_tidy_policy()}}, +\code{\link[=resp_tidy]{resp_tidy()}}, +\code{\link[=resp_tidy_json]{resp_tidy_json()}}, +\code{\link[=resp_tidy_unknown]{resp_tidy_unknown()}}, +\code{\link[=tidy_policy_body_auto]{tidy_policy_body_auto()}}, +\code{\link[=tidy_policy_prepare]{tidy_policy_prepare()}}, +\code{\link[=tidy_policy_unknown]{tidy_policy_unknown()}} +} +\concept{opinionated response parsers} diff --git a/man/tidy_policy_prepare.Rd b/man/tidy_policy_prepare.Rd new file mode 100644 index 0000000..429ffa5 --- /dev/null +++ b/man/tidy_policy_prepare.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tidy_policy_prepare.R +\name{tidy_policy_prepare} +\alias{tidy_policy_prepare} +\title{Prepare tidying independent of a request} +\usage{ +tidy_policy_prepare(tidy_fn, ...) +} +\arguments{ +\item{tidy_fn}{(\code{function}) A function that will be invoked by \code{\link[=resp_tidy]{resp_tidy()}} +to tidy a response.} + +\item{...}{(\code{any}) Arguments to pass to \code{tidy_fn}.} +} +\value{ +A list with class \code{"nectar_tidy_policy"} and elements \code{tidy_fn} and +\code{tidy_args}. +} +\description{ +This constructor stores a response tidying function and arguments so the same +tidying strategy can be reused across requests. +} +\examples{ +tidy_policy_prepare(httr2::resp_body_json, simplifyVector = TRUE) +} +\seealso{ +Other opinionated response parsers: +\code{\link[=req_tidy_policy]{req_tidy_policy()}}, +\code{\link[=resp_tidy]{resp_tidy()}}, +\code{\link[=resp_tidy_json]{resp_tidy_json()}}, +\code{\link[=resp_tidy_unknown]{resp_tidy_unknown()}}, +\code{\link[=tidy_policy_body_auto]{tidy_policy_body_auto()}}, +\code{\link[=tidy_policy_json]{tidy_policy_json()}}, +\code{\link[=tidy_policy_unknown]{tidy_policy_unknown()}} +} +\concept{opinionated response parsers} diff --git a/man/tidy_policy_unknown.Rd b/man/tidy_policy_unknown.Rd new file mode 100644 index 0000000..04537ca --- /dev/null +++ b/man/tidy_policy_unknown.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/resp_tidy_unknown.R +\name{tidy_policy_unknown} +\alias{tidy_policy_unknown} +\title{A policy to error for unknown response bodies} +\usage{ +tidy_policy_unknown() +} +\value{ +A list with class \code{"nectar_tidy_policy"} and elements \code{tidy_fn} and +\code{tidy_args}. +} +\description{ +Create a reusable tidy policy that applies \code{\link[=resp_tidy_unknown]{resp_tidy_unknown()}}, signaling +an informative error. +} +\examples{ +tidy_policy_unknown() +} +\seealso{ +Other opinionated response parsers: +\code{\link[=req_tidy_policy]{req_tidy_policy()}}, +\code{\link[=resp_tidy]{resp_tidy()}}, +\code{\link[=resp_tidy_json]{resp_tidy_json()}}, +\code{\link[=resp_tidy_unknown]{resp_tidy_unknown()}}, +\code{\link[=tidy_policy_body_auto]{tidy_policy_body_auto()}}, +\code{\link[=tidy_policy_json]{tidy_policy_json()}}, +\code{\link[=tidy_policy_prepare]{tidy_policy_prepare()}} +} +\concept{opinionated response parsers} diff --git a/tests/testthat/_snaps/resp_tidy_unknown.md b/tests/testthat/_snaps/resp_tidy_unknown.md index 9cb5d8d..2b1620a 100644 --- a/tests/testthat/_snaps/resp_tidy_unknown.md +++ b/tests/testthat/_snaps/resp_tidy_unknown.md @@ -9,3 +9,14 @@ ! No parser is defined for this response. i Response pieces: structured, other, and status +# tidy_policy_unknown() prepares resp_tidy_unknown for resp_tidy() (#86) + + Code + (expect_pkg_error_classes(resp_tidy(mock_response), "nectar", + "unknown_response_type")) + Output + + Error in `resp_tidy()`: + ! No parser is defined for this response. + i Response pieces: status and data + diff --git a/tests/testthat/test-req_prepare.R b/tests/testthat/test-req_prepare.R index 189f295..b319b1f 100644 --- a/tests/testthat/test-req_prepare.R +++ b/tests/testthat/test-req_prepare.R @@ -137,22 +137,34 @@ test_that("req_prepare() applies pagination", { ) }) -test_that("req_prepare() applies tidying", { +test_that("req_prepare() applies prepared tidying (#86)", { test_result <- req_prepare( base_url = "https://example.com", - tidy_fn = httr2::resp_body_json, - tidy_args = list(simplifyVector = TRUE) + tidy_policy = tidy_policy_prepare( + httr2::resp_body_json, + simplifyVector = TRUE + ) ) - test_result$policies$resp_tidy$tidy_fn + expect_s3_class(test_result$policies$resp_tidy, "nectar_tidy_policy") expect_identical( test_result$policies$resp_tidy, - list( - tidy_fn = httr2::resp_body_json, - tidy_args = list(simplifyVector = TRUE) + tidy_policy_prepare( + httr2::resp_body_json, + simplifyVector = TRUE ) ) }) +test_that("req_prepare() errors for unsupported tidy policy objects (#86)", { + expect_error( + req_prepare( + base_url = "https://example.com", + tidy_policy = "not_tidy_policy" + ), + class = "nectar-error-unsupported_tidy_policy_class" + ) +}) + test_that("req_prepare() applies prepared auth (#81)", { test_result <- req_prepare( base_url = "https://example.com", diff --git a/tests/testthat/test-req_tidy_policy.R b/tests/testthat/test-req_tidy_policy.R index 7198bb7..35892cf 100644 --- a/tests/testthat/test-req_tidy_policy.R +++ b/tests/testthat/test-req_tidy_policy.R @@ -1,35 +1,26 @@ -test_that("req_tidy_policy errors informatively for bad fn (#44)", { - expect_error( - req_tidy_policy( - httr2::request("https://example.com"), - tidy_fn = "not a function" - ), - "was not found" - ) -}) - -test_that("req_tidy_policy applies resp_body_auto by default (#44)", { +test_that("req_tidy_policy applies resp_body_auto by default (#44, #86)", { req <- req_tidy_policy(httr2::request("https://example.com")) + expect_s3_class(req$policies$resp_tidy, "nectar_tidy_policy") expect_identical( req$policies$resp_tidy, - list( - tidy_fn = resp_body_auto, - tidy_args = list() - ) + tidy_policy_body_auto() ) }) -test_that("req_tidy_policy applies the specified policy (#44)", { +test_that("req_tidy_policy applies the specified policy (#44, #86)", { req <- req_tidy_policy( httr2::request("https://example.com"), - tidy_fn = httr2::resp_body_json, - tidy_args = list(simplifyVector = TRUE) + tidy_policy = tidy_policy_prepare( + httr2::resp_body_json, + simplifyVector = TRUE + ) ) + expect_s3_class(req$policies$resp_tidy, "nectar_tidy_policy") expect_identical( req$policies$resp_tidy, - list( - tidy_fn = httr2::resp_body_json, - tidy_args = list(simplifyVector = TRUE) + tidy_policy_prepare( + httr2::resp_body_json, + simplifyVector = TRUE ) ) }) diff --git a/tests/testthat/test-resp_body_auto.R b/tests/testthat/test-resp_body_auto.R index 1ea2a2f..41951fc 100644 --- a/tests/testthat/test-resp_body_auto.R +++ b/tests/testthat/test-resp_body_auto.R @@ -86,3 +86,14 @@ test_that("resp_body_auto works for other things (#40)", { resp <- httr2::response(headers = list(`Content-Type` = "weird/thing")) expect_identical(resp_body_auto(resp), "raw") }) + +test_that("tidy_policy_body_auto() prepares resp_body_auto for resp_tidy() (#86)", { + mock_response <- httr2::response_json(body = list(a = 1, b = 2)) + mock_response$request <- list( + policies = list( + resp_tidy = tidy_policy_body_auto() + ) + ) + + expect_identical(resp_tidy(mock_response), list(a = 1L, b = 2L)) +}) diff --git a/tests/testthat/test-resp_tidy_json.R b/tests/testthat/test-resp_tidy_json.R index 423cb1d..ce23daa 100644 --- a/tests/testthat/test-resp_tidy_json.R +++ b/tests/testthat/test-resp_tidy_json.R @@ -72,7 +72,7 @@ test_that("resp_tidy_json tidies a response with a spec (#40)", { ) }) -test_that("resp_tidy_json works with resp_tidy (#40)", { +test_that("tidy_policy_json() prepares resp_tidy_json for resp_tidy() (#40, #86)", { source_tibble <- tibble::tibble( a = letters, b = LETTERS, @@ -88,14 +88,11 @@ test_that("resp_tidy_json works with resp_tidy (#40)", { ) mock_response$request <- list( policies = list( - resp_tidy = list( - tidy_fn = resp_tidy_json, - tidy_args = list( - spec = tibblify::tspec_df( - lc = tibblify::tib_chr("a"), - uc = tibblify::tib_chr("b"), - n = tibblify::tib_int("c"), - ) + resp_tidy = tidy_policy_json( + spec = tibblify::tspec_df( + lc = tibblify::tib_chr("a"), + uc = tibblify::tib_chr("b"), + n = tibblify::tib_int("c"), ) ) ) diff --git a/tests/testthat/test-resp_tidy_unknown.R b/tests/testthat/test-resp_tidy_unknown.R index 7d0327c..71b95ef 100644 --- a/tests/testthat/test-resp_tidy_unknown.R +++ b/tests/testthat/test-resp_tidy_unknown.R @@ -17,3 +17,19 @@ test_that("resp_tidy_unknown fails gracefully with object information", { "unknown_response_type" ) }) + +test_that("tidy_policy_unknown() prepares resp_tidy_unknown for resp_tidy() (#86)", { + mock_response <- httr2::response_json( + body = list(status = "ok", data = list(id = 1)) + ) + mock_response$request <- list( + policies = list( + resp_tidy = tidy_policy_unknown() + ) + ) + + expect_nectar_error_snapshot( + resp_tidy(mock_response), + "unknown_response_type" + ) +}) diff --git a/tests/testthat/test-tidy_policy_prepare.R b/tests/testthat/test-tidy_policy_prepare.R new file mode 100644 index 0000000..a4144ce --- /dev/null +++ b/tests/testthat/test-tidy_policy_prepare.R @@ -0,0 +1,65 @@ +test_that("tidy_policy_prepare() constructs nectar_tidy_policy objects (#86)", { + test_result <- tidy_policy_prepare( + httr2::resp_body_json, + simplifyVector = TRUE + ) + expect_s3_class(test_result, "nectar_tidy_policy") + expect_identical(test_result$tidy_fn, httr2::resp_body_json) + expect_identical( + test_result$tidy_args, + list(simplifyVector = TRUE) + ) +}) + +test_that(".as_nectar_tidy_policy() returns nectar_tidy_policy objects unchanged (#86)", { + test_input <- tidy_policy_prepare( + httr2::resp_body_json, + simplifyVector = TRUE + ) + test_result <- .as_nectar_tidy_policy(test_input) + expect_identical(test_result, test_input) +}) + +test_that(".as_nectar_tidy_policy() returns NULL for NULL input (#86)", { + test_result <- .as_nectar_tidy_policy(NULL) + expect_null(test_result) +}) + +test_that(".as_nectar_tidy_policy() errors for list without tidy_fn (#86)", { + stbl::expect_pkg_error_classes( + .as_nectar_tidy_policy(list(tidy_args = list(simplifyVector = TRUE))), + "nectar", + class = "unsupported_tidy_policy_class" + ) +}) + +test_that(".as_nectar_tidy_policy() converts function input to nectar_tidy_policy (#86)", { + test_result <- .as_nectar_tidy_policy(httr2::resp_body_json) + expect_s3_class(test_result, "nectar_tidy_policy") + expect_identical(test_result$tidy_fn, httr2::resp_body_json) + expect_identical(test_result$tidy_args, list()) +}) + +test_that(".as_nectar_tidy_policy() merges tidy_args with additional tidy policy fields (#86)", { + test_result <- .as_nectar_tidy_policy(list( + tidy_fn = httr2::resp_body_json, + tidy_args = list(simplifyVector = TRUE), + simplifyDataFrame = FALSE + )) + expect_s3_class(test_result, "nectar_tidy_policy") + expect_identical( + test_result$tidy_args, + list(simplifyVector = TRUE, simplifyDataFrame = FALSE) + ) +}) + +test_that(".as_nectar_tidy_policy() errors for non-listable tidy_args (#86)", { + stbl::expect_pkg_error_classes( + .as_nectar_tidy_policy(list( + tidy_fn = httr2::resp_body_json, + tidy_args = mean + )), + "stbl", + class = "bad_function" + ) +}) diff --git a/vignettes/nectar.Rmd b/vignettes/nectar.Rmd index 85d8231..27fb9d1 100644 --- a/vignettes/nectar.Rmd +++ b/vignettes/nectar.Rmd @@ -56,7 +56,7 @@ req <- req_auth_api_key( The Crossref API returns JSON. nectar's `resp_tidy_json()` function parses the JSON response body and converts the result to a tibble using `tibblify::tibblify()`. You can extract a nested path from the response at the same time with the `subset_path` argument. -For Crossref, the actual work items live at `message$items` in the response body. You can attach `resp_tidy_json()` to the request via the `tidy_fn` argument in `req_prepare()`, so that every response is automatically tidied when you call `resp_parse()` later: +For Crossref, the actual work items live at `message$items` in the response body. To have `req_prepare()` automatically tidy each response when you call `resp_parse()` later, supply a prepared tidying policy object via the `tidy_policy` argument, such as `tidy_policy_json(subset_path = c("message", "items"))`: ```{r tidy, eval = FALSE} req <- req_prepare( @@ -64,8 +64,7 @@ req <- req_prepare( query = list( rows = 10, cursor = "*", select = c("publisher", "DOI"), .multi = "comma" ), - tidy_fn = resp_tidy_json, - tidy_args = list(subset_path = c("message", "items")) + tidy_policy = tidy_policy_json(subset_path = c("message", "items")) ) ``` @@ -88,8 +87,7 @@ req <- req_prepare( query = list( rows = 10, cursor = "*", select = c("publisher", "DOI"), .multi = "comma" ), - tidy_fn = resp_tidy_json, - tidy_args = list(subset_path = c("message", "items")), + tidy_policy = tidy_policy_json(subset_path = c("message", "items")), pagination_fn = iterate_xref ) ``` @@ -108,7 +106,7 @@ The result is always a list of `httr2_response` objects with additional class `n ## Parsing the response with `resp_parse()` -`resp_parse()` converts the raw responses into a usable R object. Because the request was prepared with `tidy_fn = resp_tidy_json`, `resp_parse()` will find that function automatically and apply it to each response, then combine the results: +`resp_parse()` converts the raw responses into a usable R object. Because the request was prepared with `tidy_policy = tidy_policy_json(...)`, `resp_parse()` will find that function automatically and apply it to each response, then combine the results: ```{r parse, eval = FALSE} result <- resp_parse(resps) @@ -125,8 +123,7 @@ result <- req_prepare( query = list( rows = 10, cursor = "*", select = c("publisher", "DOI"), .multi = "comma" ), - tidy_fn = resp_tidy_json, - tidy_args = list(subset_path = c("message", "items")), + tidy_policy = tidy_policy_json(subset_path = c("message", "items")), pagination_fn = iterate_with_json_cursor( param_name = "cursor", next_cursor_path = c("message", "next-cursor") @@ -171,8 +168,7 @@ works <- function( "https://api.crossref.org/works", query = list(rows = rows, cursor = "*", select = select), auth = auth_api_key("mailto", api_key = mailto, location = "query"), - tidy_fn = resp_tidy_json, - tidy_args = list(subset_path = c("message", "items")), + tidy_policy = tidy_policy_json(subset_path = c("message", "items")), pagination_fn = iterate_with_json_cursor( param_name = "cursor", next_cursor_path = c("message", "next-cursor")