Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate req_prepare into req_setup and req_modify. #26

Merged
merged 1 commit into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading