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

[Feature]: Finalize Base Entrata API Request and Request Modifiers #5

Open
14 tasks
jimbrig opened this issue Nov 20, 2024 · 0 comments
Open
14 tasks
Assignees
Labels
config Configuration Management enhancement New feature or request feature New feature requests question Further information is requested refactor Code refactoring and cleanup

Comments

@jimbrig
Copy link
Member

jimbrig commented Nov 20, 2024

  • entrata_request(): Core Request Object

  • entrata_req_* Request Modifiers:

    • entrata_req_auth(): Add basic authentication via username and password
    • entrata_req_endpoint(): Adjust the target endpoint (path)
    • entrata_req_body(): Alter the request body
      • entrata_req_id(): Set the request ID in the request body
      • entrata_req_method_version(): Set the method object's version in the request body
      • entrata_req_method_params(): Add a method.params list object of params to the request body.
    • entrata_req_user_agent(): Sets / alters the user agent header
    • entrata_req_headers(): Add/alter request headers
    • entrata_req_config(): arbitrary mechanism to add configurable options to request (retry logic, error handler, logging, verbosity, caching, throttling, debugging, validation, etc.)
    • entrata_req_log(): pre-configure request logging
    • entrata_req_cache(): pre-configure the request to cache its response
    • entrata_req_validate(): validates the request in a variety of ways (R class/object is valid, endpoint is valid, conditional endpoint method name and method version is valid, method params are valid and coercible, JSON schema validation for request body, authentication included, etc.)

Questions:

  • Should it utilize its own, distinct S3 class on top of httr2_request and list?

Code:

#  ------------------------------------------------------------------------
#
# Title : Base Entrata HTTP Request
#    By : Jimmy Briggs
#  Date : 2024-11-10
#
#  ------------------------------------------------------------------------

#' Entrata Request
#'
#' @description
#' Initializes a default, custom, base [httr2::request()] object for calling the
#' GMH Communities Entrata API with the necessary configuration options,
#' including the `base_url`, authentication (i.e. `username` & `password`),
#' default headers, user agent, and error handling.
#'
#' @details
#' The derived [httr2::request()] object is configured with the base URL for the
#' GMH Communities Entrata API, the HTTP method set to POST, and the necessary
#' authentication headers for basic authentication.
#'
#' The function also sets the request headers to specify the content type and
#' accept type as JSON, and sets the user agent to identify the request as
#' coming from this package.
#'
#' @param entrata_config A list of configuration options for the GMH Communities
#'   Entrata API. By default will call [get_entrata_config()] to get the current
#'   configured values.
#'
#' @return A modified `entrata_request` and [httr2::request()] object, which is
#'   a list containing the request and configuration values as custom class
#'   attributes.
#'
#' @export
#'
#' @importFrom httr2 request req_method req_auth_basic req_headers req_user_agent
#' @importFrom httr2 req_error
entrata_request <- function(entrata_config = get_entrata_config()) {

  validate_entrata_config(entrata_config)

  # parse config
  base_url <- entrata_config$base_url # required
  username <- entrata_config$username # required
  password <- entrata_config$password # required
  user_agent <- entrata_config$user_agent %||% "gmhdatahub/0.0.1"

  # create request object
  req <- httr2::request(base_url) |>
    httr2::req_method("POST") |>
    httr2::req_auth_basic(username, password) |>
    httr2::req_headers(
      `Content-Type` = "application/json; charset=utf-8",
      `Accept` = "application/json",
    ) |>
    httr2::req_user_agent(user_agent) |>
    httr2::req_error(is_error = entrata_resp_is_error, body = entrata_resp_error_body)

  entrata_req <- structure(
    list(
      req = req,
      config = entrata_config
    ),
    class = c("entrata_request", "httr2_request")
  )

  return(entrata_req)

}

#' Print Method for Entrata Request
#'
#' @description
#' Prints the Entrata request object to the console.
#'
#' @param x An Entrata request object
#' @param ... Additional arguments passed to the print method
#'
#' @export
print.entrata_request <- function(x, ...) {

  cat("Entrata Request Object\n")
  cat("Base URL: ", x$config$base_url, "\n")
  cat("User Agent: ", x$config$user_agent, "\n")
  cat("HTTP Headers: ", x$req$headers, "\n")

  invisible(x)
}

# request modifiers -------------------------------------------------------

#' Entrata Request Modifiers
#'
#' @name entrata_request_modifiers
#'
#' @description
#' These functions are used to modify the Entrata request object before
#' sending the request to the API. These functions are used to set the
#' endpoint path, Entrata internal endpoint method (name, version, and params),
#' additional headers, authentication, request body, error handling, logging,
#' and more.
#'
#' @seealso
#' [entrata_request()], [entrata_req_auth()], [entrata_req_endpoint()],
#' [entrata_req_body()], [entrata_req_user_agent()], [entrata_req_headers()],
#' [entrata_req_config()], [entrata_req_log()]
NULL

#' Entrata Request Authentication
#'
#' @description
#' Adds basic authentication to the Entrata request object using provided credentials.
#'
#' @param req An [httr2::request()] object
#' @param username A character string representing the username for the Entrata API
#' @param password A character string representing the password for the Entrata API
#'
#' @return The modified request object with authentication headers.
#'
#' @export
#'
#' @importFrom httr2 req_auth_basic
entrata_req_auth <- function(
  req,
  username = NULL,
  password = NULL
) {

  check_request(req)

  username <- username %||% getOption("entrata.username") %||%
    Sys.getenv("ENTRATA_USERNAME") %||% get_entrata_config("username")

  password <- password %||% getOption("entrata.password") %||%
    Sys.getenv("ENTRATA_PASSWORD") %||% get_entrata_config("password")

  if (is.null(username) || is.null(password)) {
    cli::cli_abort(
      "Entrata API username and password must be provided."
    )
  }

  httr2::req_auth_basic(req, username, password)

}

#' Set Endpoint Path for Entrata Request
#'
#' @description
#' Appends an endpoint path to the request URL.
#'
#' @param req An httr2 request object
#' @param endpoint The API endpoint to set. Must be one of the valid Entrata
#'   endpoints. See [entrata_endpoints()] for a list of valid endpoints.
#'
#' @section Warning:
#'
#' This function will overwrite any existing endpoint path set on the request.
#'
#' @return Modified request object with the endpoint URL path appended
#'
#' @export
#'
#' @importFrom httr2 req_url_path_append req_url_path
#' @importFrom cli cli_alert_warning
entrata_req_endpoint <- function(req, endpoint) {

  # validate args
  check_request(req)
  check_endpoint(endpoint)

  pre_endpoint <- get_request_endpoint(req)

  if (is.null(pre_endpoint) || pre_endpoint == "") {
    return(httr2::req_url_path_append(req, endpoint))
  } else {
    cli::cli_alert_warning(
      "Overwriting existing endpoint path: {.field {pre_endpoint}} with {.field {endpoint}}."
    )
    return(httr2::req_url_path(req, endpoint))
  }

}

