Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
asadow committed Oct 30, 2023
1 parent 109de54 commit 5076db7
Show file tree
Hide file tree
Showing 29 changed files with 515 additions and 515 deletions.
13 changes: 6 additions & 7 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Generated by roxygen2: do not edit by hand

export(body_parse)
export(extract_criteria)
export(extract_data)
export(extract_schema)
export(format_params)
export(get_env_key)
export(get_env_url)
Expand All @@ -12,12 +14,9 @@ export(mm_get)
export(mm_req_paginate)
export(mm_req_perform)
export(mm_request)
export(mm_resp_parse)
export(mm_set_creds)
export(parsed_keep_df)
export(parsed_to_tbl)
export(parsed_extract)
export(remove_api_urls)
export(resp_body_parse)
export(to_tbl_criteria)
export(to_tbl_data)
export(to_tbl_schema)
export(req_opts)
importFrom(lifecycle,deprecated)
26 changes: 17 additions & 9 deletions R/checks.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,11 @@ check_creds <- function(){
check_url <- function(x,
arg = rlang::caller_arg(x),
call = rlang::caller_env()) {

check_string(x)

base_url <- "https://api.megamation.com/"

if (!startsWith(x, base_url)){
if (!startsWith(x, base_url)) {
cli::cli_abort(
"{.arg {arg}} must be of the form {.val {base_url}<institution ID>/dl},
not {x}."
Expand All @@ -60,7 +59,7 @@ check_url <- function(x,
check_bool <- function(x,
arg = rlang::caller_arg(x),
call = rlang::caller_env()) {
if(!rlang::is_bool(x)){
if (!rlang::is_bool(x)) {
cli::cli_abort(
"{.arg {arg}} must be
either {.val {TRUE}} or {.val {FALSE}}, not {x}."
Expand All @@ -74,9 +73,7 @@ check_string <- function(x,
arg = rlang::caller_arg(x),
call = rlang::caller_env(),
optional = FALSE) {
if(optional && is.null(x)){return()}

if(!rlang::is_string(x)){
if (!rlang::is_string(x)) {
cli::cli_abort(
"{.arg {arg}} must be a single string.",
call = call
Expand All @@ -89,12 +86,23 @@ check_string <- function(x,
check_date <- function(x,
arg = rlang::caller_arg(x),
call = rlang::caller_env()) {
if (!lubridate::is.Date(x)) {
cli::cli_abort(
"{.arg {arg}} must be a Date, not {.obj_type_friendly {x}}.",
call = call
)
}

if(is.null(x)){return()}
if (length(x) == 0) {
cli::cli_abort(
"{.arg {arg}} must not have length 0.",
call = call
)
}

if(!lubridate::is.Date(x)){
if (any(is.na(x))) {
cli::cli_abort(
"{.arg {arg}} must be a Date, not {.obj_type_friendly {x}}.",
"{.arg {arg}} must not contain {.val NA}.",
call = call
)
}
Expand Down
6 changes: 2 additions & 4 deletions R/creds.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#' recovery purposes.
#'
#' @param key The API key provided to you by Megamation formatted in quotes.
#' @param url The API URL provided to you by Megamation
#' @param url The API base URL provided to you by Megamation
#' formatted in quotes.
#' @param overwrite If TRUE, will overwrite existing Megamation
#' credentials that you already have in your `.Renviron` file.
Expand All @@ -41,9 +41,7 @@
#' # Reload your environment to use the credentials
#' }
#' @export
mm_set_creds <- function(key,
url,
overwrite = FALSE) {
mm_set_creds <- function(key, url, overwrite = FALSE) {
check_string(key)
check_bool(overwrite)
url <- check_url(url)
Expand Down
36 changes: 36 additions & 0 deletions R/opts.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#' Specify request options
#'
#' @param .get A length-1 character vector representing whether the request is
#' for the endpoint's `"data"`, `"criteria"`, `"labels"`, or `"schema"`.
#' @param .url API base URL for request.
#' @param .key API key for request.
#' @param .paginate If `TRUE`, paginate the request.
#' @export
req_opts <- function(.get = "data",
.url = get_env_url(),
.key = get_env_key(),
.paginate = TRUE) {
if(.key != get_env_key()) {
cli::cli_warn(c(
"The {.arg .key} you provided is not your
MEGAMATION_KEY environment variable.",
"i" = "It is highly recommended that you run {.fun mm_set_creds},
and {.emph do not} supply {.arg .key}.",
"i" = 'A typo like `kee = <your-secret>`
will end up in the request URL as a filter.'
))
}
check_string(.get)
.get <- rlang::arg_match(.get, c("criteria", "labels", "schema", "data"))

structure(
list(
.get = .get,
.url = .url,
.key = .key,
.paginate = .paginate
),
class = "megamation_req_opts"
)
}

11 changes: 7 additions & 4 deletions R/paginate.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mm_req_paginate <- function(req) {
req |>
httr2::req_paginate_next_url(
parse_resp = function(resp) {
parsed <- resp |> resp_body_parse()
parsed <- resp |> mm_resp_parse()

url <- parsed$next_page
url <- if (is.null(url) || url == "") NULL else (url)
Expand All @@ -27,8 +27,8 @@ mm_req_paginate <- function(req) {

#' Bind multiple Megamation API pages by row before converting to a tibble
#'
#' `mm_bind_then_tbl()` is needed as pages can have same-named columns with
#' different types. This is because some column(s) of a given page
#' `mm_bind_then_tbl()` is needed as pages can have same-named fields with
#' different types. This is because some field(s) of a given page
#' may or may not contain vectors of values in one of its row.
#' `mm_bind_then_tbl()` takes care of this possibility by treating each page
#' as a matrix before binding and unnesting their combination.
Expand Down Expand Up @@ -89,7 +89,7 @@ mm_req_perform_paginate_custom <- function(req, max_pages = NULL) {
i <- 1L

repeat({
out[[i]] <- httr2::req_perform(req) |> body_parse()
out[[i]] <- httr2::req_perform(req) |> mm_resp_parse()
if (!is.null(max_pages) && i == max_pages) {
break
}
Expand All @@ -114,6 +114,9 @@ mm_req_perform_paginate_custom <- function(req, max_pages = NULL) {
}

#' Detect whether a request is paginated
#'
#' `is_paginated()` checks whether an `httr2_request` has a paginate policy.
#'
#' @param req An API request.
#' @returns `TRUE` or `FALSE`.
#' @export
Expand Down
119 changes: 37 additions & 82 deletions R/parse.R
Original file line number Diff line number Diff line change
@@ -1,125 +1,80 @@
#' Parse body from Megamation API response
#'
#' `body_parse()` converts the raw bytes of an API response to an R list object.
#' After converting bytes to characters, encoding is done to resolve
#' a UTF-8 issue from Megamation's side.
#'
#' @param resp_body An API response body.
#' @returns A list.
#' @export
body_parse <- function(resp_body) {
resp_body |>
rawToChar() |>
stringi::stri_encode(from = "UTF-8", to = "UTF-8") |>
jsonlite::fromJSON()
}

#' Extract and parse body from Megamation API response
#'
#' `resp_body_parse()` extracts the raw bytes from an API response and parses
#' it, returning an R list object.
#' `mm_resp_parse()` extracts the raw bytes from an API response and parses
#' it, returning an R list object. After converting bytes to characters, encoding is done to resolve
#' a UTF-8 issue from Megamation's side.
#'
#' @param resp An API response.
#' @description The body of the response contains raw bytes.
#' After converting these bytes to a string, encoding is done to resolve
#' a UTF-8 issue from Megamation's side.
#' @returns A list.
#' @export
resp_body_parse <- function(resp) {
mm_resp_parse <- function(resp) {
resp |>
httr2::resp_body_raw() |>
body_parse()
rawToChar() |>
stringi::stri_encode(from = "UTF-8", to = "UTF-8") |>
jsonlite::fromJSON()
}

#' Create tibble from parsed response
#'
#' @description
#'
#' `parsed_to_tbl()` constructs a data frame of class [tbl_df].
#' The input should be the parsed API response body list.
#'
#' `parsed_to_tbl()` uses the following methods:
#'
#' * `to_tbl_data()` converts the Get Data parsed body, but only keeps the
#' embedded data frame.
#' * `to_tbl_criteria()` converts the Get Criteria or Labels parsed body.
#' * `to_tbl_schema()` converts the Get Schema parsed body.
#'
#' `parsed_extract()` extracts a data frame from the parsed
#' response body.
#'
#' @param parsed Parsed response body.
#' @inheritParams mm_request
#' @returns A data frame of class [tbl_df] containing either
#' * Columns of the endpoint data.
#' * Columns `list_name` and `value` representing the list given by the
#' endpoint's Criteria, Labels, or Schema. If Schema,
#' `value` is of type `list` because of the lists under names `required` and
#' `properties`.
#' @returns A data frame containing the endpoint data.
#'
#' @export
parsed_to_tbl <- function(parsed,
parsed_extract <- function(parsed,
.get = c("data", "criteria", "labels", "schema")) {
.get <- rlang::arg_match(.get)

switch(
.get,
data = to_tbl_data(parsed),
data = extract_data(parsed),
labels = ,
criteria = to_tbl_criteria(parsed),
schema = to_tbl_schema(parsed)
criteria = extract_criteria(parsed),
schema = extract_schema(parsed)
)
}

#' @rdname parsed_to_tbl
#' @rdname parsed_extract
#' @export
to_tbl_data <- function(parsed) {
extract_data <- function(parsed) {
parsed |>
parsed_keep_df() |>
tibble::as_tibble()
}

#' @rdname parsed_to_tbl
#' @export
to_tbl_criteria <- function(parsed) {
tibble::tibble(
list_name = names(parsed),
value = unlist(parsed)
)
purrr::list_flatten() |>
purrr::keep(\(x) is.data.frame(x)) |>
purrr::pluck(1)
}

#' @rdname parsed_to_tbl
#' @rdname parsed_extract
#' @export
to_tbl_schema <- function(parsed) {

list_name <- type <- NULL

properties <- parsed$properties

tbl <- tibble::tibble(
list_name = names(parsed),
value = unname(parsed)
) |>
dplyr::filter(list_name != "properties")
extract_criteria <- function(parsed) {
not_cols <- c("Table", "Criteria", "Usage")

properties <- tibble::tibble(
column = names(properties),
description = purrr::map_chr(properties, "description"),
type = purrr::map(properties, "type")
data.frame(
field = names(parsed) |> tolower(),
description = unlist(parsed)
) |>
tidyr::unnest(type)

tbl |>
dplyr::add_row(
list_name = "col_properties",
value = list(properties)
)
dplyr::filter(!field %in% !!not_cols)
}

#' @rdname parsed_to_tbl
#' @rdname parsed_extract
#' @export
parsed_keep_df <- function(parsed) {
parsed |>
purrr::list_flatten() |>
purrr::keep(\(x) is.data.frame(x)) |>
purrr::pluck(1)
extract_schema <- function(parsed) {
p <- parsed$properties

tibble::tibble(
field = names(p) |> tolower(),
description = purrr::map_chr(p, "description"),
type = purrr::map(p, "type")
) |>
tidyr::unnest(type) |>
as.data.frame()
}

26 changes: 7 additions & 19 deletions R/perform.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,22 @@
#' @returns A data frame of class [`tbl_df`][tibble::tbl_df-class]
#' containing the requested information.
#' @export
mm_get <- function(endpoint,
...,
.get = "data",
.paginate = TRUE,
.url = get_env_url(),
.key = get_env_key()) {
.get <- rlang::arg_match(.get, c("data", "criteria", "labels", "schema"))
mm_get <- function(endpoint, ..., opts = req_opts()) {

req <- mm_request(
endpoint,
...,
.get = .get,
.paginate = .paginate,
.url = .url,
.key = .key
)
req <- mm_request(endpoint, ..., opts = opts)
resp <- mm_req_perform(req)

tbl_result <- if (!is_paginated(req)) {
resp[[1]] |>
resp_body_parse() |>
parsed_to_tbl(.get)
mm_resp_parse() |>
parsed_extract(.get) |>
tibble::as_tibble()
} else {
resp |>
purrr::map(
\(x) x |>
resp_body_parse() |>
parsed_keep_df()
mm_resp_parse() |>
extract_data()
) |>
mm_bind_then_tbl()
}
Expand Down
Loading

0 comments on commit 5076db7

Please sign in to comment.