Skip to content

Commit

Permalink
Separate req_prepare into req_setup and req_modify. (#26)
Browse files Browse the repository at this point in the history
Closes #19.
Closes #5.
Closes #11.
Closes #17.
  • Loading branch information
jonthegeek authored Apr 25, 2024
1 parent d5fd891 commit acf6323
Show file tree
Hide file tree
Showing 26 changed files with 298 additions and 131 deletions.
2 changes: 2 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Description: An opinionated framework for use within api-wrapping R
License: MIT + file LICENSE
URL: https://nectar.api2r.org, https://github.com/jonthegeek/nectar
BugReports: https://github.com/jonthegeek/nectar/issues
Depends:
R (>= 3.5.0)
Imports:
cli,
curl,
Expand Down
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ S3method(resp_parse,list)
export(call_api)
export(compact_nested_list)
export(do_if_defined)
export(req_modify)
export(req_perform_opinionated)
export(req_prepare)
export(req_setup)
export(resp_parse)
export(security_api_key)
export(stabilize_string)
export(url_normalize)
export(url_path_append)
importFrom(cli,cli_abort)
importFrom(fs,path)
Expand Down
36 changes: 2 additions & 34 deletions R/aaa_shared.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,16 @@
#'
#' Reused parameter definitions are gathered here for easier editing.
#'
#' @param base_url The part of the url that is shared by all calls to the API.
#' In some cases there may be a family of base URLs, from which you will need
#' to choose one.
#' @param body An object to use as the body of the request. If any component of
#' the body is a path, pass it through [fs::path()] or otherwise give it the
#' class "fs_path" to indicate that it is a path.
#' @param case The case standard to apply. The possible values are
#' self-descriptive. Defaults to "snake_case".
#' @param depth The current recursion depth.
#' @param method If the method is something other than GET or POST, supply it.
#' Case is ignored.
#' @param mime_type A character scalar indicating the mime type of any files
#' present in the body. Some APIs allow you to leave this as NULL for them to
#' guess.
#' @param path The route to an API endpoint. Optionally, a list with the path
#' plus variables to [glue::glue()] into the path.
#' @param query An optional list of parameters to pass in the query portion of
#' the request.
#' @param req An [httr2::request()] object.
#' @param response_parser A function to parse the server response (`resp`).
#' Defaults to [httr2::resp_body_json()], since JSON responses are common. Set
#' this to `NULL` to return the raw response from [httr2::req_perform()].
#' @param response_parser_args An optional list of arguments to pass to the
#' `response_parser` function (in addition to `resp`).
#' @param security_fn A function to use to authenticate the request. By default
#' (`NULL`), no authentication is performed.
#' @param security_args An optional list of arguments to the `security_fn`
#' function.
#' @param user_agent A string to identify where this request is coming from.
#' It's polite to set the user agent to identify your package, such as
#' "MyPackage (https://mypackage.com)".
#' @param req A [httr2::request()] object.
#' @param x The object to update.
#'
#' @importFrom fs path
#'
#' @name .shared-parameters
#' @keywords internal
NULL

#' Returns from request functions
#'
#' @return An [httr2::request()] object.
#' @return A [httr2::request()] object.
#' @name .shared-request
#' @keywords internal
NULL
24 changes: 19 additions & 5 deletions R/call.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,25 @@
#' [httr2::req_perform()] and [httr2::req_perform_iterative()], and, by default,
#' [httr2::resp_body_json()].
#'
#' @inheritParams .shared-parameters
#' @seealso [req_setup()], [req_modify()], [req_perform_opinionated()],
#' [resp_parse()], and [do_if_defined()] for finer control of the process.
#'
#' @inheritParams rlang::args_dots_empty
#' @inheritParams req_setup
#' @inheritParams req_modify
#' @inheritParams req_perform_opinionated
#' @inheritParams resp_parse
#' @param security_fn A function to use to authenticate the request. By default
#' (`NULL`), no authentication is performed.
#' @param security_args An optional list of arguments to the `security_fn`
#' function.
#' @param response_parser_args An optional list of arguments to pass to the
#' `response_parser` function (in addition to `resp`).
#'
#' @return The response from the API, parsed by the `response_parser`.
#' @export
call_api <- function(base_url,
...,
path = NULL,
query = NULL,
body = NULL,
Expand All @@ -25,14 +38,15 @@ call_api <- function(base_url,
max_reqs = Inf,
max_tries_per_req = 3,
user_agent = "nectar (https://nectar.api2r.org)") {
req <- req_prepare(
base_url = base_url,
rlang::check_dots_empty()
req <- req_setup(base_url, user_agent = user_agent)
req <- req_modify(
req,
path = path,
query = query,
body = body,
mime_type = mime_type,
method = method,
user_agent = user_agent
method = method
)
req <- do_if_defined(req, security_fn, !!!security_args)
resp <- req_perform_opinionated(
Expand Down
1 change: 1 addition & 0 deletions R/nectar-package.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

## usethis namespace: start
#' @importFrom cli cli_abort
#' @importFrom fs path
#' @importFrom httr2 req_perform
#' @importFrom httr2 req_perform_iterative
#' @importFrom rlang :=
Expand Down
1 change: 0 additions & 1 deletion R/perform.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#' then performs the request, using either [httr2::req_perform_iterative()] (if
#' a `next_req` function is supplied) or [httr2::req_perform()] (if not).
#'
#' @inheritParams .shared-parameters
#' @inheritParams httr2::req_perform_iterative
#' @inheritParams rlang::args_dots_empty
#' @param next_req An optional function that takes the previous response
Expand Down
108 changes: 90 additions & 18 deletions R/req.R
Original file line number Diff line number Diff line change
@@ -1,13 +1,87 @@
#' Setup a basic API request
#'
#' For a given API, the `base_url` and `user_agent` will almost always be the
#' same. Use this function to prepare that piece of the request once for easy
#' reuse.
#'
#' @inheritParams rlang::args_dots_empty
#' @param base_url The part of the url that is shared by all calls to the API.
#' In some cases there may be a family of base URLs, from which you will need
#' to choose one.
#' @param user_agent A string to identify where this request is coming from.
#' It's polite to set the user agent to identify your package, such as
#' "MyPackage (https://mypackage.com)".
#'
#' @inherit .shared-request return
#' @export
#'
#' @examples
#' req_setup("https://example.com")
#' req_setup(
#' "https://example.com",
#' user_agent = "my_api_client (https://my.api.client)"
#' )
req_setup <- function(base_url,
...,
user_agent = "nectar (https://nectar.api2r.org)") {
req <- httr2::request(base_url)
req <- httr2::req_user_agent(req, user_agent)
return(req)
}

#' Modify an API request for a particular endpoint
#'
#' Modify the basic request for an API by adding a path and any other
#' path-specific properties.
#'
#' @inheritParams rlang::args_dots_empty
#' @inheritParams .req_body_auto
#' @inheritParams .shared-parameters
#' @param method If the method is something other than GET or POST, supply it.
#' Case is ignored.
#' @param path The route to an API endpoint. Optionally, a list with the path
#' plus variables to [glue::glue()] into the path.
#' @param query An optional list of parameters to pass in the query portion of
#' the request.
#'
#' @inherit .shared-request return
#' @export
#'
#' @examples
#' req_base <- req_setup(
#' "https://example.com",
#' user_agent = "my_api_client (https://my.api.client)"
#' )
#' req <- req_modify(req_base, path = c("specific/{path}", path = "endpoint"))
#' req
#' req <- req_modify(req, query = c("param1" = "value1", "param2" = "value2"))
#' req
req_modify <- function(req,
...,
path = NULL,
query = NULL,
body = NULL,
mime_type = NULL,
method = NULL) {
rlang::check_dots_empty()
req <- .req_path_append(req, path)
req <- .req_query_flatten(req, query)
req <- .req_body_auto(req, body, mime_type)
req <- .req_method_apply(req, method)
return(req)
}

#' Prepare a request for an API
#'
#' This function implements an opinionated framework for preparing an API
#' request. It is intended to be used inside an API client package. It serves as
#' a wrapper around the `req_` family of functions, such as [httr2::request()].
#'
#' @inheritParams .shared-parameters
#' @inheritParams req_setup
#' @inheritParams req_modify
#' @inheritParams rlang::args_dots_empty
#'
#' @return A [httr2::request()] object.
#' @inherit .shared-request return
#' @export
req_prepare <- function(base_url,
...,
Expand All @@ -18,31 +92,29 @@ req_prepare <- function(base_url,
method = NULL,
user_agent = "nectar (https://nectar.api2r.org)") {
rlang::check_dots_empty()
req <- httr2::request(base_url)
req <- .req_path_append(req, path)
req <- .req_query_flatten(req, query)
req <- .req_body_auto(req, body, mime_type)
req <- .req_method_apply(req, method)
req <- httr2::req_user_agent(req, user_agent)
req <- req_setup(base_url, user_agent = user_agent)
req <- req_modify(
req,
path = path,
query = query,
body = body,
mime_type = mime_type,
method = method
)
return(req)
}

.req_path_append <- function(req, path) {
if (length(path)) {
path <- rlang::inject(glue::glue(!!!path))
req <- httr2::req_url_path_append(req, path)
}
return(httr2::req_url_path_append(req, path))
return(req)
}

.req_method_apply <- function(req, method) {
if (!length(method)) {
# I'm pretty sure this is a current httr2 or httptest2 bug. NULL methods
# fail during testing.
if (length(req$body)) {
method <- "POST"
} else {
method <- "GET"
}
if (length(method)) {
return(httr2::req_method(req, method))
}
return(httr2::req_method(req, method))
return(req)
}
6 changes: 6 additions & 0 deletions R/req_body.R
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
#' currently experimental and needs to be tested on more APIs.
#'
#' @inheritParams .shared-parameters
#' @param body An object to use as the body of the request. If any component of
#' the body is a path, pass it through [fs::path()] or otherwise give it the
#' class "fs_path" to indicate that it is a path.
#' @param mime_type A character scalar indicating the mime type of any files
#' present in the body. Some APIs allow you to leave this as NULL for them to
#' guess.
#'
#' @inherit httr2::req_body_json return
#' @keywords internal
Expand Down
9 changes: 6 additions & 3 deletions R/resp.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
#' (in addition to `resp`).
#'
#' @return The response parsed by the `response_parser`. If `resp` was a list,
#' the parsed responses are concatenated. See [httr2::resps_data()] for
#' examples.
#' the parsed responses are concatenated when possible. Unlike
#' [httr2::resps_data], this function does not concatenate raw vector
#' responses.
#' @export
resp_parse <- function(resp, ...) {
UseMethod("resp_parse")
Expand All @@ -42,7 +43,9 @@ resp_parse.default <- function(resp,
)
}

#' @inheritParams .shared-parameters
#' @param response_parser A function to parse the server response (`resp`).
#' Defaults to [httr2::resp_body_json()], since JSON responses are common. Set
#' this to `NULL` to return the raw response from [httr2::req_perform()].
#' @export
#' @rdname resp_parse
resp_parse.httr2_response <- function(resp,
Expand Down
27 changes: 25 additions & 2 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ compact_nested_list <- function(lst) {
#' Discard empty elements
#'
#' @param lst A (nested) list to filter.
#' @inheritParams .shared-parameters
#' @param depth The current recursion depth.
#'
#' @return The list, minus empty elements and branches.
#' @keywords internal
Expand Down Expand Up @@ -56,6 +56,29 @@ url_path_append <- function(url, ...) {
return(httr2::url_build(url))
}

#' Normalize a URL
#'
#' This function normalizes a URL by adding a trailing slash to the base if it
#' is missing. It is mainly for testing and other comparisons.
#'
#' @param url A URL to normalize.
#'
#' @return A normalized URL
#' @export
#'
#' @examples
#' identical(
#' url_normalize("https://example.com"),
#' url_normalize("https://example.com/")
#' )
#' identical(
#' url_normalize("https://example.com?param=value"),
#' url_normalize("https://example.com/?param=value")
#' )
url_normalize <- function(url) {
url_path_append(url)
}

.path_merge <- function(...) {
path <- paste(c(...), collapse = "/")
path <- sub("^([^/])", "/\\1", path)
Expand All @@ -70,7 +93,7 @@ url_path_append <- function(url, ...) {
#' one endpoint might use a special security function that isn't used by other
#' endpoints. This function exists to make coding such situations easier.
#'
#' @param x An object to potentially modify, such as an [httr2::request()]
#' @param x An object to potentially modify, such as a [httr2::request()]
#' object.
#' @param fn A function to apply to `x`. If `fn` is `NULL`, `x` is returned
#' unchanged.
Expand Down
7 changes: 7 additions & 0 deletions man/call_api.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/do_if_defined.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit acf6323

Please sign in to comment.