#' Entrata Request Body
#'
#' @description
#' Modifies the Entrata request object with the provided request body options.
#'
#' Note that the endpoint should be set before attempting to use this function
#' to get the best results.
#'
#' @param req An httr2 request object
#' @param id Request ID
#' @param method Request Endpoint Method (not HTTP method)
#' @param version Request Endpoint Method Version
#' @param params Request Endpoint Method Parameters
#'
#' @return The modified request object with the new request body options
#'
#' @export
#'
#' @importFrom purrr pluck compact
#' @importFrom cli cli_alert_warning
#' @importFrom httr2 req_body_json
entrata_req_body <- function(
    req,
    id = NULL,
    method = NULL,
    version = NULL,
    params = list(NULL)
) {

  check_request(req)

  # extract endpoint
  req_endpoint <- get_request_endpoint(req)

  # method, version, and params
  if (req_endpoint != "") {
    req_method <- method %||% get_default_method(endpoint = req_endpoint)
    check_endpoint_method(method = req_method, endpoint = req_endpoint)

    req_version <- version %||% get_default_version(method = req_method, endpoint = req_endpoint)
    check_endpoint_method_version(version = req_version, method = req_method, endpoint = req_endpoint)

    req_params <- params %||% get_default_params(method = req_method, endpoint = req_endpoint)
    check_endpoint_method_params(params = params, method = method, endpoint = req_endpoint)
  } else {
    cli::cli_alert_warning(
      "No endpoint set on request. Using provided method, version, and params without validation."
    )

    req_method <- method
    req_version <- version
    req_params <- params
  }

  # request ID
  req_id <- id %||% getOption("entrata.default_request_id") %||%
    Sys.getenv("ENTRATA_REQUEST_ID") %||%
    get_entrata_config("default_request_id") %||%
    as.integer(Sys.time())

  # get current request body
  pre_req_body <- purrr::pluck(req, "body", "data")

  if (!is.null(pre_req_body) > 0) {
    cli::cli_alert_warning(
      "Overwriting existing request body with new request body."
    )
  }

  # build new request body
  req_body <- list(
    auth = list(
      type = "basic"
    ),
    requestId = req_id,
    method = list(
      name = req_method,
      version = req_version,
      params = req_params
    )
  ) |>
    purrr::compact()

  # modify body and return
  req |>
    httr2::req_body_json(req_body)
}

#' Entrata Request User Agent
#'
#' @description
#' Modifies the Entrata request object with the provided user agent string.
#'
#' @param req An httr2 request object
#' @param string A character string representing the user agent to set on the request
#'   object. If not provided, will use the default user agent set in the Entrata
#'   configuration or the package version and URL.
#'
#' @return The modified request object with the new user agent string
#'
#' @export
#'
#' @importFrom httr2 req_user_agent
#' @importFrom utils packageVersion packageDescription
entrata_req_user_agent <- function(req, string) {

  check_request(req)

  ua <- string %||% getOption("entrata.user_agent") %||%
    Sys.getenv("ENTRATA_USER_AGENT") %||%
    get_entrata_config("user_agent") %||%
    paste0(
      "gmhdatahub/",
      utils::packageVersion("gmhdatahub"), " (",
      utils::packageDescription("gmhdatahub")$URL[[1]], ")"
    )

  req |>
    httr2::req_user_agent(ua)

}

#' Set Entrata Request Configuration
#'
#' @description
#' Modifies the Entrata request object with the provided configuration options.
#'
#' @param req An httr2 request object
#' @param ... Additional configuration options to set on the request object
#'
#' @return The modified request object with the new configuration options
#'
#' @export
entrata_req_config <- function(req, ...) {

  check_request(req)

  req$config <- modifyList(req$config, list(...))

  req
}

#' Set Entrata Request Headers
#'
#' @description
#' Modifies the Entrata request object with the provided request headers.
#'
#' @param req An httr2 request object
#' @param ... Additional headers to set on the request object
#'
#' @return The modified request object with the new request headers
#'
#' @export
#'
#' @importFrom httr2 req_headers
entrata_req_headers <- function(req, ...) {

  check_request(req)

  req |>
    httr2::req_headers(...)

}
@jimbrig jimbrig self-assigned this Nov 20, 2024
@jimbrig jimbrig added config Configuration Management enhancement New feature or request feature New feature requests question Further information is requested refactor Code refactoring and cleanup labels Nov 20, 2024
@jimbrig jimbrig pinned this issue Nov 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
config Configuration Management enhancement New feature or request feature New feature requests question Further information is requested refactor Code refactoring and cleanup
Projects
None yet
Development

No branches or pull requests

1 participant